Compare commits
No commits in common. "23917b2976b4f4846aa980add7d90bdcac431f57" and "44f4ee5b013037f6dddd8dae89643c7bfcd4cbd5" have entirely different histories.
23917b2976
...
44f4ee5b01
2
.gitignore
vendored
2
.gitignore
vendored
@ -40,5 +40,3 @@ dist/
|
||||
build/
|
||||
|
||||
docker-compose.yml
|
||||
|
||||
ucaptcha-config.yaml
|
6
.idea/compiler.xml
Normal file
6
.idea/compiler.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="useTypesFromServer" value="true" />
|
||||
</component>
|
||||
</project>
|
@ -28,8 +28,6 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/crawler/logs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/data" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/redis" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.2.8-debian
|
||||
FROM oven/bun
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -6,12 +6,6 @@ COPY ./packages/core ./core
|
||||
|
||||
COPY ./packages/backend/package.json ./packages/backend/bun.lock ./backend/
|
||||
|
||||
RUN apt update && apt install -y curl
|
||||
|
||||
RUN ln -s /bin/uname /usr/bin/uname
|
||||
|
||||
RUN /bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"
|
||||
|
||||
WORKDIR backend
|
||||
|
||||
RUN bun install
|
||||
|
@ -1,7 +1,5 @@
|
||||
FROM oven/bun
|
||||
|
||||
ARG BACKEND_URL
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
@ -14,7 +12,6 @@ RUN bun run build
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
ENV BACKEND_URL=${BACKEND_URL}
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
|
136
bun.lock
136
bun.lock
@ -19,7 +19,6 @@
|
||||
"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",
|
||||
@ -33,7 +32,6 @@
|
||||
"packages/core": {
|
||||
"name": "core",
|
||||
"dependencies": {
|
||||
"@koshnic/ratelimit": "^1.0.3",
|
||||
"chalk": "^5.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
"logform": "^2.7.0",
|
||||
@ -235,8 +233,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -373,8 +369,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -387,8 +381,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -425,8 +417,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -465,8 +455,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -475,14 +463,10 @@
|
||||
|
||||
"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@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=="],
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"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=="],
|
||||
@ -515,7 +499,7 @@
|
||||
|
||||
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
|
||||
|
||||
"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=="],
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
@ -583,8 +567,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -695,14 +677,8 @@
|
||||
|
||||
"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=="],
|
||||
@ -723,8 +699,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -733,17 +707,13 @@
|
||||
|
||||
"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@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": ["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-regex": ["glob-regex@0.3.2", "", {}, "sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw=="],
|
||||
|
||||
@ -791,8 +761,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -809,8 +777,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -821,21 +787,13 @@
|
||||
|
||||
"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-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="],
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
@ -845,8 +803,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -889,22 +845,16 @@
|
||||
|
||||
"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=="],
|
||||
@ -1021,7 +971,7 @@
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
@ -1031,8 +981,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1099,8 +1047,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1119,8 +1065,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1191,8 +1135,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1201,7 +1143,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@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@ -1269,8 +1211,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1339,8 +1279,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1377,8 +1315,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1401,8 +1337,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1495,8 +1429,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1515,8 +1447,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1543,8 +1473,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -1575,42 +1503,28 @@
|
||||
|
||||
"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=="],
|
||||
@ -1619,8 +1533,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1629,16 +1541,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -1685,18 +1587,14 @@
|
||||
|
||||
"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=="],
|
||||
@ -1705,10 +1603,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -1721,8 +1615,6 @@
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
16
deno.json
Normal file
16
deno.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
46
packages/backend/db/config.ts
Normal file
46
packages/backend/db/config.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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
|
||||
};
|
5
packages/backend/db/db.ts
Normal file
5
packages/backend/db/db.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import postgres from "postgres";
|
||||
import { postgresConfigNpm, postgresCredConfigNpm } from "./config";
|
||||
|
||||
export const sql = postgres(postgresConfigNpm);
|
||||
export const sqlCred = postgres(postgresCredConfigNpm)
|
@ -1,15 +1,15 @@
|
||||
import logger from "@core/log/logger.ts";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { sql } from "@core/db/dbNew.ts";
|
||||
import { sql } from "./db.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 "./snapshots.ts";
|
||||
import { idSchema } from "../routes/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) {
|
@ -1,4 +1,4 @@
|
||||
import { sql } from "@core/db/dbNew";
|
||||
import { sql } from "./db";
|
||||
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
|
||||
|
||||
export async function getVideoSnapshots(
|
@ -1,62 +0,0 @@
|
||||
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<CaptchaDifficultyConfig[]> => {
|
||||
return sql<CaptchaDifficultyConfig[]>`
|
||||
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<seconds> => {
|
||||
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<number | null> => {
|
||||
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;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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];
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
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<ErrorResponse>(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<ErrorResponse>(response, 400);
|
||||
}
|
||||
|
||||
const [r, err] = getJWTsecret();
|
||||
if (err) {
|
||||
return c.json<ErrorResponse>(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<ErrorResponse>(response, 401);
|
||||
}
|
||||
if (difficulty < requiredDifficulty) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Token too weak.",
|
||||
code: "UNAUTHORIZED"
|
||||
};
|
||||
return c.json<ErrorResponse>(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<ErrorResponse>(response, 400);
|
||||
}
|
||||
else if (e instanceof JwtTokenExpired) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Token expired.",
|
||||
code: "INVALID_CREDENTIALS"
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
}
|
||||
else if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse = {
|
||||
code: "INVALID_QUERY_PARAMS",
|
||||
message: "Invalid query parameters",
|
||||
errors: e.errors
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
}
|
||||
else {
|
||||
const response: ErrorResponse = {
|
||||
message: "Unknown error.",
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse>(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();
|
||||
};
|
@ -1,13 +1,12 @@
|
||||
import { Hono } from "hono";
|
||||
import { Variables } from "hono/types";
|
||||
import { bodyLimitForPing } from "./bodyLimits.ts";
|
||||
import { pingHandler } from "routes/ping";
|
||||
import { pingHandler } from "../routes/ping.ts";
|
||||
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 }>) {
|
||||
app.use("*", contentType);
|
||||
@ -16,6 +15,5 @@ export function configureMiddleWares(app: Hono<{ Variables: Variables }>) {
|
||||
app.use("*", logger({}));
|
||||
|
||||
app.post("/user", registerRateLimiter);
|
||||
app.post("/user", captchaMiddleware);
|
||||
app.all("/ping", bodyLimitForPing, ...pingHandler);
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -1,44 +1,27 @@
|
||||
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 { Context, Next } from "hono";
|
||||
import { generateRandomId } from "@core/lib/randomID.ts";
|
||||
import { RateLimiter } from "@koshnic/ratelimit";
|
||||
import { ErrorResponse } from "@/src/schema";
|
||||
import type { Context } from "hono";
|
||||
import { redis } from "@core/db/redis.ts";
|
||||
import { RedisStore } from "rate-limit-redis";
|
||||
|
||||
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;
|
||||
export const registerRateLimiter = rateLimiter<BlankEnv, "/user", {}>({
|
||||
windowMs: 60 * MINUTE,
|
||||
limit: 10,
|
||||
standardHeaders: "draft-6",
|
||||
keyGenerator: (c) => {
|
||||
const info = getConnInfo(c as unknown as Context<BlankEnv, "/user", {}>);
|
||||
if (!info.remote || !info.remote.address) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
const forwardedFor = c.req.header("X-Forwarded-For");
|
||||
if (forwardedFor) {
|
||||
ipAddr = forwardedFor.split(",")[0];
|
||||
}
|
||||
const path = c.req.path;
|
||||
const addr = info.remote.address;
|
||||
const path = new URL(c.req.url).pathname;
|
||||
const method = c.req.method;
|
||||
const ipIdentifier = includeIP ? `@${ipAddr}` : "";
|
||||
return `${method}-${path}${ipIdentifier}`;
|
||||
};
|
||||
|
||||
export const registerRateLimiter = async (c: Context<BlankEnv, "/user", {}>, 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
|
||||
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
|
||||
});
|
||||
|
||||
if (!allowed) {
|
||||
const response: ErrorResponse = {
|
||||
message: `Too many requests, please retry after ${Math.round(retryAfter)} seconds.`,
|
||||
code: "RATE_LIMIT_EXCEEDED"
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 429);
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
@ -10,7 +10,6 @@
|
||||
"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",
|
||||
|
@ -1,88 +0,0 @@
|
||||
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<BlankEnv & { Bindings: Bindings }, "/captcha/:id/result">) => {
|
||||
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<ErrorResponse>(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<ErrorResponse>(response, 401);
|
||||
} else if (data.error && res.status === 400) {
|
||||
const response: ErrorResponse = {
|
||||
message: data.error,
|
||||
code: "INVALID_QUERY_PARAMS"
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 400);
|
||||
} else if (data.error) {
|
||||
const response: ErrorResponse = {
|
||||
message: data.error,
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 500);
|
||||
}
|
||||
if (!data.success) {
|
||||
const response: ErrorResponse = {
|
||||
message: "Incorrect answer",
|
||||
code: "INVALID_CREDENTIALS"
|
||||
};
|
||||
return c.json<ErrorResponse>(response, 401);
|
||||
}
|
||||
|
||||
const [r, err] = getJWTsecret();
|
||||
if (err) {
|
||||
return c.json<ErrorResponse>(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
|
||||
});
|
||||
}
|
||||
);
|
@ -1,43 +0,0 @@
|
||||
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<unknown> = {
|
||||
code: "ENTITY_NOT_FOUND",
|
||||
message: "No difficulty configs found for this route."
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(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<ErrorResponse>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,2 +0,0 @@
|
||||
export * from "./session/POST.ts";
|
||||
export * from "./[id]/result/GET.ts";
|
@ -1,59 +0,0 @@
|
||||
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<CaptchaSessionResponse|unknown>(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<ErrorResponse>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
errors: [e]
|
||||
};
|
||||
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,29 +1,17 @@
|
||||
import { getSingerForBirthday, pickSinger, pickSpecialSinger, type Singer } from "lib/const/singers.ts";
|
||||
import { VERSION } from "src/main.ts";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
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";
|
||||
|
||||
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}生日快乐~`;
|
||||
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);
|
||||
}
|
||||
} else if (shouldShowSpecialSinger) {
|
||||
singer = pickSpecialSinger();
|
||||
} else {
|
||||
singer = pickSinger();
|
||||
}
|
||||
return c.json({
|
||||
project: {
|
||||
name: "中V档案馆",
|
||||
motto: "一起唱吧,心中的歌!"
|
||||
},
|
||||
status: 200,
|
||||
version: VERSION,
|
||||
time: Date.now(),
|
||||
singer: singer
|
||||
});
|
||||
});
|
||||
|
24
packages/backend/routes/ping.ts
Normal file
24
packages/backend/routes/ping.ts
Normal file
@ -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,
|
||||
}
|
||||
});
|
||||
});
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
29
packages/backend/routes/root/root.ts
Normal file
29
packages/backend/routes/root/root.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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
|
||||
});
|
||||
});
|
@ -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/snapshots.ts";
|
||||
import { getVideoSnapshots, getVideoSnapshotsByBV } from "../db/videoSnapshot.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<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<unknown> = {
|
||||
code: "UNKNOWN_ERROR",
|
||||
code: "UNKNOWN_ERR",
|
||||
message: "Unhandled error",
|
||||
errors: [e]
|
||||
};
|
@ -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 "@core/db/dbNew.ts";
|
||||
import { sqlCred } from "db/db.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<StatusResponse>(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<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
}
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
code: "UNKNOWN_ERR"
|
||||
}
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./register.ts";
|
@ -1,2 +0,0 @@
|
||||
export * from "./[id]/info";
|
||||
export * from "./[id]/snapshots";
|
@ -1,7 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import type { TimingVariables } from "hono/timing";
|
||||
import { startServer } from "./startServer.ts";
|
||||
import { configureRoutes } from "./routing.ts";
|
||||
import { configureRoutes } from "routes";
|
||||
import { configureMiddleWares } from "middleware";
|
||||
import { notFoundRoute } from "routes/404.ts";
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
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)
|
||||
}
|
19
packages/backend/src/schema.d.ts
vendored
19
packages/backend/src/schema.d.ts
vendored
@ -1,20 +1,9 @@
|
||||
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";
|
||||
type ErrorCode = "INVALID_QUERY_PARAMS" | "UNKNOWN_ERR" | "INVALID_PAYLOAD" | "INVALID_FORMAT" | "BODY_TOO_LARGE";
|
||||
|
||||
export interface ErrorResponse<E=string> {
|
||||
code: ErrorCode;
|
||||
export interface ErrorResponse<E> {
|
||||
code: ErrorCode
|
||||
message: string;
|
||||
errors?: E[];
|
||||
errors: E[];
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
|
@ -10,7 +10,6 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@core/*": ["../core/*"],
|
||||
"@/*": ["./*"],
|
||||
"@crawler/*": ["../crawler/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
@ -1,8 +1,6 @@
|
||||
import postgres from "postgres";
|
||||
import { postgresConfigCred, postgresConfig } from "./pgConfigNew";
|
||||
import { postgresConfigNpm } from "./pgConfigNew";
|
||||
|
||||
export const sql = postgres(postgresConfig);
|
||||
export const sql = postgres(postgresConfigNpm);
|
||||
|
||||
export const sqlCred = postgres(postgresConfigCred);
|
||||
|
||||
export const sqlTest = postgres(postgresConfig);
|
||||
export const sqlTest = postgres(postgresConfigNpm);
|
@ -1,23 +1,27 @@
|
||||
const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT", "DB_NAME_CRED"];
|
||||
|
||||
const getEnvVar = (key: string) => {
|
||||
return process.env[key] || import.meta.env[key];
|
||||
}
|
||||
|
||||
const unsetVars = requiredEnvVars.filter((key) => getEnvVar(key) === undefined);
|
||||
const unsetVars = requiredEnvVars.filter((key) => process.env[key] === undefined);
|
||||
|
||||
if (unsetVars.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`);
|
||||
}
|
||||
|
||||
const databaseHost = getEnvVar("DB_HOST")!;
|
||||
const databaseName = getEnvVar("DB_NAME");
|
||||
const databaseNameCred = getEnvVar("DB_NAME_CRED")!;
|
||||
const databaseUser = getEnvVar("DB_USER")!;
|
||||
const databasePassword = getEnvVar("DB_PASSWORD")!;
|
||||
const databasePort = getEnvVar("DB_PORT")!;
|
||||
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,
|
||||
|
17
packages/core/deno.json
Normal file
17
packages/core/deno.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
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<void> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
packages/core/mq/rateLimiter.ts
Normal file
56
packages/core/mq/rateLimiter.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
for (let i = 0; i < this.configs.length; i++) {
|
||||
const config = this.configs[i];
|
||||
const eventName = this.configEventNames[i];
|
||||
await config.window.clear(eventName);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,20 +32,14 @@ export class SlidingWindow {
|
||||
|
||||
/*
|
||||
* Count the number of events in the sliding window
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {number} [duration] The duration of the window in seconds
|
||||
* @param eventName The name of the event
|
||||
*/
|
||||
async count(eventName: string, duration?: number): Promise<number> {
|
||||
async count(eventName: string): Promise<number> {
|
||||
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);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import logger from "@core/log/logger.ts";
|
||||
import { MultipleRateLimiter, RateLimiterError, type RateLimiterConfig } from "@core/mq/multipleRateLimiter.ts";
|
||||
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
|
||||
import { SlidingWindow } from "mq/slidingWindow.ts";
|
||||
import { redis } from "db/redis.ts";
|
||||
import { ReplyError } from "ioredis";
|
||||
import { SECOND } from "@core/const/time.ts";
|
||||
import { SECOND } from "../const/time.ts";
|
||||
import { spawn, SpawnOptions } from "child_process";
|
||||
|
||||
export function spawnPromise(
|
||||
@ -71,11 +73,11 @@ export class NetSchedulerError extends Error {
|
||||
}
|
||||
|
||||
type LimiterMap = {
|
||||
[name: string]: MultipleRateLimiter;
|
||||
[name: string]: RateLimiter;
|
||||
};
|
||||
|
||||
type OptionalLimiterMap = {
|
||||
[name: string]: MultipleRateLimiter | null;
|
||||
[name: string]: RateLimiter | null;
|
||||
};
|
||||
|
||||
type TaskMap = {
|
||||
@ -119,23 +121,20 @@ class NetworkDelegate {
|
||||
const proxies = this.getTaskProxies(taskName);
|
||||
for (const proxyName of proxies) {
|
||||
const limiterId = "proxy-" + proxyName + "-" + taskName;
|
||||
this.proxyLimiters[limiterId] = config ? new MultipleRateLimiter(limiterId, config) : null;
|
||||
this.proxyLimiters[limiterId] = config ? new RateLimiter(limiterId, config) : null;
|
||||
}
|
||||
}
|
||||
|
||||
async triggerLimiter(task: string, proxy: string, force: boolean = false): Promise<void> {
|
||||
async triggerLimiter(task: string, proxy: string): Promise<void> {
|
||||
const limiterId = "proxy-" + proxy + "-" + task;
|
||||
const providerLimiterId = "provider-" + proxy + "-" + this.tasks[task].provider;
|
||||
try {
|
||||
await this.proxyLimiters[limiterId]?.trigger(!force);
|
||||
await this.providerLimiters[providerLimiterId]?.trigger(!force);
|
||||
await this.proxyLimiters[limiterId]?.trigger();
|
||||
await this.providerLimiters[providerLimiterId]?.trigger();
|
||||
} 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");
|
||||
}
|
||||
@ -150,7 +149,7 @@ class NetworkDelegate {
|
||||
}
|
||||
for (const proxyName of bindProxies) {
|
||||
const limiterId = "provider-" + proxyName + "-" + providerName;
|
||||
this.providerLimiters[limiterId] = new MultipleRateLimiter(limiterId, config);
|
||||
this.providerLimiters[limiterId] = new RateLimiter(limiterId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,15 +168,9 @@ class NetworkDelegate {
|
||||
// find a available proxy
|
||||
const proxiesNames = this.getTaskProxies(task);
|
||||
for (const proxyName of shuffleArray(proxiesNames)) {
|
||||
try {
|
||||
if (await this.getProxyAvailability(proxyName, task)) {
|
||||
return await this.proxyRequest<R>(url, proxyName, task, method);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof RateLimiterError) {
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw new NetSchedulerError("No proxy is available currently.", "NO_PROXY_AVAILABLE");
|
||||
}
|
||||
@ -209,8 +202,16 @@ class NetworkDelegate {
|
||||
throw new NetSchedulerError(`Proxy "${proxyName}" not found`, "PROXY_NOT_FOUND");
|
||||
}
|
||||
|
||||
await this.triggerLimiter(task, proxyName, force);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.makeRequest<R>(url, proxy, method);
|
||||
await this.triggerLimiter(task, proxyName);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -225,6 +226,32 @@ class NetworkDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private async getProxyAvailability(proxyName: string, taskName: string): Promise<boolean> {
|
||||
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<R>(url: string, method: string): Promise<R> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
@ -289,37 +316,37 @@ class NetworkDelegate {
|
||||
const networkDelegate = new NetworkDelegate();
|
||||
const videoInfoRateLimiterConfig: RateLimiterConfig[] = [
|
||||
{
|
||||
duration: 0.3,
|
||||
window: new SlidingWindow(redis, 0.3),
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
duration: 3,
|
||||
window: new SlidingWindow(redis, 3),
|
||||
max: 5,
|
||||
},
|
||||
{
|
||||
duration: 30,
|
||||
window: new SlidingWindow(redis, 30),
|
||||
max: 30,
|
||||
},
|
||||
{
|
||||
duration: 2 * 60,
|
||||
window: new SlidingWindow(redis, 2 * 60),
|
||||
max: 50,
|
||||
},
|
||||
];
|
||||
const biliLimiterConfig: RateLimiterConfig[] = [
|
||||
{
|
||||
duration: 1,
|
||||
window: new SlidingWindow(redis, 1),
|
||||
max: 6,
|
||||
},
|
||||
{
|
||||
duration: 5,
|
||||
window: new SlidingWindow(redis, 5),
|
||||
max: 20,
|
||||
},
|
||||
{
|
||||
duration: 30,
|
||||
window: new SlidingWindow(redis, 30),
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
duration: 5 * 60,
|
||||
window: new SlidingWindow(redis, 5 * 60),
|
||||
max: 200,
|
||||
},
|
||||
];
|
||||
|
@ -1,10 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
|
@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
import { AllDataType, BiliUserType } from "@core/db/schema";
|
||||
import { AkariModelVersion } from "ml/const";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { LatestSnapshotType } from "@core/db/schema";
|
||||
import { SnapshotNumber } from "mq/task/getVideoStats.ts";
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export async function getVideosNearMilestone(sql: Psql) {
|
||||
const queryResult = await sql<LatestSnapshotType[]>`
|
||||
|
@ -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 "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
const REDIS_KEY = "cvsa:snapshot_window_counts";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
import { parseTimestampFromPsql } from "utils/formatTimestampToPostgre.ts";
|
||||
|
||||
export async function getNotCollectedSongs(sql: Psql) {
|
||||
|
@ -1,3 +1,3 @@
|
||||
import type postgres from "postgres";
|
||||
|
||||
export type Psql = postgres.Sql;
|
||||
export type Psql = postgres.Sql<{}>;
|
@ -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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import { getLatestVideoSnapshot } from "db/snapshot.ts";
|
||||
import { HOUR, MINUTE } from "@core/const/time.ts";
|
||||
import { sql } from "@core/db/dbNew";
|
||||
|
@ -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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import { aidExistsInSongs } from "db/songs.ts";
|
||||
import { insertIntoSongs } from "mq/task/collectSongs.ts";
|
||||
import { scheduleSnapshot } from "db/snapshotSchedule.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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import { getRegularSnapshotInterval } from "mq/task/regularSnapshotInterval.ts";
|
||||
import { sql } from "@core/db/dbNew.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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts";
|
||||
import { insertVideoSnapshot } from "mq/task/getVideoStats.ts";
|
||||
import { getSongsPublihsedAt } from "db/songs.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;
|
@ -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 "@core/db/global.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
const log = (value: number, base: number = 10) => Math.log(value) / Math.log(base);
|
||||
|
||||
|
@ -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 "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export async function collectSongs() {
|
||||
const aids = await getNotCollectedSongs(sql);
|
||||
|
@ -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 "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export async function insertVideoInfo(sql: Psql, aid: number) {
|
||||
const videoExists = await videoExistsInAllData(sql, aid);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { getVideoInfo } from "@core/net/getVideoInfo.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
import type { Psql } from "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export interface SnapshotNumber {
|
||||
time: number;
|
||||
|
@ -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 "@core/db/psql.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export async function queueLatestVideos(
|
||||
sql: Psql,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts";
|
||||
import { HOUR } from "@core/const/time.ts";
|
||||
import type { Psql } from "@core/db/global.d.ts";
|
||||
import type { Psql } from "global.d.ts";
|
||||
|
||||
export const getRegularSnapshotInterval = async (sql: Psql, aid: number) => {
|
||||
const now = Date.now();
|
||||
|
@ -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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import Akari from "ml/akari.ts";
|
||||
|
||||
const shutdown = async (signal: string) => {
|
||||
|
@ -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 "@core/mq/lockManager.ts";
|
||||
import { lockManager } from "mq/lockManager.ts";
|
||||
import { WorkerError } from "mq/schema.ts";
|
||||
|
||||
const releaseLockForJob = async (name: string) => {
|
||||
|
@ -3,6 +3,6 @@ const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<tr>
|
||||
<td class="max-w-14 min-w-14 md:max-w-24 md:min-w-24 border dark:border-zinc-500 px-2 md:px-3 py-2 font-semibold">{title}</td>
|
||||
<td class="max-w-14 min-w-14 md:max-w-none md:min-w-none border dark:border-zinc-500 px-2 md:px-4 py-2 font-semibold">{title}</td>
|
||||
<td class="break-all max-w-[calc(100vw-4.5rem)] border dark:border-zinc-500 px-4 py-2">{description}</td>
|
||||
</tr>
|
@ -7,7 +7,7 @@
|
||||
};
|
||||
|
||||
export function changeFocusState(target: boolean) {
|
||||
if (!inputElement) return;
|
||||
if (!inputElement) return; // 使用 inputElement 而不是 inputBox
|
||||
if (target) {
|
||||
inputElement.focus();
|
||||
} else {
|
||||
@ -22,13 +22,13 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const value = inputValue.trim();
|
||||
const value = inputValue.trim(); // 使用绑定的变量
|
||||
if (!value) return;
|
||||
search(value);
|
||||
}
|
||||
}
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
let inputElement: HTMLInputElement; // 引用 input 元素
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex justify-between w-36">
|
||||
<span>
|
||||
{title}
|
||||
</span>
|
||||
<span>
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
@ -9,6 +9,22 @@ import { getAidFromBV } from "src/db/bilibili_metadata/getAidFromBV";
|
||||
import { getVideoMetadata } from "src/db/bilibili_metadata/getVideoMetadata";
|
||||
import { aidExists as idExists } from "src/db/bilibili_metadata/aidExists";
|
||||
|
||||
const databaseHost = import.meta.env.DB_HOST;
|
||||
const databaseName = import.meta.env.DB_NAME;
|
||||
const databaseUser = import.meta.env.DB_USER;
|
||||
const databasePassword = import.meta.env.DB_PASSWORD;
|
||||
const databasePort = import.meta.env.DB_PORT;
|
||||
|
||||
const postgresConfig = {
|
||||
hostname: databaseHost,
|
||||
port: parseInt(databasePort!),
|
||||
database: databaseName,
|
||||
user: databaseUser,
|
||||
password: databasePassword,
|
||||
};
|
||||
|
||||
console.log(postgresConfig);
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
async function getVideoAid(id: string) {
|
||||
@ -36,6 +52,18 @@ if (!aidExists) {
|
||||
}
|
||||
const videoInfo = await getVideoMetadata(aid);
|
||||
const snapshots = await getAllSnapshots(aid);
|
||||
|
||||
interface Snapshot {
|
||||
created_at: Date;
|
||||
views: number;
|
||||
danmakus: number;
|
||||
replies: number;
|
||||
coins: number;
|
||||
likes: number;
|
||||
favorites: number;
|
||||
shares: number;
|
||||
id: number;
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
@ -51,9 +79,9 @@ const snapshots = await getAllSnapshots(aid);
|
||||
<div class="overflow-x-auto max-w-full px-2">
|
||||
<table class="table-fixed">
|
||||
<tbody>
|
||||
<MetadataRow title="ID" description={videoInfo?.id} />
|
||||
<MetadataRow title="av 号" description={videoInfo?.aid} />
|
||||
<MetadataRow title="BV 号" description={videoInfo?.bvid} />
|
||||
<MetadataRow title={id} description={videoInfo?.id} />
|
||||
<MetadataRow title={videoInfo?.aid} description={videoInfo?.aid} />
|
||||
<MetadataRow title={videoInfo?.bvid} description={videoInfo?.bvid} />
|
||||
<MetadataRow title="标题" description={videoInfo?.title} />
|
||||
<MetadataRow title="描述" description={videoInfo?.description} />
|
||||
<MetadataRow title="UID" description={videoInfo?.uid} />
|
||||
@ -96,7 +124,7 @@ const snapshots = await getAllSnapshots(aid);
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snapshots.map((snapshot) => (
|
||||
{snapshots.map((snapshot: Snapshot) => (
|
||||
<tr>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{format(new Date(snapshot.created_at), "yyyy-MM-dd HH:mm:ss", {
|
||||
|
@ -1,68 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import TitleBar from "@components/TitleBar.astro";
|
||||
import MetadataRow from "@components/InfoPage/MetadataRow.astro";
|
||||
import { format } from "date-fns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { getAllSnapshots } from "src/db/snapshots/getAllSnapshots";
|
||||
import { getAidFromBV } from "src/db/bilibili_metadata/getAidFromBV";
|
||||
import { getVideoMetadata } from "src/db/bilibili_metadata/getVideoMetadata";
|
||||
import { aidExists as idExists } from "src/db/bilibili_metadata/aidExists";
|
||||
import StatRow from "@components/VideoInfoPage/StatRow.astro";
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
if (!id) {
|
||||
Astro.response.status = 404;
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const backendURL = import.meta.env.BACKEND_URL;
|
||||
const res = await fetch(backendURL + `video/${id}/info`);
|
||||
const data = await res.json();
|
||||
---
|
||||
|
||||
<Layout title={`${data.title ?? data.bvid} - 视频信息`}>
|
||||
<TitleBar />
|
||||
<main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
|
||||
<div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6 px-4">
|
||||
<h2 class="text-lg md:text-2xl mb-2">
|
||||
<a href={`https://www.bilibili.com/video/${data.bvid}`}>{data.title}</a>
|
||||
</h2>
|
||||
<p
|
||||
class="text-sm md:text-base font-normal text-on-surface-variant
|
||||
dark:text-dark-on-surface-variant mb-4"
|
||||
>
|
||||
<span>{data.bvid} · av{data.aid}</span><br />
|
||||
<span>
|
||||
发布于
|
||||
{format(new Date(data.pubdate * 1000), "yyyy-MM-dd HH:mm:ss")}
|
||||
</span><br />
|
||||
<span>播放:{(data.stat?.view ?? 0).toLocaleString()}</span> ·
|
||||
<span>弹幕:{(data.stat?.danmaku ?? 0).toLocaleString()}</span>
|
||||
<br/>
|
||||
<span>分区: {data.tname}, tid{data.tid} · v2: {data.tname_v2}, tid{data.tid_v2}</span>
|
||||
</p>
|
||||
<img src={data.pic} referrerpolicy="no-referrer" class="rounded-lg" />
|
||||
|
||||
<h3 class="font-medium text-lg mt-6 mb-1">简介</h3>
|
||||
<pre
|
||||
class="max-w-full wrap-anywhere break-all text-on-surface-variant
|
||||
text-sm md:text-base whitespace-pre-wrap dark:text-dark-on-surface-variant
|
||||
font-zh">{data.desc}</pre>
|
||||
|
||||
<div class="mb-6 mt-4">
|
||||
<h2 class="mb-2 text-xl font-medium">统计数据</h2>
|
||||
<div class="flex flex-col gap-1">
|
||||
<StatRow title="播放" description={data.stat?.view} />
|
||||
<StatRow title="点赞" description={data.stat?.like} />
|
||||
<StatRow title="收藏" description={data.stat?.favorite} />
|
||||
<StatRow title="硬币" description={data.stat?.coin} />
|
||||
<StatRow title="评论" description={data.stat?.reply} />
|
||||
<StatRow title="弹幕" description={data.stat?.danmaku} />
|
||||
<StatRow title="分享" description={data.stat?.share} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
@ -102,8 +102,6 @@
|
||||
--color-dark-surface-container-low: rgb(35 25 24);
|
||||
--color-surface-container-highest: rgb(241 223 220);
|
||||
--color-dark-surface-container-highest: rgb(61 50 48);
|
||||
|
||||
--font-zh: "InterVariable", "MiSans VF", sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
|
Loading…
Reference in New Issue
Block a user