diff --git a/.gitignore b/.gitignore index e2ff74a..c0bff53 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ redis/ dist/ build/ -docker-compose.yml \ No newline at end of file +docker-compose.yml + +ucaptcha-config.yaml \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 8ca546d..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/cvsa.iml b/.idea/cvsa.iml index 7bfcf20..916ca6a 100644 --- a/.idea/cvsa.iml +++ b/.idea/cvsa.iml @@ -28,6 +28,8 @@ + + diff --git a/bun.lock b/bun.lock index 5dee910..251cf70 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "hono": "^4.7.8", "hono-rate-limiter": "^0.4.2", "ioredis": "^5.6.1", + "limiter": "^3.0.0", "postgres": "^3.4.5", "rate-limit-redis": "^4.2.0", "yup": "^1.6.1", @@ -32,6 +33,7 @@ "packages/core": { "name": "core", "dependencies": { + "@koshnic/ratelimit": "^1.0.3", "chalk": "^5.4.1", "ioredis": "^5.6.1", "logform": "^2.7.0", @@ -233,6 +235,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@koshnic/ratelimit": ["@koshnic/ratelimit@1.0.3", "", { "dependencies": { "@types/chai": "^4.3.9", "@types/mocha": "^10.0.3", "chai": "^4.3.10", "ioredis": "^5.3.2", "mocha": "^10.2.0" } }, "sha512-cfDcSc+I+M4hNM+/4M+lfn8UuTq4OEFKl78ThOcGNaO7g8tWb1vm2qVpV1p1loYao1mqk00NBNwHQu2E/qFq2g=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], @@ -369,6 +373,8 @@ "@types/bun": ["@types/bun@1.2.11", "", { "dependencies": { "bun-types": "1.2.11" } }, "sha512-ZLbbI91EmmGwlWTRWuV6J19IUiUC5YQ3TCEuSHI3usIP75kuoA8/0PVF+LTrbEnVc8JIhpElWOxv1ocI1fJBbw=="], + "@types/chai": ["@types/chai@4.3.20", "", {}, "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], @@ -381,6 +387,8 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mocha": ["@types/mocha@10.0.10", "", {}, "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], @@ -417,6 +425,8 @@ "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -455,6 +465,8 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -463,10 +475,14 @@ "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="], + "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], "bullmq": ["bullmq@5.52.1", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-u7CSV9wID3MBEX2DNubEErbAlrADgm8abUBAi6h8rQTnuTkhhgMs2iD7uhqplK8lIgUOkBIW3sDJWaMSInH47A=="], @@ -499,7 +515,7 @@ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -567,6 +583,8 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], + "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="], "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], @@ -677,8 +695,14 @@ "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], + "flatbuffers": ["flatbuffers@25.2.10", "", {}, "sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw=="], "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], @@ -699,6 +723,8 @@ "frontend": ["frontend@workspace:packages/frontend"], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -707,13 +733,17 @@ "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob-regex": ["glob-regex@0.3.2", "", {}, "sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw=="], @@ -761,6 +791,8 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "hono": ["hono@4.7.8", "", {}, "sha512-PCibtFdxa7/Ldud9yddl1G81GjYaeMYYTq4ywSaNsYbB1Lug4mwtOMJf2WXykL0pntYwmpRJeOI3NmoDgD+Jxw=="], "hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="], @@ -777,6 +809,8 @@ "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="], @@ -787,13 +821,21 @@ "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -803,6 +845,8 @@ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -845,16 +889,22 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], + "limiter": ["limiter@3.0.0", "", {}, "sha512-hev7DuXojsTFl2YwyzUJMDnZ/qBDd3yZQLSH3aD4tdL1cqfc3TMnoecEJtWFaQFdErZsKoFMBTxF/FBSkgDbEg=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -971,7 +1021,7 @@ "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -981,6 +1031,8 @@ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "mocha": ["mocha@10.8.2", "", { "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", "chokidar": "^3.5.3", "debug": "^4.3.5", "diff": "^5.2.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^8.1.0", "he": "^1.2.0", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^5.1.6", "ms": "^2.1.3", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^6.5.1", "yargs": "^16.2.0", "yargs-parser": "^20.2.9", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1047,6 +1099,8 @@ "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-queue": ["p-queue@8.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw=="], "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], @@ -1065,6 +1119,8 @@ "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -1135,6 +1191,8 @@ "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "rate-limit-redis": ["rate-limit-redis@4.2.0", "", { "peerDependencies": { "express-rate-limit": ">= 6" } }, "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA=="], @@ -1143,7 +1201,7 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "recrawl-sync": ["recrawl-sync@2.2.3", "", { "dependencies": { "@cush/relative": "^1.0.0", "glob-regex": "^0.3.0", "slash": "^3.0.0", "sucrase": "^3.20.3", "tslib": "^1.9.3" } }, "sha512-vSaTR9t+cpxlskkdUFrsEpnf67kSmPk66yAGT1fZPrDudxQjoMzPgQhSMImQ0pAw5k0NPirefQfhopSjhdUtpQ=="], @@ -1211,6 +1269,8 @@ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], "server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="], @@ -1279,6 +1339,8 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -1315,6 +1377,8 @@ "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="], @@ -1337,6 +1401,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -1429,6 +1495,8 @@ "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "workerpool": ["workerpool@6.5.1", "", {}, "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1447,6 +1515,8 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-unparser": ["yargs-unparser@2.0.0", "", { "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" } }, "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA=="], + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], "yocto-spinner": ["yocto-spinner@0.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-21rPcM3e4vCpOXThiFRByX8amU5By1R0wNS8Oex+DP3YgC8xdU0vEJ/K8cbPLiIJVosSSysgcFof6s6MSD5/Vw=="], @@ -1473,6 +1543,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@koshnic/ratelimit/chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], @@ -1503,28 +1575,42 @@ "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], "jake/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "mocha/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "mocha/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="], + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "recrawl-sync/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -1533,6 +1619,8 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "yargs-unparser/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "@huggingface/transformers/onnxruntime-node/onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -1541,6 +1629,16 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@koshnic/ratelimit/chai/assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "@koshnic/ratelimit/chai/check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "@koshnic/ratelimit/chai/deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "@koshnic/ratelimit/chai/loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "@koshnic/ratelimit/chai/pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + "astro/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "astro/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -1587,14 +1685,18 @@ "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "jake/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "jake/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "mocha/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], @@ -1603,6 +1705,10 @@ "pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -1615,6 +1721,8 @@ "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "mocha/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], } } diff --git a/deno.json b/deno.json deleted file mode 100644 index 20c75ba..0000000 --- a/deno.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "lock": false, - "workspace": ["./packages/crawler", "./packages/core"], - "nodeModulesDir": "auto", - "tasks": { - "crawler": "deno task --filter 'crawler' all", - "backend": "deno task --filter 'backend' start" - }, - "fmt": { - "useTabs": true, - "lineWidth": 120, - "indentWidth": 4, - "semiColons": true, - "proseWrap": "always" - } -} diff --git a/packages/backend/db/config.ts b/packages/backend/db/config.ts deleted file mode 100644 index 59ab5fa..0000000 --- a/packages/backend/db/config.ts +++ /dev/null @@ -1,46 +0,0 @@ -const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT", "DB_NAME_CRED"]; - -const unsetVars = requiredEnvVars.filter((key) => process.env[key] === undefined); - -if (unsetVars.length > 0) { - throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`); -} - -const databaseHost = process.env["DB_HOST"]!; -const databaseName = process.env["DB_NAME"]; -const databaseNameCred = process.env["DB_NAME_CRED"]!; -const databaseUser = process.env["DB_USER"]!; -const databasePassword = process.env["DB_PASSWORD"]!; -const databasePort = process.env["DB_PORT"]!; - -export const postgresConfig = { - hostname: databaseHost, - port: parseInt(databasePort), - database: databaseName, - user: databaseUser, - password: databasePassword -}; - -export const postgresConfigNpm = { - host: databaseHost, - port: parseInt(databasePort), - database: databaseName, - username: databaseUser, - password: databasePassword -}; - -export const postgresCredConfigNpm = { - host: databaseHost, - port: parseInt(databasePort), - database: databaseNameCred, - username: databaseUser, - password: databasePassword -} - -export const postgresConfigCred = { - hostname: databaseHost, - port: parseInt(databasePort), - database: databaseNameCred, - user: databaseUser, - password: databasePassword -}; diff --git a/packages/backend/db/db.ts b/packages/backend/db/db.ts deleted file mode 100644 index 9643fa5..0000000 --- a/packages/backend/db/db.ts +++ /dev/null @@ -1,5 +0,0 @@ -import postgres from "postgres"; -import { postgresConfigNpm, postgresCredConfigNpm } from "./config"; - -export const sql = postgres(postgresConfigNpm); -export const sqlCred = postgres(postgresCredConfigNpm) \ No newline at end of file diff --git a/packages/backend/db/videoSnapshot.ts b/packages/backend/db/snapshots.ts similarity index 97% rename from packages/backend/db/videoSnapshot.ts rename to packages/backend/db/snapshots.ts index d5b8fa5..83ea7b5 100644 --- a/packages/backend/db/videoSnapshot.ts +++ b/packages/backend/db/snapshots.ts @@ -1,4 +1,4 @@ -import { sql } from "./db"; +import { sql } from "@core/db/dbNew"; import type { VideoSnapshotType } from "@core/db/schema.d.ts"; export async function getVideoSnapshots( diff --git a/packages/backend/lib/auth/captchaDifficulty.ts b/packages/backend/lib/auth/captchaDifficulty.ts new file mode 100644 index 0000000..4dd270e --- /dev/null +++ b/packages/backend/lib/auth/captchaDifficulty.ts @@ -0,0 +1,62 @@ +import { Psql } from "@core/db/psql"; +import { SlidingWindow } from "@core/mq/slidingWindow.ts"; +import { redis } from "@core/db/redis.ts"; +import { getIdentifier } from "@/middleware/rateLimiters.ts"; +import { Context } from "hono"; + +type seconds = number; + +export interface CaptchaDifficultyConfig { + global: boolean; + duration: seconds; + threshold: number; + difficulty: number; +} + +export const getCaptchaDifficultyConfigByRoute = async (sql: Psql, route: string): Promise => { + return sql` + SELECT duration, threshold, difficulty, global + FROM captcha_difficulty_settings + WHERE CONCAT(method, '-', path) = ${route} + ORDER BY duration + `; +}; + +export const getCaptchaConfigMaxDuration = async (sql: Psql, route: string): Promise => { + const rows = await sql<{max: number}[]>` + SELECT MAX(duration) + FROM captcha_difficulty_settings + WHERE CONCAT(method, '-', path) = ${route} + `; + if (rows.length < 1){ + return Number.MAX_SAFE_INTEGER; + } + return rows[0].max; +} + + +export const getCurrentCaptchaDifficulty = async (sql: Psql, c: Context | string): Promise => { + const isRoute = typeof c === "string"; + const route = isRoute ? c : `${c.req.method}-${c.req.path}` + const configs = await getCaptchaDifficultyConfigByRoute(sql, route); + if (configs.length < 1) { + return null + } + else if (configs.length == 1) { + return configs[0].difficulty + } + const maxDuration = configs.reduce((max, config) => + Math.max(max, config.duration), 0); + const slidingWindow = new SlidingWindow(redis, maxDuration); + for (let i = 1; i < configs.length; i++) { + const config = configs[i]; + const lastConfig = configs[i - 1]; + const identifier = isRoute ? c : getIdentifier(c, config.global); + const count = await slidingWindow.count(`captcha-${identifier}`, config.duration); + if (count >= config.threshold) { + continue; + } + return lastConfig.difficulty + } + return configs[configs.length-1].difficulty; +} diff --git a/packages/backend/lib/auth/getJWTsecret.ts b/packages/backend/lib/auth/getJWTsecret.ts new file mode 100644 index 0000000..9388892 --- /dev/null +++ b/packages/backend/lib/auth/getJWTsecret.ts @@ -0,0 +1,13 @@ +import { ErrorResponse } from "src/schema"; + +export const getJWTsecret = () => { + const secret = process.env["JWT_SECRET"]; + if (!secret) { + const response: ErrorResponse = { + message: "JWT_SECRET is not set", + code: "SERVER_ERROR" + }; + return [response, true]; + } + return [secret, null]; +} \ No newline at end of file diff --git a/packages/backend/routes/root/singers.ts b/packages/backend/lib/const/singers.ts similarity index 100% rename from packages/backend/routes/root/singers.ts rename to packages/backend/lib/const/singers.ts diff --git a/packages/backend/middleware/captcha.ts b/packages/backend/middleware/captcha.ts new file mode 100644 index 0000000..7f9e55e --- /dev/null +++ b/packages/backend/middleware/captcha.ts @@ -0,0 +1,117 @@ +import { Context, Next } from "hono"; +import { ErrorResponse } from "src/schema"; +import { SlidingWindow } from "@core/mq/slidingWindow.ts"; +import { getCaptchaConfigMaxDuration, getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts"; +import { sqlCred } from "@core/db/dbNew.ts"; +import { redis } from "@core/db/redis.ts"; +import { verify } from 'hono/jwt'; +import { JwtTokenInvalid, JwtTokenExpired } from "hono/utils/jwt/types"; +import { getJWTsecret } from "@/lib/auth/getJWTsecret.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; +import { object, string, number, ValidationError } from "yup"; +import { getIdentifier } from "@/middleware/rateLimiters.ts"; + +const tokenSchema = object({ + exp: number().integer(), + id: string().length(6), + difficulty: number().integer().moreThan(0) +}); + +export const captchaMiddleware = async (c: Context, next: Next) => { + const authHeader = c.req.header("Authorization"); + + if (!authHeader) { + const response: ErrorResponse = { + message: "'Authorization' header is missing.", + code: "UNAUTHORIZED" + }; + return c.json(response, 401); + } + + const authIsBearer = authHeader.startsWith("Bearer "); + if (!authIsBearer || authHeader.length < 8) { + const response: ErrorResponse = { + message: "'Authorization' header is invalid.", + code: "INVALID_HEADER" + }; + return c.json(response, 400); + } + + const [r, err] = getJWTsecret(); + if (err) { + return c.json(r as ErrorResponse, 500); + } + const jwtSecret = r as string; + + const token = authHeader.substring(7); + + const path = c.req.path; + const method = c.req.method; + const route = `${method}-${path}`; + + const requiredDifficulty = await getCurrentCaptchaDifficulty(sqlCred, c); + + try { + const decodedPayload = await verify(token, jwtSecret); + const payload = await tokenSchema.validate(decodedPayload); + const difficulty = payload.difficulty; + const tokenID = payload.id; + const consumed = await lockManager.isLocked(tokenID); + if (consumed) { + const response: ErrorResponse = { + message: "Token has already been used.", + code: "INVALID_CREDENTIALS" + }; + return c.json(response, 401); + } + if (difficulty < requiredDifficulty) { + const response: ErrorResponse = { + message: "Token too weak.", + code: "UNAUTHORIZED" + }; + return c.json(response, 401); + } + const EXPIRE_FIVE_MINUTES = 300; + await lockManager.acquireLock(tokenID, EXPIRE_FIVE_MINUTES); + } + catch (e) { + if (e instanceof JwtTokenInvalid) { + const response: ErrorResponse = { + message: "Failed to verify the token.", + code: "INVALID_CREDENTIALS" + }; + return c.json(response, 400); + } + else if (e instanceof JwtTokenExpired) { + const response: ErrorResponse = { + message: "Token expired.", + code: "INVALID_CREDENTIALS" + }; + return c.json(response, 400); + } + else if (e instanceof ValidationError) { + const response: ErrorResponse = { + code: "INVALID_QUERY_PARAMS", + message: "Invalid query parameters", + errors: e.errors + }; + return c.json(response, 400); + } + else { + const response: ErrorResponse = { + message: "Unknown error.", + code: "UNKNOWN_ERROR" + }; + return c.json(response, 500); + } + } + const duration = await getCaptchaConfigMaxDuration(sqlCred, route); + const window = new SlidingWindow(redis, duration); + + const identifierWithIP = getIdentifier(c, true); + const identifier = getIdentifier(c, false); + await window.event(`captcha-${identifier}`); + await window.event(`captcha-${identifierWithIP}`); + + await next(); +}; \ No newline at end of file diff --git a/packages/backend/middleware/contentType.ts b/packages/backend/middleware/contentType.ts index 7e47d75..edd4908 100644 --- a/packages/backend/middleware/contentType.ts +++ b/packages/backend/middleware/contentType.ts @@ -3,4 +3,4 @@ import { Context, Next } from "hono"; export const contentType = async (c: Context, next: Next) => { await next(); c.header("Content-Type", "application/json; charset=utf-8"); -}; \ No newline at end of file +}; diff --git a/packages/backend/middleware/index.ts b/packages/backend/middleware/index.ts index a43357e..6f8e411 100644 --- a/packages/backend/middleware/index.ts +++ b/packages/backend/middleware/index.ts @@ -1,19 +1,21 @@ import { Hono } from "hono"; import { Variables } from "hono/types"; import { bodyLimitForPing } from "./bodyLimits.ts"; -import { pingHandler } from "../routes/ping.ts"; +import { pingHandler } from "routes/ping"; import { registerRateLimiter } from "./rateLimiters.ts"; import { preetifyResponse } from "./preetifyResponse.ts"; import { logger } from "./logger.ts"; import { timing } from "hono/timing"; import { contentType } from "./contentType.ts"; +import { captchaMiddleware } from "./captcha.ts"; -export function configureMiddleWares(app: Hono<{Variables: Variables }>) { +export function configureMiddleWares(app: Hono<{ Variables: Variables }>) { app.use("*", contentType); app.use(timing()); app.use("*", preetifyResponse); app.use("*", logger({})); app.post("/user", registerRateLimiter); + app.post("/user", captchaMiddleware); app.all("/ping", bodyLimitForPing, ...pingHandler); -} \ No newline at end of file +} diff --git a/packages/backend/middleware/logger.ts b/packages/backend/middleware/logger.ts index 0625157..59a52b7 100644 --- a/packages/backend/middleware/logger.ts +++ b/packages/backend/middleware/logger.ts @@ -77,7 +77,7 @@ const defaultFormatter = (params) => { `${methodColor} ${params.method.padEnd(6)}${reset} ${params.path}` ); }; -type Ctx = Context +type Ctx = Context; export const logger = (config) => { const { formatter = defaultFormatter, output = console, skipPaths = [], skip = null } = config; diff --git a/packages/backend/middleware/preetifyResponse.ts b/packages/backend/middleware/preetifyResponse.ts index 5cf289b..ccc5944 100644 --- a/packages/backend/middleware/preetifyResponse.ts +++ b/packages/backend/middleware/preetifyResponse.ts @@ -15,4 +15,4 @@ export const preetifyResponse = async (c: Context, next: Next) => { endTime(c, "seralize"); c.res = new Response(prettyJson, { headers: { "Content-Type": "text/plain; charset=utf-8" } }); } -}; \ No newline at end of file +}; diff --git a/packages/backend/middleware/rateLimiters.ts b/packages/backend/middleware/rateLimiters.ts index 3c62d7f..53c4115 100644 --- a/packages/backend/middleware/rateLimiters.ts +++ b/packages/backend/middleware/rateLimiters.ts @@ -1,27 +1,44 @@ -import { rateLimiter, Store } from "hono-rate-limiter"; import type { BlankEnv } from "hono/types"; -import { MINUTE } from "@core/const/time.ts"; import { getConnInfo } from "hono/bun"; -import type { Context } from "hono"; +import { Context, Next } from "hono"; +import { generateRandomId } from "@core/lib/randomID.ts"; +import { RateLimiter } from "@koshnic/ratelimit"; +import { ErrorResponse } from "@/src/schema"; import { redis } from "@core/db/redis.ts"; -import { RedisStore } from "rate-limit-redis"; -export const registerRateLimiter = rateLimiter({ - windowMs: 60 * MINUTE, - limit: 10, - standardHeaders: "draft-6", - keyGenerator: (c) => { - const info = getConnInfo(c as unknown as Context); - if (!info.remote || !info.remote.address) { - return crypto.randomUUID(); - } - const addr = info.remote.address; - const path = new URL(c.req.url).pathname; - const method = c.req.method; - return `${method}-${path}@${addr}`; - }, - store: new RedisStore({ - // @ts-expect-error - Known issue: the `c`all` function is not present in @types/ioredis - sendCommand: (...args: string[]) => redis.call(...args) - }) as unknown as Store -}); \ No newline at end of file +export const getIdentifier = (c: Context, includeIP: boolean = true) => { + let ipAddr = generateRandomId(6); + const info = getConnInfo(c); + if (info.remote && info.remote.address) { + ipAddr = info.remote.address; + } + const forwardedFor = c.req.header("X-Forwarded-For"); + if (forwardedFor) { + ipAddr = forwardedFor.split(",")[0]; + } + const path = c.req.path; + const method = c.req.method; + const ipIdentifier = includeIP ? `@${ipAddr}` : ""; + return `${method}-${path}${ipIdentifier}`; +}; + +export const registerRateLimiter = async (c: Context, next: Next) => { + const limiter = new RateLimiter(redis); + const identifier = getIdentifier(c, true); + const { allowed, retryAfter } = await limiter.allow(identifier, { + burst: 5, + ratePerPeriod: 5, + period: 120, + cost: 1 + }); + + if (!allowed) { + const response: ErrorResponse = { + message: `Too many requests, please retry after ${Math.round(retryAfter)} seconds.`, + code: "RATE_LIMIT_EXCEEDED" + }; + return c.json(response, 429); + } + + await next(); +}; \ No newline at end of file diff --git a/packages/backend/package.json b/packages/backend/package.json index 16bbc0a..2cc883d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -10,6 +10,7 @@ "hono": "^4.7.8", "hono-rate-limiter": "^0.4.2", "ioredis": "^5.6.1", + "limiter": "^3.0.0", "postgres": "^3.4.5", "rate-limit-redis": "^4.2.0", "yup": "^1.6.1", diff --git a/packages/backend/routes/404.ts b/packages/backend/routes/404.ts index 910b9f9..4ff883a 100644 --- a/packages/backend/routes/404.ts +++ b/packages/backend/routes/404.ts @@ -7,4 +7,4 @@ export const notFoundRoute = (c: Context) => { }, 404 ); -}; \ No newline at end of file +}; diff --git a/packages/backend/routes/captcha/[id]/result/GET.ts b/packages/backend/routes/captcha/[id]/result/GET.ts new file mode 100644 index 0000000..25d307c --- /dev/null +++ b/packages/backend/routes/captcha/[id]/result/GET.ts @@ -0,0 +1,88 @@ +import { Context } from "hono"; +import { Bindings, BlankEnv } from "hono/types"; +import { ErrorResponse } from "src/schema"; +import { createHandlers } from "src/utils.ts"; +import { sign } from 'hono/jwt' +import { generateRandomId } from "@core/lib/randomID.ts"; +import { getJWTsecret } from "lib/auth/getJWTsecret.ts"; + +interface CaptchaResponse { + success: boolean; + difficulty?: number; + error?: string; +} + +const getChallengeVerificationResult = async (id: string, ans: string) => { + const baseURL = process.env["UCAPTCHA_URL"]; + const url = new URL(baseURL); + url.pathname = `/challenge/${id}/validation`; + return await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + y: ans + }) + }); +}; + +export const verifyChallengeHandler = createHandlers( + async (c: Context) => { + const id = c.req.param("id"); + const ans = c.req.query("ans"); + if (!ans) { + const response: ErrorResponse = { + message: "Missing required query parameter: ans", + code: "INVALID_QUERY_PARAMS" + }; + return c.json(response, 400); + } + const res = await getChallengeVerificationResult(id, ans); + const data: CaptchaResponse = await res.json(); + if (data.error && res.status === 404) { + const response: ErrorResponse = { + message: data.error, + code: "ENTITY_NOT_FOUND" + }; + return c.json(response, 401); + } else if (data.error && res.status === 400) { + const response: ErrorResponse = { + message: data.error, + code: "INVALID_QUERY_PARAMS" + }; + return c.json(response, 400); + } else if (data.error) { + const response: ErrorResponse = { + message: data.error, + code: "UNKNOWN_ERROR" + }; + return c.json(response, 500); + } + if (!data.success) { + const response: ErrorResponse = { + message: "Incorrect answer", + code: "INVALID_CREDENTIALS" + }; + return c.json(response, 401); + } + + const [r, err] = getJWTsecret(); + if (err) { + return c.json(r as ErrorResponse, 500); + } + const jwtSecret = r as string; + + const tokenID = generateRandomId(6); + const NOW = Math.floor(Date.now() / 1000) + const FIVE_MINUTES_LATER = NOW + 60 * 5; + const jwt = await sign({ + difficulty: data.difficulty!, + id: tokenID, + exp: FIVE_MINUTES_LATER + }, jwtSecret); + return c.json({ + token: jwt + }); + } +); diff --git a/packages/backend/routes/captcha/difficulty/GET.ts b/packages/backend/routes/captcha/difficulty/GET.ts new file mode 100644 index 0000000..0bed7cc --- /dev/null +++ b/packages/backend/routes/captcha/difficulty/GET.ts @@ -0,0 +1,43 @@ +import { createHandlers } from "src/utils.ts"; +import { object, string, ValidationError } from "yup"; +import { ErrorResponse } from "src/schema"; +import { getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts"; +import { sqlCred } from "@core/db/dbNew.ts"; + +const queryParamsSchema = object({ + route: string().matches(/(?:GET|POST|PUT|PATCH|DELETE)-\/.*/g) +}); + +export const getCaptchaDifficultyHandler = createHandlers(async (c) => { + try { + const queryParams = await queryParamsSchema.validate(c.req.query()); + const { route } = queryParams; + const difficulty = await getCurrentCaptchaDifficulty(sqlCred, route); + if (!difficulty) { + const response: ErrorResponse = { + code: "ENTITY_NOT_FOUND", + message: "No difficulty configs found for this route." + }; + return c.json>(response, 404); + } + return c.json({ + "difficulty": difficulty + }); + } catch (e: unknown) { + if (e instanceof ValidationError) { + const response: ErrorResponse = { + code: "INVALID_QUERY_PARAMS", + message: "Invalid query parameters", + errors: e.errors + }; + return c.json(response, 400); + } else { + const response: ErrorResponse = { + code: "UNKNOWN_ERROR", + message: "Unknown error", + errors: [e] + }; + return c.json>(response, 500); + } + } +}); diff --git a/packages/backend/routes/captcha/index.ts b/packages/backend/routes/captcha/index.ts new file mode 100644 index 0000000..fa6d476 --- /dev/null +++ b/packages/backend/routes/captcha/index.ts @@ -0,0 +1,2 @@ +export * from "./session/POST.ts"; +export * from "./[id]/result/GET.ts"; \ No newline at end of file diff --git a/packages/backend/routes/captcha/session/POST.ts b/packages/backend/routes/captcha/session/POST.ts new file mode 100644 index 0000000..2686a77 --- /dev/null +++ b/packages/backend/routes/captcha/session/POST.ts @@ -0,0 +1,59 @@ +import { createHandlers } from "src/utils.ts"; +import { getCurrentCaptchaDifficulty } from "@/lib/auth/captchaDifficulty.ts"; +import { sqlCred } from "@core/db/dbNew.ts"; +import { object, string, ValidationError } from "yup"; +import { ErrorResponse } from "@/src/schema"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; + +const bodySchema = object({ + route: string().matches(/(?:GET|POST|PUT|PATCH|DELETE)-\/.*/g) +}); + +interface CaptchaSessionResponse { + success: boolean; + id: string; + g: string; + n: string; + t: number; +} + +const createNewChallenge = async (difficulty: number) => { + const baseURL = process.env["UCAPTCHA_URL"]; + const url = new URL(baseURL); + url.pathname = "/challenge"; + return await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + difficulty: difficulty, + }) + }); +} + +export const createCaptchaSessionHandler = createHandlers(async (c) => { + try { + const requestBody = await bodySchema.validate(await c.req.json()); + const { route } = requestBody; + const difficuly = await getCurrentCaptchaDifficulty(sqlCred, route) + const res = await createNewChallenge(difficuly); + return c.json(await res.json(), res.status as ContentfulStatusCode); + } catch (e: unknown) { + if (e instanceof ValidationError) { + const response: ErrorResponse = { + code: "INVALID_QUERY_PARAMS", + message: "Invalid query parameters", + errors: e.errors + }; + return c.json(response, 400); + } else { + const response: ErrorResponse = { + code: "UNKNOWN_ERROR", + message: "Unknown error", + errors: [e] + }; + return c.json>(response, 500); + } + } +}); diff --git a/packages/backend/routes/index.ts b/packages/backend/routes/index.ts index 86ebf07..de83d72 100644 --- a/packages/backend/routes/index.ts +++ b/packages/backend/routes/index.ts @@ -1,17 +1,29 @@ -import { rootHandler } from "./root/root.ts"; -import { pingHandler } from "./ping.ts"; -import { getSnapshotsHanlder } from "./snapshots.ts"; -import { registerHandler } from "./user.ts"; -import { videoInfoHandler } from "db/videoInfo.ts"; -import { Hono } from "hono"; -import { Variables } from "hono/types"; +import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "lib/const/singers.ts"; +import { VERSION } from "src/main.ts"; +import { createHandlers } from "src/utils.ts"; -export function configureRoutes(app: Hono<{Variables: Variables }>) { - app.get("/", ...rootHandler); - app.all("/ping", ...pingHandler); - - app.get("/video/:id/snapshots", ...getSnapshotsHanlder); - app.post("/user", ...registerHandler); - - app.get("/video/:id/info", ...videoInfoHandler); -} \ No newline at end of file +export const rootHandler = createHandlers((c) => { + let singer: Singer | Singer[]; + const shouldShowSpecialSinger = Math.random() < 0.016; + if (getSingerForBirthday().length !== 0) { + singer = JSON.parse(JSON.stringify(getSingerForBirthday())) as Singer[]; + for (const s of singer) { + delete s.birthday; + s.message = `祝${s.name}生日快乐~`; + } + } else if (shouldShowSpecialSinger) { + singer = pickSpecialSinger(); + } else { + singer = pickSinger(); + } + return c.json({ + project: { + name: "中V档案馆", + motto: "一起唱吧,心中的歌!" + }, + status: 200, + version: VERSION, + time: Date.now(), + singer: singer + }); +}); diff --git a/packages/backend/routes/ping.ts b/packages/backend/routes/ping.ts deleted file mode 100644 index a84b588..0000000 --- a/packages/backend/routes/ping.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getClientIP } from "middleware/logger.ts"; -import { createHandlers } from "../src/utils.ts"; -import { VERSION } from "../src/main.ts"; - -export const pingHandler = createHandlers(async (c) => { - const requestHeaders = c.req.raw.headers; - return c.json({ - "message": "pong", - "request": { - "headers": requestHeaders, - "ip": getClientIP(c), - "mode": c.req.raw.mode, - "method": c.req.method, - "query": new URL(c.req.url).searchParams, - "body": await c.req.text(), - "url": c.req.raw.url - }, - "response": { - "time": new Date().getTime(), - "status": 200, - "version": VERSION, - } - }); -}); \ No newline at end of file diff --git a/packages/backend/routes/ping/index.ts b/packages/backend/routes/ping/index.ts new file mode 100644 index 0000000..c1caecb --- /dev/null +++ b/packages/backend/routes/ping/index.ts @@ -0,0 +1,24 @@ +import { getClientIP } from "middleware/logger.ts"; +import { createHandlers } from "src/utils.ts"; +import { VERSION } from "src/main.ts"; + +export const pingHandler = createHandlers(async (c) => { + const requestHeaders = c.req.raw.headers; + return c.json({ + message: "pong", + request: { + headers: requestHeaders, + ip: getClientIP(c), + mode: c.req.raw.mode, + method: c.req.method, + query: new URL(c.req.url).searchParams, + body: await c.req.text(), + url: c.req.raw.url + }, + response: { + time: new Date().getTime(), + status: 200, + version: VERSION + } + }); +}); diff --git a/packages/backend/routes/root/root.ts b/packages/backend/routes/root/root.ts deleted file mode 100644 index d570bce..0000000 --- a/packages/backend/routes/root/root.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "./singers.ts"; -import { VERSION } from "../../src/main.ts"; -import { createHandlers } from "../../src/utils.ts"; - -export const rootHandler = createHandlers((c) => { - let singer: Singer | Singer[]; - const shouldShowSpecialSinger = Math.random() < 0.016; - if (getSingerForBirthday().length !== 0) { - singer = JSON.parse(JSON.stringify(getSingerForBirthday())) as Singer[]; - for (const s of singer) { - delete s.birthday; - s.message = `祝${s.name}生日快乐~`; - } - } else if (shouldShowSpecialSinger) { - singer = pickSpecialSinger(); - } else { - singer = pickSinger(); - } - return c.json({ - project: { - name: "中V档案馆", - motto: "一起唱吧,心中的歌!" - }, - status: 200, - version: VERSION, - time: Date.now(), - singer: singer - }); -}); diff --git a/packages/backend/routes/user/index.ts b/packages/backend/routes/user/index.ts new file mode 100644 index 0000000..d9f7bf7 --- /dev/null +++ b/packages/backend/routes/user/index.ts @@ -0,0 +1 @@ +export * from "./register.ts"; diff --git a/packages/backend/routes/user.ts b/packages/backend/routes/user/register.ts similarity index 94% rename from packages/backend/routes/user.ts rename to packages/backend/routes/user/register.ts index 15cca30..d774b56 100644 --- a/packages/backend/routes/user.ts +++ b/packages/backend/routes/user/register.ts @@ -3,7 +3,7 @@ import Argon2id from "@rabbit-company/argon2id"; import { object, string, ValidationError } from "yup"; import type { Context } from "hono"; import type { Bindings, BlankEnv, BlankInput } from "hono/types"; -import { sqlCred } from "db/db.ts"; +import { sqlCred } from "@core/db/dbNew.ts"; import { ErrorResponse, StatusResponse } from "src/schema"; const RegistrationBodySchema = object({ @@ -44,7 +44,7 @@ export const registerHandler = createHandlers(async (c: ContextType) => { const response: StatusResponse = { message: `User '${username}' registered successfully.` - } + }; return c.json(response, 201); } catch (e) { @@ -53,21 +53,21 @@ export const registerHandler = createHandlers(async (c: ContextType) => { message: "Invalid registration data.", errors: e.errors, code: "INVALID_PAYLOAD" - } + }; return c.json>(response, 400); } else if (e instanceof SyntaxError) { const response: ErrorResponse = { message: "Invalid JSON payload.", errors: [e.message], code: "INVALID_FORMAT" - } + }; return c.json>(response, 400); } else { const response: ErrorResponse = { - message: "Invalid JSON payload.", + message: "Unknown error.", errors: [(e as Error).message], - code: "UNKNOWN_ERR" - } + code: "UNKNOWN_ERROR" + }; return c.json>(response, 500); } } diff --git a/packages/backend/db/videoInfo.ts b/packages/backend/routes/video/[id]/info.ts similarity index 84% rename from packages/backend/db/videoInfo.ts rename to packages/backend/routes/video/[id]/info.ts index e9fee88..3e05493 100644 --- a/packages/backend/db/videoInfo.ts +++ b/packages/backend/routes/video/[id]/info.ts @@ -1,15 +1,15 @@ import logger from "@core/log/logger.ts"; import { redis } from "@core/db/redis.ts"; -import { sql } from "./db.ts"; +import { sql } from "@core/db/dbNew.ts"; import { number, ValidationError } from "yup"; -import { createHandlers } from "../src/utils.ts"; +import { createHandlers } from "@/src/utils.ts"; import { getVideoInfo, getVideoInfoByBV } from "@core/net/getVideoInfo.ts"; -import { idSchema } from "../routes/snapshots.ts"; +import { idSchema } from "./snapshots.ts"; import { NetSchedulerError } from "@core/net/delegate.ts"; import type { Context } from "hono"; import type { BlankEnv, BlankInput } from "hono/types"; import type { VideoInfoData } from "@core/net/bilibili.d.ts"; -import { startTime, endTime } from 'hono/timing' +import { startTime, endTime } from "hono/timing"; const CACHE_EXPIRATION_SECONDS = 60; @@ -34,7 +34,7 @@ async function insertVideoSnapshot(data: VideoInfoData) { } export const videoInfoHandler = createHandlers(async (c: ContextType) => { - startTime(c, 'parse', 'Parse the request'); + startTime(c, "parse", "Parse the request"); try { const id = await idSchema.validate(c.req.param("id")); let videoId: string | number = id as string; @@ -45,33 +45,33 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => { } const cacheKey = `cvsa:videoInfo:${videoId}`; - endTime(c, 'parse'); - startTime(c, 'cache', 'Check for cached data'); + endTime(c, "parse"); + startTime(c, "cache", "Check for cached data"); const cachedData = await redis.get(cacheKey); - endTime(c, 'cache'); + endTime(c, "cache"); if (cachedData) { return c.json(JSON.parse(cachedData)); } - startTime(c, 'net', 'Fetch data'); + startTime(c, "net", "Fetch data"); let result: VideoInfoData | number; if (typeof videoId === "number") { result = await getVideoInfo(videoId, "getVideoInfo"); } else { result = await getVideoInfoByBV(videoId, "getVideoInfo"); } - endTime(c, 'net'); + endTime(c, "net"); if (typeof result === "number") { return c.json({ message: "Error fetching video info", code: result }, 500); } - startTime(c, 'db', 'Write data to database'); + startTime(c, "db", "Write data to database"); await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result)); await insertVideoSnapshot(result); - endTime(c, 'db'); + endTime(c, "db"); return c.json(result); } catch (e) { if (e instanceof ValidationError) { diff --git a/packages/backend/routes/snapshots.ts b/packages/backend/routes/video/[id]/snapshots.ts similarity index 93% rename from packages/backend/routes/snapshots.ts rename to packages/backend/routes/video/[id]/snapshots.ts index 956a452..e738e50 100644 --- a/packages/backend/routes/snapshots.ts +++ b/packages/backend/routes/video/[id]/snapshots.ts @@ -1,10 +1,10 @@ import type { Context } from "hono"; -import { createHandlers } from "../src/utils.ts"; +import { createHandlers } from "src/utils.ts"; import type { BlankEnv, BlankInput } from "hono/types"; -import { getVideoSnapshots, getVideoSnapshotsByBV } from "../db/videoSnapshot.ts"; +import { getVideoSnapshots, getVideoSnapshotsByBV } from "db/snapshots.ts"; import type { VideoSnapshotType } from "@core/db/schema.d.ts"; import { boolean, mixed, number, object, ValidationError } from "yup"; -import { ErrorResponse } from "../src/schema"; +import { ErrorResponse } from "src/schema"; import { startTime, endTime } from "hono/timing"; const SnapshotQueryParamsSchema = object({ @@ -96,7 +96,7 @@ export const getSnapshotsHanlder = createHandlers(async (c: ContextType) => { return c.json>(response, 400); } else { const response: ErrorResponse = { - code: "UNKNOWN_ERR", + code: "UNKNOWN_ERROR", message: "Unhandled error", errors: [e] }; diff --git a/packages/backend/routes/video/index.ts b/packages/backend/routes/video/index.ts new file mode 100644 index 0000000..4c7abb2 --- /dev/null +++ b/packages/backend/routes/video/index.ts @@ -0,0 +1,2 @@ +export * from "./[id]/info"; +export * from "./[id]/snapshots"; diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index 06285a7..61ababf 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import type { TimingVariables } from "hono/timing"; import { startServer } from "./startServer.ts"; -import { configureRoutes } from "routes"; +import { configureRoutes } from "./routing.ts"; import { configureMiddleWares } from "middleware"; import { notFoundRoute } from "routes/404.ts"; diff --git a/packages/backend/src/routing.ts b/packages/backend/src/routing.ts new file mode 100644 index 0000000..d5a7aa2 --- /dev/null +++ b/packages/backend/src/routing.ts @@ -0,0 +1,23 @@ +import { rootHandler } from "routes"; +import { pingHandler } from "routes/ping"; +import { registerHandler } from "routes/user"; +import { videoInfoHandler, getSnapshotsHanlder } from "routes/video"; +import { Hono } from "hono"; +import { Variables } from "hono/types"; +import { createCaptchaSessionHandler, verifyChallengeHandler } from "routes/captcha"; +import { getCaptchaDifficultyHandler } from "../routes/captcha/difficulty/GET.ts"; + +export function configureRoutes(app: Hono<{ Variables: Variables }>) { + app.get("/", ...rootHandler); + app.all("/ping", ...pingHandler); + + app.get("/video/:id/snapshots", ...getSnapshotsHanlder); + app.post("/user", ...registerHandler); + + app.get("/video/:id/info", ...videoInfoHandler); + + app.post("/captcha/session", ...createCaptchaSessionHandler); + app.get("/captcha/:id/result", ...verifyChallengeHandler); + + app.get("/captcha/difficulty", ...getCaptchaDifficultyHandler) +} diff --git a/packages/backend/src/schema.d.ts b/packages/backend/src/schema.d.ts index 279a83d..5b16743 100644 --- a/packages/backend/src/schema.d.ts +++ b/packages/backend/src/schema.d.ts @@ -1,11 +1,22 @@ -type ErrorCode = "INVALID_QUERY_PARAMS" | "UNKNOWN_ERR" | "INVALID_PAYLOAD" | "INVALID_FORMAT" | "BODY_TOO_LARGE"; +type ErrorCode = + | "INVALID_QUERY_PARAMS" + | "UNKNOWN_ERROR" + | "INVALID_PAYLOAD" + | "INVALID_FORMAT" + | "INVALID_HEADER" + | "BODY_TOO_LARGE" + | "UNAUTHORIZED" + | "INVALID_CREDENTIALS" + | "ENTITY_NOT_FOUND" + | "SERVER_ERROR" + | "RATE_LIMIT_EXCEEDED"; -export interface ErrorResponse { - code: ErrorCode +export interface ErrorResponse { + code: ErrorCode; message: string; - errors: E[]; + errors?: E[]; } export interface StatusResponse { message: string; -} \ No newline at end of file +} diff --git a/packages/backend/src/startServer.ts b/packages/backend/src/startServer.ts index 1719393..4df0c07 100644 --- a/packages/backend/src/startServer.ts +++ b/packages/backend/src/startServer.ts @@ -32,7 +32,7 @@ function logStartup(hostname: string, port: number, wasAutoIncremented: boolean, console.log("\nPress Ctrl+C to quit."); } -export async function startServer(app: Hono<{Variables: Variables }>) { +export async function startServer(app: Hono<{ Variables: Variables }>) { const NODE_ENV = process.env.NODE_ENV || "production"; const HOST = process.env.HOST ?? (NODE_ENV === "development" ? "0.0.0.0" : "127.0.0.1"); const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 6cbd96f..6b2128e 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "paths": { "@core/*": ["../core/*"], + "@/*": ["./*"], "@crawler/*": ["../crawler/*"] }, "allowSyntheticDefaultImports": true, diff --git a/packages/core/db/dbNew.ts b/packages/core/db/dbNew.ts index 11088e5..2f925ec 100644 --- a/packages/core/db/dbNew.ts +++ b/packages/core/db/dbNew.ts @@ -1,6 +1,8 @@ import postgres from "postgres"; -import { postgresConfigNpm } from "./pgConfigNew"; +import { postgresConfigCred, postgresConfig } from "./pgConfigNew"; -export const sql = postgres(postgresConfigNpm); +export const sql = postgres(postgresConfig); -export const sqlTest = postgres(postgresConfigNpm); \ No newline at end of file +export const sqlCred = postgres(postgresConfigCred); + +export const sqlTest = postgres(postgresConfig); \ No newline at end of file diff --git a/packages/core/db/pgConfigNew.ts b/packages/core/db/pgConfigNew.ts index 238adfd..7edfc92 100644 --- a/packages/core/db/pgConfigNew.ts +++ b/packages/core/db/pgConfigNew.ts @@ -18,14 +18,6 @@ const databasePassword = getEnvVar("DB_PASSWORD")!; const databasePort = getEnvVar("DB_PORT")!; export const postgresConfig = { - hostname: databaseHost, - port: parseInt(databasePort), - database: databaseName, - user: databaseUser, - password: databasePassword -}; - -export const postgresConfigNpm = { host: databaseHost, port: parseInt(databasePort), database: databaseName, diff --git a/packages/crawler/global.d.ts b/packages/core/db/psql.d.ts similarity index 51% rename from packages/crawler/global.d.ts rename to packages/core/db/psql.d.ts index 37fc0e2..f629cd5 100644 --- a/packages/crawler/global.d.ts +++ b/packages/core/db/psql.d.ts @@ -1,3 +1,3 @@ import type postgres from "postgres"; -export type Psql = postgres.Sql<{}>; +export type Psql = postgres.Sql; diff --git a/packages/core/deno.json b/packages/core/deno.json deleted file mode 100644 index b1027e9..0000000 --- a/packages/core/deno.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@cvsa/core", - "exports": "./main.ts", - "imports": { - "ioredis": "npm:ioredis", - "log/": "./log/", - "db/": "./db/", - "$std/": "https://deno.land/std@0.216.0/", - "mq/": "./mq/", - "chalk": "npm:chalk", - "winston": "npm:winston", - "logform": "npm:logform", - "@core/": "./", - "child_process": "node:child_process", - "util": "node:util" - } -} diff --git a/packages/core/lib/randomID.ts b/packages/core/lib/randomID.ts new file mode 100644 index 0000000..7448438 --- /dev/null +++ b/packages/core/lib/randomID.ts @@ -0,0 +1,15 @@ +export function generateRandomId(length: number): string { + const characters = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + const charactersLength = characters.length; + const randomBytes = new Uint8Array(length); + + crypto.getRandomValues(randomBytes); + + let result = ''; + for (let i = 0; i < length; i++) { + const randomIndex = randomBytes[i] % charactersLength; + result += characters.charAt(randomIndex); + } + + return result; +} \ No newline at end of file diff --git a/packages/crawler/mq/lockManager.ts b/packages/core/mq/lockManager.ts similarity index 96% rename from packages/crawler/mq/lockManager.ts rename to packages/core/mq/lockManager.ts index fefae7b..129001a 100644 --- a/packages/crawler/mq/lockManager.ts +++ b/packages/core/mq/lockManager.ts @@ -1,5 +1,5 @@ import { Redis } from "ioredis"; -import { redis } from "../../core/db/redis.ts"; +import { redis } from "@core/db/redis.ts"; class LockManager { private redis: Redis; diff --git a/packages/core/mq/multipleRateLimiter.ts b/packages/core/mq/multipleRateLimiter.ts new file mode 100644 index 0000000..5c9333e --- /dev/null +++ b/packages/core/mq/multipleRateLimiter.ts @@ -0,0 +1,55 @@ +import { RateLimiter as Limiter } from "@koshnic/ratelimit"; +import { redis } from "@core/db/redis.ts"; + +export interface RateLimiterConfig { + duration: number; + max: number; +} + +export class RateLimiterError extends Error { + public code: string; + constructor(message: string) { + super(message); + this.name = "RateLimiterError"; + this.code = "RATE_LIMIT_EXCEEDED"; + } +} + +export class MultipleRateLimiter { + private readonly name: string; + private readonly configs: RateLimiterConfig[] = []; + private readonly limiter: Limiter; + + /* + * @param name The name of the rate limiter + * @param configs The configuration of the rate limiter, containing: + * - duration: The duration of window in seconds + * - max: The maximum number of tokens allowed in the window + */ + constructor( + name: string, + configs: RateLimiterConfig[] + ) { + this.configs = configs; + this.limiter = new Limiter(redis); + this.name = name; + } + + /* + * Trigger an event in the rate limiter + */ + async trigger(shouldThrow = true): Promise { + for (let i = 0; i < this.configs.length; i++) { + const { duration, max } = this.configs[i]; + const { allowed } = await this.limiter.allow(`cvsa:${this.name}_${i}`, { + burst: max, + ratePerPeriod: max, + period: duration, + cost: 1 + }); + if (!allowed && shouldThrow) { + throw new RateLimiterError("Rate limit exceeded") + } + } + } +} \ No newline at end of file diff --git a/packages/core/mq/rateLimiter.ts b/packages/core/mq/rateLimiter.ts deleted file mode 100644 index ac42748..0000000 --- a/packages/core/mq/rateLimiter.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { SlidingWindow } from "./slidingWindow.ts"; - -export interface RateLimiterConfig { - window: SlidingWindow; - max: number; -} - -export class RateLimiter { - private readonly configs: RateLimiterConfig[]; - private readonly configEventNames: string[]; - - /* - * @param name The name of the rate limiter - * @param configs The configuration of the rate limiter, containing: - * - window: The sliding window to use - * - max: The maximum number of events allowed in the window - */ - constructor(name: string, configs: RateLimiterConfig[]) { - this.configs = configs; - this.configEventNames = configs.map((_, index) => `${name}_config_${index}`); - } - - /* - * Check if the event has reached the rate limit - */ - async getAvailability(): Promise { - for (let i = 0; i < this.configs.length; i++) { - const config = this.configs[i]; - const eventName = this.configEventNames[i]; - const count = await config.window.count(eventName); - if (count >= config.max) { - return false; - } - } - return true; - } - - /* - * Trigger an event in the rate limiter - */ - async trigger(): Promise { - for (let i = 0; i < this.configs.length; i++) { - const config = this.configs[i]; - const eventName = this.configEventNames[i]; - await config.window.event(eventName); - } - } - - async clear(): Promise { - for (let i = 0; i < this.configs.length; i++) { - const config = this.configs[i]; - const eventName = this.configEventNames[i]; - await config.window.clear(eventName); - } - } -} diff --git a/packages/core/mq/slidingWindow.ts b/packages/core/mq/slidingWindow.ts index 457303c..092190c 100644 --- a/packages/core/mq/slidingWindow.ts +++ b/packages/core/mq/slidingWindow.ts @@ -32,14 +32,20 @@ export class SlidingWindow { /* * Count the number of events in the sliding window - * @param eventName The name of the event + * @param {string} eventName The name of the event + * @param {number} [duration] The duration of the window in seconds */ - async count(eventName: string): Promise { + async count(eventName: string, duration?: number): Promise { const key = `cvsa:sliding_window:${eventName}`; const now = Date.now(); // Remove timestamps outside the window await this.redis.zremrangebyscore(key, 0, now - this.windowSize); + + if (duration) { + return this.redis.zcount(key, now - duration * 1000, now); + } + // Get the number of timestamps in the window return this.redis.zcard(key); } @@ -48,4 +54,4 @@ export class SlidingWindow { const key = `cvsa:sliding_window:${eventName}`; return this.redis.del(key); } -} +} \ No newline at end of file diff --git a/packages/core/net/delegate.ts b/packages/core/net/delegate.ts index e7ca3f1..0c5d856 100644 --- a/packages/core/net/delegate.ts +++ b/packages/core/net/delegate.ts @@ -1,9 +1,7 @@ import logger from "@core/log/logger.ts"; -import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts"; -import { SlidingWindow } from "mq/slidingWindow.ts"; -import { redis } from "db/redis.ts"; +import { MultipleRateLimiter, RateLimiterError, type RateLimiterConfig } from "@core/mq/multipleRateLimiter.ts"; import { ReplyError } from "ioredis"; -import { SECOND } from "../const/time.ts"; +import { SECOND } from "@core/const/time.ts"; import { spawn, SpawnOptions } from "child_process"; export function spawnPromise( @@ -73,11 +71,11 @@ export class NetSchedulerError extends Error { } type LimiterMap = { - [name: string]: RateLimiter; + [name: string]: MultipleRateLimiter; }; type OptionalLimiterMap = { - [name: string]: RateLimiter | null; + [name: string]: MultipleRateLimiter | null; }; type TaskMap = { @@ -121,20 +119,23 @@ class NetworkDelegate { const proxies = this.getTaskProxies(taskName); for (const proxyName of proxies) { const limiterId = "proxy-" + proxyName + "-" + taskName; - this.proxyLimiters[limiterId] = config ? new RateLimiter(limiterId, config) : null; + this.proxyLimiters[limiterId] = config ? new MultipleRateLimiter(limiterId, config) : null; } } - async triggerLimiter(task: string, proxy: string): Promise { + async triggerLimiter(task: string, proxy: string, force: boolean = false): Promise { const limiterId = "proxy-" + proxy + "-" + task; const providerLimiterId = "provider-" + proxy + "-" + this.tasks[task].provider; try { - await this.proxyLimiters[limiterId]?.trigger(); - await this.providerLimiters[providerLimiterId]?.trigger(); + await this.proxyLimiters[limiterId]?.trigger(!force); + await this.providerLimiters[providerLimiterId]?.trigger(!force); } catch (e) { const error = e as Error; if (e instanceof ReplyError) { logger.error(error, "redis"); + } else if (e instanceof RateLimiterError) { + // Re-throw it to ensure this.request can catch it + throw e; } logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest"); } @@ -149,7 +150,7 @@ class NetworkDelegate { } for (const proxyName of bindProxies) { const limiterId = "provider-" + proxyName + "-" + providerName; - this.providerLimiters[limiterId] = new RateLimiter(limiterId, config); + this.providerLimiters[limiterId] = new MultipleRateLimiter(limiterId, config); } } @@ -168,9 +169,15 @@ class NetworkDelegate { // find a available proxy const proxiesNames = this.getTaskProxies(task); for (const proxyName of shuffleArray(proxiesNames)) { - if (await this.getProxyAvailability(proxyName, task)) { + try { return await this.proxyRequest(url, proxyName, task, method); } + catch (e) { + if (e instanceof RateLimiterError) { + continue; + } + throw e; + } } throw new NetSchedulerError("No proxy is available currently.", "NO_PROXY_AVAILABLE"); } @@ -202,16 +209,8 @@ class NetworkDelegate { throw new NetSchedulerError(`Proxy "${proxyName}" not found`, "PROXY_NOT_FOUND"); } - if (!force) { - const isAvailable = await this.getProxyAvailability(proxyName, task); - const limiter = "proxy-" + proxyName + "-" + task; - if (!isAvailable) { - throw new NetSchedulerError(`Proxy "${limiter}" is rate limited`, "PROXY_RATE_LIMITED"); - } - } - + await this.triggerLimiter(task, proxyName, force); const result = await this.makeRequest(url, proxy, method); - await this.triggerLimiter(task, proxyName); return result; } @@ -226,32 +225,6 @@ class NetworkDelegate { } } - private async getProxyAvailability(proxyName: string, taskName: string): Promise { - try { - const task = this.tasks[taskName]; - const provider = task.provider; - const proxyLimiterId = "proxy-" + proxyName + "-" + task; - const providerLimiterId = "provider-" + proxyName + "-" + provider; - if (!this.proxyLimiters[proxyLimiterId]) { - const providerLimiter = this.providerLimiters[providerLimiterId]; - return await providerLimiter.getAvailability(); - } - const proxyLimiter = this.proxyLimiters[proxyLimiterId]; - const providerLimiter = this.providerLimiters[providerLimiterId]; - const providerAvailable = await providerLimiter.getAvailability(); - const proxyAvailable = await proxyLimiter.getAvailability(); - return providerAvailable && proxyAvailable; - } catch (e) { - const error = e as Error; - if (e instanceof ReplyError) { - logger.error(error, "redis"); - return false; - } - logger.error(error, "mq", "getProxyAvailability"); - return false; - } - } - private async nativeRequest(url: string, method: string): Promise { try { const controller = new AbortController(); @@ -316,37 +289,37 @@ class NetworkDelegate { const networkDelegate = new NetworkDelegate(); const videoInfoRateLimiterConfig: RateLimiterConfig[] = [ { - window: new SlidingWindow(redis, 0.3), + duration: 0.3, max: 1, }, { - window: new SlidingWindow(redis, 3), + duration: 3, max: 5, }, { - window: new SlidingWindow(redis, 30), + duration: 30, max: 30, }, { - window: new SlidingWindow(redis, 2 * 60), + duration: 2 * 60, max: 50, }, ]; const biliLimiterConfig: RateLimiterConfig[] = [ { - window: new SlidingWindow(redis, 1), + duration: 1, max: 6, }, { - window: new SlidingWindow(redis, 5), + duration: 5, max: 20, }, { - window: new SlidingWindow(redis, 30), + duration: 30, max: 100, }, { - window: new SlidingWindow(redis, 5 * 60), + duration: 5 * 60, max: 200, }, ]; diff --git a/packages/core/package.json b/packages/core/package.json index a11ede0..f8ada58 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,13 +1,17 @@ { - "name": "core", - "dependencies": { - "chalk": "^5.4.1", - "ioredis": "^5.6.1", - "logform": "^2.7.0", - "postgres": "^3.4.5", - "winston": "^3.17.0" - }, - "devDependencies": { - "@types/ioredis": "^5.0.0" - } -} \ No newline at end of file + "name": "core", + "scripts": { + "test": "bun --env-file=.env.test run vitest" + }, + "dependencies": { + "@koshnic/ratelimit": "^1.0.3", + "chalk": "^5.4.1", + "ioredis": "^5.6.1", + "logform": "^2.7.0", + "postgres": "^3.4.5", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/ioredis": "^5.0.0" + } +} diff --git a/packages/core/test/lib/randomID.test.ts b/packages/core/test/lib/randomID.test.ts new file mode 100644 index 0000000..8f69dcd --- /dev/null +++ b/packages/core/test/lib/randomID.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { generateRandomId } from "@core/lib/randomID.ts"; + +describe("generateRandomId", () => { + it("should generate an ID of the specified length", () => { + const length = 15; + const id = generateRandomId(length); + expect(id).toHaveLength(length); + }); + + it("should generate an ID containing only allowed characters", () => { + const allowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + const id = generateRandomId(20); + for (const char of id) { + expect(allowedChars).toContain(char); + } + }); +}); diff --git a/packages/crawler/db/bilibili_metadata.ts b/packages/crawler/db/bilibili_metadata.ts index acc136c..6c68516 100644 --- a/packages/crawler/db/bilibili_metadata.ts +++ b/packages/crawler/db/bilibili_metadata.ts @@ -1,4 +1,4 @@ -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; import { AllDataType, BiliUserType } from "@core/db/schema"; import { AkariModelVersion } from "ml/const"; diff --git a/packages/crawler/db/snapshot.ts b/packages/crawler/db/snapshot.ts index b4635ee..16df13c 100644 --- a/packages/crawler/db/snapshot.ts +++ b/packages/crawler/db/snapshot.ts @@ -1,6 +1,6 @@ import { LatestSnapshotType } from "@core/db/schema"; import { SnapshotNumber } from "mq/task/getVideoStats.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; export async function getVideosNearMilestone(sql: Psql) { const queryResult = await sql` diff --git a/packages/crawler/db/snapshotSchedule.ts b/packages/crawler/db/snapshotSchedule.ts index 9937a2d..6fcfdc1 100644 --- a/packages/crawler/db/snapshotSchedule.ts +++ b/packages/crawler/db/snapshotSchedule.ts @@ -4,7 +4,7 @@ import { MINUTE } from "@core/const/time.ts"; import { redis } from "@core/db/redis.ts"; import { Redis } from "ioredis"; import { parseTimestampFromPsql } from "../utils/formatTimestampToPostgre.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; const REDIS_KEY = "cvsa:snapshot_window_counts"; diff --git a/packages/crawler/db/songs.ts b/packages/crawler/db/songs.ts index 5f5070f..ebdd08c 100644 --- a/packages/crawler/db/songs.ts +++ b/packages/crawler/db/songs.ts @@ -1,4 +1,4 @@ -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; import { parseTimestampFromPsql } from "utils/formatTimestampToPostgre.ts"; export async function getNotCollectedSongs(sql: Psql) { diff --git a/packages/crawler/mq/exec/archiveSnapshots.ts b/packages/crawler/mq/exec/archiveSnapshots.ts index 91411a9..c667416 100644 --- a/packages/crawler/mq/exec/archiveSnapshots.ts +++ b/packages/crawler/mq/exec/archiveSnapshots.ts @@ -1,7 +1,7 @@ import { Job } from "bullmq"; import { getAllVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts"; import logger from "@core/log/logger.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import { getLatestVideoSnapshot } from "db/snapshot.ts"; import { HOUR, MINUTE } from "@core/const/time.ts"; import { sql } from "@core/db/dbNew"; diff --git a/packages/crawler/mq/exec/classifyVideo.ts b/packages/crawler/mq/exec/classifyVideo.ts index 622eeed..6c3d37c 100644 --- a/packages/crawler/mq/exec/classifyVideo.ts +++ b/packages/crawler/mq/exec/classifyVideo.ts @@ -3,7 +3,7 @@ import { getUnlabelledVideos, getVideoInfoFromAllData, insertVideoLabel } from " import Akari from "ml/akari.ts"; import { ClassifyVideoQueue } from "mq/index.ts"; import logger from "@core/log/logger.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import { aidExistsInSongs } from "db/songs.ts"; import { insertIntoSongs } from "mq/task/collectSongs.ts"; import { scheduleSnapshot } from "db/snapshotSchedule.ts"; diff --git a/packages/crawler/mq/exec/dispatchRegularSnapshots.ts b/packages/crawler/mq/exec/dispatchRegularSnapshots.ts index 5c1652f..49a7893 100644 --- a/packages/crawler/mq/exec/dispatchRegularSnapshots.ts +++ b/packages/crawler/mq/exec/dispatchRegularSnapshots.ts @@ -4,7 +4,7 @@ import { truncate } from "utils/truncate.ts"; import { getVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts"; import logger from "@core/log/logger.ts"; import { HOUR, MINUTE, WEEK } from "@core/const/time.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import { getRegularSnapshotInterval } from "mq/task/regularSnapshotInterval.ts"; import { sql } from "@core/db/dbNew.ts"; diff --git a/packages/crawler/mq/exec/snapshotVideo.ts b/packages/crawler/mq/exec/snapshotVideo.ts index 261b78b..59f05db 100644 --- a/packages/crawler/mq/exec/snapshotVideo.ts +++ b/packages/crawler/mq/exec/snapshotVideo.ts @@ -2,7 +2,7 @@ import { Job } from "bullmq"; import { scheduleSnapshot, setSnapshotStatus, snapshotScheduleExists } from "db/snapshotSchedule.ts"; import logger from "@core/log/logger.ts"; import { HOUR, MINUTE, SECOND } from "@core/const/time.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts"; import { insertVideoSnapshot } from "mq/task/getVideoStats.ts"; import { getSongsPublihsedAt } from "db/songs.ts"; diff --git a/packages/crawler/mq/scheduling.ts b/packages/crawler/mq/scheduling.ts index b84d73f..cf7427f 100644 --- a/packages/crawler/mq/scheduling.ts +++ b/packages/crawler/mq/scheduling.ts @@ -2,7 +2,7 @@ import { findClosestSnapshot, getLatestSnapshot, hasAtLeast2Snapshots } from "db import { truncate } from "utils/truncate.ts"; import { closetMilestone } from "./exec/snapshotTick.ts"; import { HOUR, MINUTE } from "@core/const/time.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/global.d.ts"; const log = (value: number, base: number = 10) => Math.log(value) / Math.log(base); diff --git a/packages/crawler/mq/task/collectSongs.ts b/packages/crawler/mq/task/collectSongs.ts index 1d7634d..dcf5472 100644 --- a/packages/crawler/mq/task/collectSongs.ts +++ b/packages/crawler/mq/task/collectSongs.ts @@ -3,7 +3,7 @@ import { aidExistsInSongs, getNotCollectedSongs } from "db/songs.ts"; import logger from "@core/log/logger.ts"; import { scheduleSnapshot } from "db/snapshotSchedule.ts"; import { MINUTE } from "@core/const/time.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; export async function collectSongs() { const aids = await getNotCollectedSongs(sql); diff --git a/packages/crawler/mq/task/getVideoDetails.ts b/packages/crawler/mq/task/getVideoDetails.ts index 80a5867..1fc618a 100644 --- a/packages/crawler/mq/task/getVideoDetails.ts +++ b/packages/crawler/mq/task/getVideoDetails.ts @@ -4,7 +4,7 @@ import logger from "@core/log/logger.ts"; import { ClassifyVideoQueue } from "mq/index.ts"; import { userExistsInBiliUsers, videoExistsInAllData } from "../../db/bilibili_metadata.ts"; import { HOUR, SECOND } from "@core/const/time.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; export async function insertVideoInfo(sql: Psql, aid: number) { const videoExists = await videoExistsInAllData(sql, aid); diff --git a/packages/crawler/mq/task/getVideoStats.ts b/packages/crawler/mq/task/getVideoStats.ts index 2a7d7ff..ffec09f 100644 --- a/packages/crawler/mq/task/getVideoStats.ts +++ b/packages/crawler/mq/task/getVideoStats.ts @@ -1,6 +1,6 @@ import { getVideoInfo } from "@core/net/getVideoInfo.ts"; import logger from "@core/log/logger.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; export interface SnapshotNumber { time: number; diff --git a/packages/crawler/mq/task/queueLatestVideo.ts b/packages/crawler/mq/task/queueLatestVideo.ts index af824f7..9f583c2 100644 --- a/packages/crawler/mq/task/queueLatestVideo.ts +++ b/packages/crawler/mq/task/queueLatestVideo.ts @@ -4,7 +4,7 @@ import { sleep } from "utils/sleep.ts"; import { SECOND } from "@core/const/time.ts"; import logger from "@core/log/logger.ts"; import { LatestVideosQueue } from "mq/index.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/psql.d.ts"; export async function queueLatestVideos( sql: Psql, diff --git a/packages/crawler/mq/task/regularSnapshotInterval.ts b/packages/crawler/mq/task/regularSnapshotInterval.ts index 11871d8..852d401 100644 --- a/packages/crawler/mq/task/regularSnapshotInterval.ts +++ b/packages/crawler/mq/task/regularSnapshotInterval.ts @@ -1,6 +1,6 @@ import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts"; import { HOUR } from "@core/const/time.ts"; -import type { Psql } from "global.d.ts"; +import type { Psql } from "@core/db/global.d.ts"; export const getRegularSnapshotInterval = async (sql: Psql, aid: number) => { const now = Date.now(); diff --git a/packages/crawler/src/filterWorker.ts b/packages/crawler/src/filterWorker.ts index 7ba2de6..ab85cba 100644 --- a/packages/crawler/src/filterWorker.ts +++ b/packages/crawler/src/filterWorker.ts @@ -3,7 +3,7 @@ import { redis } from "@core/db/redis.ts"; import logger from "@core/log/logger.ts"; import { classifyVideosWorker, classifyVideoWorker } from "mq/exec/classifyVideo.ts"; import { WorkerError } from "mq/schema.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import Akari from "ml/akari.ts"; const shutdown = async (signal: string) => { diff --git a/packages/crawler/src/worker.ts b/packages/crawler/src/worker.ts index ca644df..534f488 100644 --- a/packages/crawler/src/worker.ts +++ b/packages/crawler/src/worker.ts @@ -14,7 +14,7 @@ import { } from "mq/exec/executors.ts"; import { redis } from "@core/db/redis.ts"; import logger from "@core/log/logger.ts"; -import { lockManager } from "mq/lockManager.ts"; +import { lockManager } from "@core/mq/lockManager.ts"; import { WorkerError } from "mq/schema.ts"; const releaseLockForJob = async (name: string) => {