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) => {