diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..656bebc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# App config files (must be mounted at runtime) +keypair_*.pem +.env + +# macOS +.DS_Store + +# Build artifacts +node_modules +dist +coverage +.nyc_output +openapi_exported.json + +# Editor +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Debug logs +*.log + +# Git +.git +.github +.gitignore +.husky + +# Test files (not needed in production image) +test/ diff --git a/.eslintrc b/.eslintrc index 795f260..1bf01c7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,22 +1,45 @@ { + "parser": "@typescript-eslint/parser", "plugins": [ - "jsdoc" + "jsdoc", + "@typescript-eslint" ], "extends": [ "eslint:recommended", - "google" + "google", + "plugin:@typescript-eslint/recommended" ], - "rules": { - "linebreak-style": 1, - "indent": [ - "error", - 4 - ], - "quotes": [ - "error", - "double" - ] - }, + "rules": { + "linebreak-style": 0, + "indent": [ + "error", + 4 + ], + "quotes": [ + "error", + "double" + ], + "@typescript-eslint/no-explicit-any": 0, + "new-cap": [ + "error", + { + "capIsNewExceptions": [ + "Object", + "String", + "Optional", + "Number", + "Boolean", + "Any", + "Union", + "Intersect", + "Record", + "Array", + "Enum", + "Nullable" + ] + } + ] + }, "env": { "es6": true, "node": true, @@ -24,6 +47,7 @@ "browser": false }, "parserOptions": { - "ecmaVersion": 2021 + "ecmaVersion": 2021, + "sourceType": "module" } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8e4d326..d4c0e19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,23 @@ -FROM node:20-alpine +FROM oven/bun:alpine -ENV RUNTIME_ENV=container +ENV TRUST_PROXY="uniquelocal" +ENV HTTP_HOSTNAME="0.0.0.0" +ENV RUNTIME_ENV="container" -RUN adduser -u 3000 -D recv +RUN addgroup -g 3000 recv && \ + adduser -HD -u 3000 -G recv -h /app recv -RUN mkdir -p /.npm /workplace -WORKDIR /workplace -ADD . /workplace - -RUN chown -R \ - 3000:3000 \ - /.npm /workplace +WORKDIR /app +RUN chown 3000:3000 /app USER 3000 -RUN npm install -EXPOSE 3000 -CMD ["npm", "start"] +COPY --chown=3000:3000 package.json ./ +RUN bun install --production + +COPY --chown=3000:3000 . . + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/robots.txt || exit 1 +CMD ["bun", "start"] diff --git a/app.js b/app.js deleted file mode 100644 index e499430..0000000 --- a/app.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; - -// Import config -const { - runLoader, - getMust, - getEnvironmentOverview, -} = require("./src/config"); - -// Load config -runLoader(); - -// Import constants -const constant = require("./src/init/const"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Import useApp -const {useApp} = require("./src/init/express"); - -// Initialize application -const app = useApp(); - -// Initialize prepare handlers -const { - prepare: prepareDatabase, -} = require("./src/init/database"); - -const prepareHandlers = [ - prepareDatabase, -]; - -// Redirect / to INDEX_REDIRECT_URL -app.get("/", (_, res) => { - const redirectCode = getMust("INDEX_REDIRECT_TYPE") === "permanent" ? - StatusCodes.MOVED_PERMANENTLY : - StatusCodes.MOVED_TEMPORARILY; - const redirectUrl = getMust("INDEX_REDIRECT_URL"); - res.redirect(redirectCode, redirectUrl); -}); - -// The handler for robots.txt (deny all friendly robots) -app.get("/robots.txt", (_, res) => { - res.type("txt").send("User-agent: *\nDisallow: /"); -}); - -// Load router dispatcher -const routerDispatcher = require("./src/routes/index"); -routerDispatcher.load(); - -// Show banner message -(() => { - const {APP_NAME: appName} = constant; - const {node, runtime} = getEnvironmentOverview(); - const statusMessage = `(environment: ${node}, ${runtime})`; - console.info(appName, statusMessage, "\n===="); -})(); - -// Mount application and execute it -require("./src/execute")(app, prepareHandlers, - ({protocol, hostname, port}) => { - console.info(`Protocol "${protocol}" is listening at`); - console.info(`${protocol}://${hostname}:${port}`); - }, -); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..886558b --- /dev/null +++ b/bun.lock @@ -0,0 +1,1711 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "sara.recv", + "dependencies": { + "@elysiajs/cors": "^1.4.2", + "@elysiajs/swagger": "^1.3.1", + "@simplewebauthn/server": "^11.0.0", + "@simplewebauthn/types": "^11.0.0", + "@sinclair/typebox": "^0.34.49", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "elysia": "^1.4.28", + "elysia-rate-limit": "^4.6.1", + "http-status-codes": "^2.2.0", + "jsonwebtoken": "^9.0.0", + "mongoose": "^7.0.3", + "mongoose-to-swagger": "^1.4.0", + "nanoid": "^3.3.4", + "node-cache": "^5.1.2", + "nodemailer": "^6.7.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^4.6.2", + "ua-parser-js": "^1.0.39", + }, + "devDependencies": { + "@commitlint/cli": "^17.4.4", + "@commitlint/config-conventional": "^17.4.4", + "@types/bun": "^1.3.13", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^20.0.0", + "@types/nodemailer": "^8.0.0", + "@types/ua-parser-js": "^0.7.39", + "@typescript-eslint/eslint-plugin": "^8.59.3", + "@typescript-eslint/parser": "^8.59.3", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^8.17.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-jsdoc": "^50.4.3", + "husky": "^8.0.1", + "lint-staged": "^13.2.0", + "mocha": "^10.0.0", + "mocha-steps": "^1.3.0", + "nodemon": "^2.0.13", + "nyc": "^15.1.0", + "supertest": "^6.2.3", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@9.1.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.6", "call-me-maybe": "^1.0.1", "js-yaml": "^4.1.0" } }, "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg=="], + + "@apidevtools/openapi-schemas": ["@apidevtools/openapi-schemas@2.1.0", "", {}, "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="], + + "@apidevtools/swagger-methods": ["@apidevtools/swagger-methods@3.0.2", "", {}, "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="], + + "@apidevtools/swagger-parser": ["@apidevtools/swagger-parser@10.0.3", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.6", "@apidevtools/openapi-schemas": "^2.0.4", "@apidevtools/swagger-methods": "^3.0.2", "@jsdevtools/ono": "^7.1.3", "call-me-maybe": "^1.0.1", "z-schema": "^5.0.1" }, "peerDependencies": { "openapi-types": ">=7" } }, "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + + "@commitlint/cli": ["@commitlint/cli@17.8.1", "", { "dependencies": { "@commitlint/format": "^17.8.1", "@commitlint/lint": "^17.8.1", "@commitlint/load": "^17.8.1", "@commitlint/read": "^17.8.1", "@commitlint/types": "^17.8.1", "execa": "^5.0.0", "lodash.isfunction": "^3.0.9", "resolve-from": "5.0.0", "resolve-global": "1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg=="], + + "@commitlint/config-conventional": ["@commitlint/config-conventional@17.8.1", "", { "dependencies": { "conventional-changelog-conventionalcommits": "^6.1.0" } }, "sha512-NxCOHx1kgneig3VLauWJcDWS40DVjg7nKOpBEEK9E5fjJpQqLCilcnKkIIjdBH98kEO1q3NpE5NSrZ2kl/QGJg=="], + + "@commitlint/config-validator": ["@commitlint/config-validator@17.8.1", "", { "dependencies": { "@commitlint/types": "^17.8.1", "ajv": "^8.11.0" } }, "sha512-UUgUC+sNiiMwkyiuIFR7JG2cfd9t/7MV8VB4TZ+q02ZFkHoduUS4tJGsCBWvBOGD9Btev6IecPMvlWUfJorkEA=="], + + "@commitlint/ensure": ["@commitlint/ensure@17.8.1", "", { "dependencies": { "@commitlint/types": "^17.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-xjafwKxid8s1K23NFpL8JNo6JnY/ysetKo8kegVM7c8vs+kWLP8VrQq+NbhgVlmCojhEDbzQKp4eRXSjVOGsow=="], + + "@commitlint/execute-rule": ["@commitlint/execute-rule@17.8.1", "", {}, "sha512-JHVupQeSdNI6xzA9SqMF+p/JjrHTcrJdI02PwesQIDCIGUrv04hicJgCcws5nzaoZbROapPs0s6zeVHoxpMwFQ=="], + + "@commitlint/format": ["@commitlint/format@17.8.1", "", { "dependencies": { "@commitlint/types": "^17.8.1", "chalk": "^4.1.0" } }, "sha512-f3oMTyZ84M9ht7fb93wbCKmWxO5/kKSbwuYvS867duVomoOsgrgljkGGIztmT/srZnaiGbaK8+Wf8Ik2tSr5eg=="], + + "@commitlint/is-ignored": ["@commitlint/is-ignored@17.8.1", "", { "dependencies": { "@commitlint/types": "^17.8.1", "semver": "7.5.4" } }, "sha512-UshMi4Ltb4ZlNn4F7WtSEugFDZmctzFpmbqvpyxD3la510J+PLcnyhf9chs7EryaRFJMdAKwsEKfNK0jL/QM4g=="], + + "@commitlint/lint": ["@commitlint/lint@17.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^17.8.1", "@commitlint/parse": "^17.8.1", "@commitlint/rules": "^17.8.1", "@commitlint/types": "^17.8.1" } }, "sha512-aQUlwIR1/VMv2D4GXSk7PfL5hIaFSfy6hSHV94O8Y27T5q+DlDEgd/cZ4KmVI+MWKzFfCTiTuWqjfRSfdRllCA=="], + + "@commitlint/load": ["@commitlint/load@17.8.1", "", { "dependencies": { "@commitlint/config-validator": "^17.8.1", "@commitlint/execute-rule": "^17.8.1", "@commitlint/resolve-extends": "^17.8.1", "@commitlint/types": "^17.8.1", "@types/node": "20.5.1", "chalk": "^4.1.0", "cosmiconfig": "^8.0.0", "cosmiconfig-typescript-loader": "^4.0.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0", "resolve-from": "^5.0.0", "ts-node": "^10.8.1", "typescript": "^4.6.4 || ^5.2.2" } }, "sha512-iF4CL7KDFstP1kpVUkT8K2Wl17h2yx9VaR1ztTc8vzByWWcbO/WaKwxsnCOqow9tVAlzPfo1ywk9m2oJ9ucMqA=="], + + "@commitlint/message": ["@commitlint/message@17.8.1", "", {}, "sha512-6bYL1GUQsD6bLhTH3QQty8pVFoETfFQlMn2Nzmz3AOLqRVfNNtXBaSY0dhZ0dM6A2MEq4+2d7L/2LP8TjqGRkA=="], + + "@commitlint/parse": ["@commitlint/parse@17.8.1", "", { "dependencies": { "@commitlint/types": "^17.8.1", "conventional-changelog-angular": "^6.0.0", "conventional-commits-parser": "^4.0.0" } }, "sha512-/wLUickTo0rNpQgWwLPavTm7WbwkZoBy3X8PpkUmlSmQJyWQTj0m6bDjiykMaDt41qcUbfeFfaCvXfiR4EGnfw=="], + + "@commitlint/read": ["@commitlint/read@17.8.1", "", { "dependencies": { "@commitlint/top-level": "^17.8.1", "@commitlint/types": "^17.8.1", "fs-extra": "^11.0.0", "git-raw-commits": "^2.0.11", "minimist": "^1.2.6" } }, "sha512-Fd55Oaz9irzBESPCdMd8vWWgxsW3OWR99wOntBDHgf9h7Y6OOHjWEdS9Xzen1GFndqgyoaFplQS5y7KZe0kO2w=="], + + "@commitlint/resolve-extends": ["@commitlint/resolve-extends@17.8.1", "", { "dependencies": { "@commitlint/config-validator": "^17.8.1", "@commitlint/types": "^17.8.1", "import-fresh": "^3.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0", "resolve-global": "^1.0.0" } }, "sha512-W/ryRoQ0TSVXqJrx5SGkaYuAaE/BUontL1j1HsKckvM6e5ZaG0M9126zcwL6peKSuIetJi7E87PRQF8O86EW0Q=="], + + "@commitlint/rules": ["@commitlint/rules@17.8.1", "", { "dependencies": { "@commitlint/ensure": "^17.8.1", "@commitlint/message": "^17.8.1", "@commitlint/to-lines": "^17.8.1", "@commitlint/types": "^17.8.1", "execa": "^5.0.0" } }, "sha512-2b7OdVbN7MTAt9U0vKOYKCDsOvESVXxQmrvuVUZ0rGFMCrCPJWWP1GJ7f0lAypbDAhaGb8zqtdOr47192LBrIA=="], + + "@commitlint/to-lines": ["@commitlint/to-lines@17.8.1", "", {}, "sha512-LE0jb8CuR/mj6xJyrIk8VLz03OEzXFgLdivBytoooKO5xLt5yalc8Ma5guTWobw998sbR3ogDd+2jed03CFmJA=="], + + "@commitlint/top-level": ["@commitlint/top-level@17.8.1", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-l6+Z6rrNf5p333SHfEte6r+WkOxGlWK4bLuZKbtf/2TXRN+qhrvn1XE63VhD8Oe9oIHQ7F7W1nG2k/TJFhx2yA=="], + + "@commitlint/types": ["@commitlint/types@17.8.1", "", { "dependencies": { "chalk": "^4.1.0" } }, "sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], + + "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.50.2", "", { "dependencies": { "@types/estree": "^1.0.6", "@typescript-eslint/types": "^8.11.0", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" } }, "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.11", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.7.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-iD3VskhVQnM4nE3PN9cBdPTR7JrqZy3FYk+uD2CeG6DUqKoANqaEfx0f7izPmW+Qm5JBM35ek+viLCmjy18ByQ=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.7.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.7.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.7.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.7.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g=="], + + "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + + "@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="], + + "@simplewebauthn/server": ["@simplewebauthn/server@11.0.0", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@simplewebauthn/types": "^11.0.0", "cross-fetch": "^4.0.0" } }, "sha512-zu8dxKcPiRUNSN2kmrnNOzNbRI8VaR/rL4ENCHUfC6PEE7SAAdIql9g5GBOd/wOVZolIsaZz3ccFxuGoVP0iaw=="], + + "@simplewebauthn/types": ["@simplewebauthn/types@11.0.0", "", {}, "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + + "@types/nodemailer": ["@types/nodemailer@8.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA=="], + + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/ua-parser-js": ["@types/ua-parser-js@0.7.39", "", {}, "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg=="], + + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + + "@types/whatwg-url": ["@types/whatwg-url@8.2.2", "", { "dependencies": { "@types/node": "*", "@types/webidl-conversions": "*" } }, "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "append-transform": ["append-transform@2.0.0", "", { "dependencies": { "default-require-extensions": "^3.0.0" } }, "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg=="], + + "archy": ["archy@1.0.0", "", {}, "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw=="], + + "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], + + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], + + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "bson": ["bson@5.5.1", "", {}, "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cachedir": ["cachedir@2.3.0", "", {}, "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw=="], + + "caching-transform": ["caching-transform@4.0.0", "", { "dependencies": { "hasha": "^5.0.0", "make-dir": "^3.0.0", "package-hash": "^4.0.0", "write-file-atomic": "^3.0.0" } }, "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "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=="], + + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], + + "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@11.0.0", "", {}, "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ=="], + + "comment-parser": ["comment-parser@1.4.1", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="], + + "commitizen": ["commitizen@4.3.1", "", { "dependencies": { "cachedir": "2.3.0", "cz-conventional-changelog": "3.3.0", "dedent": "0.7.0", "detect-indent": "6.1.0", "find-node-modules": "^2.1.2", "find-root": "1.1.0", "fs-extra": "9.1.0", "glob": "7.2.3", "inquirer": "8.2.5", "is-utf8": "^0.2.1", "lodash": "4.17.21", "minimist": "1.2.7", "strip-bom": "4.0.0", "strip-json-comments": "3.1.1" }, "bin": { "cz": "bin/git-cz", "git-cz": "bin/git-cz", "commitizen": "bin/commitizen" } }, "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@6.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@6.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw=="], + + "conventional-commit-types": ["conventional-commit-types@3.0.0", "", {}, "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg=="], + + "conventional-commits-parser": ["conventional-commits-parser@4.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^1.0.1", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg=="], + + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@4.4.0", "", { "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=7", "ts-node": ">=10", "typescript": ">=4" } }, "sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], + + "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + + "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "default-require-extensions": ["default-require-extensions@3.0.1", "", { "dependencies": { "strip-bom": "^4.0.0" } }, "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-file": ["detect-file@1.0.0", "", {}, "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + + "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.352", "", {}, "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg=="], + + "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], + + "elysia-rate-limit": ["elysia-rate-limit@4.6.1", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-SsA8oD/FgCSAslDTrPm+GogwCjISPjNtMSkEVEbvnUwH7PsGdqDqP5RfN167evESiC+tIxZA3ZndHP1c5O3Qvw=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], + + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-google": ["eslint-config-google@0.14.0", "", { "peerDependencies": { "eslint": ">=5.16.0" } }, "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw=="], + + "eslint-plugin-jsdoc": ["eslint-plugin-jsdoc@50.8.0", "", { "dependencies": { "@es-joy/jsdoccomment": "~0.50.2", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", "debug": "^4.4.1", "escape-string-regexp": "^4.0.0", "espree": "^10.3.0", "esquery": "^1.6.0", "parse-imports-exports": "^0.2.4", "semver": "^7.7.2", "spdx-expression-parse": "^4.0.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="], + + "find-node-modules": ["find-node-modules@2.1.3", "", { "dependencies": { "findup-sync": "^4.0.0", "merge": "^2.1.1" } }, "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "findup-sync": ["findup-sync@4.0.0", "", { "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^4.0.2", "resolve-dir": "^1.0.1" } }, "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ=="], + + "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "foreground-child": ["foreground-child@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^3.0.2" } }, "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "formidable": ["formidable@2.1.5", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0", "qs": "^6.11.0" } }, "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fromentries": ["fromentries@1.3.2", "", {}, "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg=="], + + "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], + + "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=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "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-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "git-raw-commits": ["git-raw-commits@2.0.11", "", { "dependencies": { "dargs": "^7.0.0", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", "through2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A=="], + + "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@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "global-directory": ["global-directory@5.0.0", "", { "dependencies": { "ini": "6.0.0" } }, "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w=="], + + "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], + + "global-modules": ["global-modules@1.0.0", "", { "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" } }, "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg=="], + + "global-prefix": ["global-prefix@1.0.2", "", { "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasha": ["hasha@5.2.2", "", { "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" } }, "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "husky": ["husky@8.0.3", "", { "bin": { "husky": "lib/bin.js" } }, "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "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=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "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-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], + + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-hook": ["istanbul-lib-hook@3.0.0", "", { "dependencies": { "append-transform": "^2.0.0" } }, "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@4.0.3", "", { "dependencies": { "@babel/core": "^7.7.5", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" } }, "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ=="], + + "istanbul-lib-processinfo": ["istanbul-lib-processinfo@2.0.3", "", { "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", "uuid": "^8.3.2" } }, "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.1.0", "", {}, "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "kareem": ["kareem@2.5.1", "", {}, "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@13.3.0", "", { "dependencies": { "chalk": "5.3.0", "commander": "11.0.0", "debug": "4.3.4", "execa": "7.2.0", "lilconfig": "2.1.0", "listr2": "6.6.1", "micromatch": "4.0.5", "pidtree": "0.6.0", "string-argv": "0.3.2", "yaml": "2.3.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ=="], + + "listr2": ["listr2@6.6.1", "", { "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^5.0.1", "rfdc": "^1.3.0", "wrap-ansi": "^8.1.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg=="], + + "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.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.flattendeep": ["lodash.flattendeep@4.4.0", "", {}, "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ=="], + + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + + "lodash.isfunction": ["lodash.isfunction@3.0.9", "", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], + + "lodash.map": ["lodash.map@4.6.0", "", {}, "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="], + + "longest": ["longest@2.0.1", "", {}, "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + + "meow": ["meow@8.1.2", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q=="], + + "merge": ["merge@2.1.1", "", {}, "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.7", "", {}, "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="], + + "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], + + "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=="], + + "mocha-steps": ["mocha-steps@1.3.0", "", {}, "sha512-KZvpMJTqzLZw3mOb+EEuYi4YZS41C9iTnb7skVFRxHjUd1OYbl64tCMSmpdIRM9LnwIrSOaRfPtNpF5msgv6Eg=="], + + "mongodb": ["mongodb@5.9.2", "", { "dependencies": { "bson": "^5.5.0", "mongodb-connection-string-url": "^2.6.0", "socks": "^2.7.1" }, "optionalDependencies": { "@mongodb-js/saslprep": "^1.1.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.0.0", "kerberos": "^1.0.0 || ^2.0.0", "mongodb-client-encryption": ">=2.3.0 <3", "snappy": "^7.2.2" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "kerberos", "mongodb-client-encryption", "snappy"] }, "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ=="], + + "mongodb-connection-string-url": ["mongodb-connection-string-url@2.6.0", "", { "dependencies": { "@types/whatwg-url": "^8.2.1", "whatwg-url": "^11.0.0" } }, "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ=="], + + "mongoose": ["mongoose@7.8.9", "", { "dependencies": { "bson": "^5.5.0", "kareem": "2.5.1", "mongodb": "5.9.2", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "16.0.1" } }, "sha512-V3GBAJbmOAdzEP8murOvlg7q1szlbe4jTBRyW+JBHRduJBe7F9dk5eyqJDTuYrdBcOOWfLbr6AgXrDK7F0/o5A=="], + + "mongoose-to-swagger": ["mongoose-to-swagger@1.5.1", "", {}, "sha512-gwEr0NhA+DyGswtgypIoV/iR0RGX4jY/SE9zmYYk6qpG2C83urcQfYJWzm7PsDPo3snMm8XjBYiAtLyYAqirXA=="], + + "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], + + "mquery": ["mquery@5.0.0", "", { "dependencies": { "debug": "4.x" } }, "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-preload": ["node-preload@0.2.1", "", { "dependencies": { "process-on-spawn": "^1.0.0" } }, "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ=="], + + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="], + + "nodemon": ["nodemon@2.0.22", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^3.2.7", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^5.7.1", "simple-update-notifier": "^1.0.7", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ=="], + + "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nyc": ["nyc@15.1.0", "", { "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "caching-transform": "^4.0.0", "convert-source-map": "^1.7.0", "decamelize": "^1.2.0", "find-cache-dir": "^3.2.0", "find-up": "^4.1.0", "foreground-child": "^2.0.0", "get-package-type": "^0.1.0", "glob": "^7.1.6", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-hook": "^3.0.0", "istanbul-lib-instrument": "^4.0.0", "istanbul-lib-processinfo": "^2.0.2", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.0.2", "make-dir": "^3.0.0", "node-preload": "^0.2.1", "p-map": "^3.0.0", "process-on-spawn": "^1.0.0", "resolve-from": "^5.0.0", "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "spawn-wrap": "^2.0.0", "test-exclude": "^6.0.0", "yargs": "^15.0.2" }, "bin": { "nyc": "bin/nyc.js" } }, "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@3.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-hash": ["package-hash@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.15", "hasha": "^5.0.0", "lodash.flattendeep": "^4.4.0", "release-zalgo": "^1.0.0" } }, "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-imports-exports": ["parse-imports-exports@0.2.4", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + + "parse-statements": ["parse-statements@1.0.11", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "process-on-spawn": ["process-on-spawn@1.1.0", "", { "dependencies": { "fromentries": "^1.2.0" } }, "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], + + "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=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], + + "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], + + "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=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "release-zalgo": ["release-zalgo@1.0.0", "", { "dependencies": { "es6-error": "^4.0.1" } }, "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "sift": ["sift@16.0.1", "", {}, "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-update-notifier": ["simple-update-notifier@1.1.0", "", { "dependencies": { "semver": "~7.0.0" } }, "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.8", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + + "spawn-wrap": ["spawn-wrap@2.0.0", "", { "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", "make-dir": "^3.0.0", "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" } }, "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + + "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + + "superagent": ["superagent@8.1.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^2.1.2", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0", "semver": "^7.3.8" } }, "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA=="], + + "supertest": ["supertest@6.3.4", "", { "dependencies": { "methods": "^1.1.2", "superagent": "^8.1.2" } }, "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "swagger-jsdoc": ["swagger-jsdoc@6.2.8", "", { "dependencies": { "commander": "6.2.0", "doctrine": "3.0.0", "glob": "7.1.6", "lodash.mergewith": "^4.6.2", "swagger-parser": "^10.0.3", "yaml": "2.0.0-1" }, "bin": { "swagger-jsdoc": "bin/swagger-jsdoc.js" } }, "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ=="], + + "swagger-parser": ["swagger-parser@10.0.3", "", { "dependencies": { "@apidevtools/swagger-parser": "10.0.3" } }, "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg=="], + + "swagger-ui-dist": ["swagger-ui-dist@5.32.5", "", { "dependencies": { "@scarf/scarf": "=1.4.0" } }, "sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA=="], + + "swagger-ui-express": ["swagger-ui-express@4.6.3", "", { "dependencies": { "swagger-ui-dist": ">=4.11.0" }, "peerDependencies": { "express": ">=4.0.0 || >=5.0.0-beta" } }, "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "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=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "validator": ["validator@13.15.35", "", {}, "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "workerpool": ["workerpool@6.5.1", "", {}, "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA=="], + + "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=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yaml": ["yaml@2.3.1", "", {}, "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "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=="], + + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "z-schema": ["z-schema@5.0.6", "", { "dependencies": { "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^13.7.0" }, "optionalDependencies": { "commander": "^10.0.0" }, "bin": { "z-schema": "bin/z-schema" } }, "sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@commitlint/config-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "@commitlint/format/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@commitlint/is-ignored/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], + + "@commitlint/load/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@commitlint/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@es-joy/jsdoccomment/@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + + "@types/jsonwebtoken/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@types/nodemailer/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "bun-types/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "camelcase-keys/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "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=="], + + "commitizen/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "commitizen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "cz-conventional-changelog/@commitlint/load": ["@commitlint/load@20.5.3", "", { "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/execute-rule": "^20.0.0", "@commitlint/resolve-extends": "^20.5.3", "@commitlint/types": "^20.5.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ=="], + + "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "elysia-rate-limit/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-plugin-jsdoc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "git-raw-commits/lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "global-directory/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "hasha/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "inquirer/lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "inquirer/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=="], + + "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "istanbul-lib-source-maps/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "lint-staged/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], + + "lint-staged/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "lint-staged/execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "log-update/ansi-escapes": ["ansi-escapes@5.0.0", "", { "dependencies": { "type-fest": "^1.0.2" } }, "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA=="], + + "log-update/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], + + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "mocha/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "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=="], + + "mongodb-connection-string-url/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], + + "nodemon/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "nodemon/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "nodemon/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "nyc/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "nyc/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "nyc/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "simple-update-notifier/semver": ["semver@7.0.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "swagger-jsdoc/commander": ["commander@6.2.0", "", {}, "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="], + + "swagger-jsdoc/glob": ["glob@7.1.6", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="], + + "swagger-jsdoc/yaml": ["yaml@2.0.0-1", "", {}, "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="], + + "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "ts-node/diff": ["diff@4.0.4", "", {}, "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ=="], + + "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yargs-unparser/decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], + + "yargs-unparser/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], + + "z-schema/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@commitlint/format/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@commitlint/format/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@commitlint/load/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@commitlint/load/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@commitlint/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@commitlint/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], + + "@scalar/themes/@scalar/types/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/config-validator": ["@commitlint/config-validator@20.5.0", "", { "dependencies": { "@commitlint/types": "^20.5.0", "ajv": "^8.11.0" } }, "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/execute-rule": ["@commitlint/execute-rule@20.0.0", "", {}, "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/resolve-extends": ["@commitlint/resolve-extends@20.5.3", "", { "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/types": "^20.5.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "import-meta-resolve": "^4.0.0", "resolve-from": "^5.0.0" } }, "sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/types": ["@commitlint/types@20.5.0", "", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA=="], + + "cz-conventional-changelog/@commitlint/load/cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], + + "cz-conventional-changelog/@commitlint/load/cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="], + + "elysia-rate-limit/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "eslint-plugin-jsdoc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "eslint/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "inquirer/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "inquirer/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "istanbul-lib-source-maps/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "lint-staged/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "lint-staged/execa/human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="], + + "lint-staged/execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "lint-staged/execa/npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "lint-staged/execa/onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "lint-staged/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "log-update/ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "log-update/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "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=="], + + "mongodb-connection-string-url/whatwg-url/tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + + "mongodb-connection-string-url/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "nodemon/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "nyc/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "nyc/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "nyc/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "nyc/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + + "ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@commitlint/format/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@commitlint/load/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@commitlint/types/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/config-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/types/conventional-commits-parser": ["conventional-commits-parser@6.4.0", "", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0", "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw=="], + + "eslint/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "inquirer/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "inquirer/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "lint-staged/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "lint-staged/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "log-symbols/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "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=="], + + "nyc/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "nyc/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "nyc/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "ora/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@commitlint/format/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@commitlint/load/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@commitlint/types/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "cz-conventional-changelog/@commitlint/load/@commitlint/types/conventional-commits-parser/meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], + + "eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "inquirer/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "inquirer/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "mocha/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "nyc/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "nyc/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "mocha/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "nyc/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "mocha/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "nyc/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/commitlint.config.js b/commitlint.config.cjs similarity index 100% rename from commitlint.config.js rename to commitlint.config.cjs diff --git a/export_openapi.js b/export_openapi.js deleted file mode 100644 index 7267eb3..0000000 --- a/export_openapi.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; - -// Import config -const { - runLoader, -} = require("./src/config"); - -// Load config -runLoader(); - -// Import constant -const constant = require("./src/init/const"); - -// Import modules -const {writeFileSync} = require("node:fs"); - -const {useApiDoc} = require("./src/init/api_doc"); - -// Get the API documentation -const apiDoc = useApiDoc(); -const apiDocJson = JSON.stringify(apiDoc); - -// Write the JSON file -try { - const {OPENAPI_EXPORTED_FILENAME: filename} = constant; - writeFileSync(filename, apiDocJson, { - encoding: "utf-8", - }); - console.info(`The documentation has been saved into "${filename}".`); -} catch (error) { - console.error(error); -} diff --git a/package.json b/package.json index 915b50d..0ffd589 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,29 @@ "description": "A passwordless authentication system.", "author": "Taiwan Web Technology Promotion Organization", "license": "MIT", - "main": "app.js", "scripts": { "export-openapi": "node export_openapi.js", - "dev": "nodemon app.js", - "start": "node app.js", - "lint": "npx lint-staged", - "lint:es": "eslint \"*.js\" \"src/**/*.js\"", + "dev": "bun --hot src/index.ts", + "start": "bun src/index.ts", + "lint": "eslint src test --ext .ts", "lint:es:fix": "eslint \"*.js\" \"src/**/*.js\" --fix", - "test": "mocha test --exit --recursive --timeout 5000", + "test": "bun test", "cover": "nyc mocha test --recursive --timeout 5000 --exit", "prepare": "husky install" }, "lint-staged": { - "*.js": "eslint" + "*.{js,ts}": "eslint" }, "dependencies": { + "@elysiajs/cors": "^1.4.2", + "@elysiajs/swagger": "^1.3.1", "@simplewebauthn/server": "^11.0.0", "@simplewebauthn/types": "^11.0.0", + "@sinclair/typebox": "^0.34.49", "cors": "^2.8.5", "dotenv": "^16.0.3", - "express": "^4.18.2", - "express-validator": "^6.14.3", + "elysia": "^1.4.28", + "elysia-rate-limit": "^4.6.1", "http-status-codes": "^2.2.0", "jsonwebtoken": "^9.0.0", "mongoose": "^7.0.3", @@ -40,6 +41,13 @@ "devDependencies": { "@commitlint/cli": "^17.4.4", "@commitlint/config-conventional": "^17.4.4", + "@types/bun": "^1.3.13", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^20.0.0", + "@types/nodemailer": "^8.0.0", + "@types/ua-parser-js": "^0.7.39", + "@typescript-eslint/eslint-plugin": "^8.59.3", + "@typescript-eslint/parser": "^8.59.3", "cz-conventional-changelog": "^3.3.0", "eslint": "^8.17.0", "eslint-config-google": "^0.14.0", diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 73f80b1..0000000 --- a/src/config.js +++ /dev/null @@ -1,126 +0,0 @@ -"use strict"; - -// Import modules -const {join: pathJoin} = require("node:path"); -const {existsSync} = require("node:fs"); - -/** - * Load configs from system environment variables. - */ -function runLoader() { - const dotenvPath = pathJoin(__dirname, "..", ".env"); - - const isDotEnvFileExists = existsSync(dotenvPath); - const isCustomDefined = get("APP_CONFIGURED") === "1"; - - if (!isDotEnvFileExists && !isCustomDefined) { - console.error( - "No '.env' file detected in app root.", - "If you're not using dotenv file,", - "set 'APP_CONFIGURED=1' into environment variables.", - "\n", - ); - throw new Error(".env not exists"); - } - - require("dotenv").config(); -} - -/** - * Check is production mode. - * @module config - * @function - * @return {boolean} true if production - */ -function isProduction() { - return getMust("NODE_ENV") === "production"; -} - -/** - * Get environment overview. - * @module config - * @function - * @return {object} - */ -function getEnvironmentOverview() { - return { - node: getFallback("NODE_ENV", "development"), - runtime: getFallback("RUNTIME_ENV", "native"), - }; -} - -/** - * Shortcut to get config value. - * @module config - * @function - * @param {string} key the key - * @return {string} the value - */ -function get(key) { - return process.env[key]; -} - -/** - * Get the bool value from config, if yes, returns true. - * @module config - * @function - * @param {string} key the key - * @return {bool} the bool value - */ -function getEnabled(key) { - return getMust(key) === "yes"; -} - -/** - * Get the array value from config. - * @module config - * @function - * @param {string} key the key - * @param {string} [separator=,] the separator. - * @return {array} the array value - */ -function getSplited(key, separator=",") { - return getMust(key). - split(separator). - filter((s) => s). - map((s) => s.trim()); -} - -/** - * Get the value from config with error thrown. - * @module config - * @function - * @param {string} key the key - * @return {string} the expected value - * @throws {Error} if value is undefined, throw an error - */ -function getMust(key) { - const value = get(key); - if (value === undefined) { - throw new Error(`config key ${key} is undefined`); - } - return value; -} - -/** - * Get the value from config with fallback. - * @module config - * @function - * @param {string} key the key - * @param {string} fallback the fallback value - * @return {string} the expected value - */ -function getFallback(key, fallback) { - return get(key) || fallback; -} - -module.exports = { - runLoader, - isProduction, - getEnvironmentOverview, - get, - getEnabled, - getSplited, - getMust, - getFallback, -}; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..83c6b6b --- /dev/null +++ b/src/config.ts @@ -0,0 +1,99 @@ +import {join as pathJoin} from "node:path"; +import {existsSync} from "node:fs"; +import dotenv from "dotenv"; + +/** + * Load configs from system environment variables. + */ +export function runLoader(): void { + // In Bun, import.meta.dir is the equivalent of __dirname + const dotenvPath = pathJoin(import.meta.dir, "..", ".env"); + + const isDotEnvFileExists = existsSync(dotenvPath); + const isCustomDefined = get("APP_CONFIGURED") === "1"; + + if (!isDotEnvFileExists && !isCustomDefined) { + console.error( + "No '.env' file detected in app root.", + "If you're not using dotenv file,", + "set 'APP_CONFIGURED=1' into environment variables.", + "\n", + ); + throw new Error(".env not exists"); + } + + dotenv.config(); +} + +/** + * Check is production mode. + * @return {boolean} Is production mode. + */ +export function isProduction(): boolean { + return getFallback("NODE_ENV", "development") === "production"; +} + +/** + * Get environment overview. + * @return {{node: string, runtime: string}} Environment overview. + */ +export function getEnvironmentOverview(): { node: string; runtime: string } { + return { + node: getFallback("NODE_ENV", "development"), + runtime: getFallback("RUNTIME_ENV", "bun"), + }; +} + +/** + * Shortcut to get config value. + * @param {string} key Config key. + * @return {string|undefined} Config value. + */ +export function get(key: string): string | undefined { + return process.env[key]; +} + +/** + * Get the bool value from config, if yes, returns true. + * @param {string} key Config key. + * @return {boolean} Config value. + */ +export function getEnabled(key: string): boolean { + return getMust(key) === "yes"; +} + +/** + * Get the array value from config. + * @param {string} key Config key. + * @param {string} [separator=","] Separator. + * @return {string[]} Config values. + */ +export function getSplited(key: string, separator: string = ","): string[] { + return getMust(key) + .split(separator) + .filter((s) => !!s) + .map((s) => s.trim()); +} + +/** + * Get the value from config with error thrown. + * @param {string} key Config key. + * @return {string} Config value. + */ +export function getMust(key: string): string { + const value = get(key); + if (value === undefined) { + throw new Error(`config key ${key} is undefined`); + } + return value; +} + +/** + * Get the value from config with fallback. + * @param {string} key Config key. + * @param {string} fallback Fallback value. + * @return {string} Config value. + */ +export function getFallback(key: string, fallback: string): string { + return get(key) || fallback; +} diff --git a/src/execute.js b/src/execute.js deleted file mode 100644 index 8cb3cfb..0000000 --- a/src/execute.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; - -// Import config -const {getMust, getSplited} = require("./config"); - -// Import modules -const fs = require("node:fs"); -const http = require("node:http"); -const https = require("node:https"); - -/** - * Setup protocol - http - * @param {object} app - * @param {function} callback - */ -function setupHttpProtocol(app, callback) { - const options = {}; - const httpServer = http.createServer(options, app); - const port = parseInt(getMust("HTTP_PORT")); - httpServer.listen(port, getMust("HTTP_HOSTNAME")); - callback({protocol: "http", hostname: getMust("HTTP_HOSTNAME"), port}); -} - -/** - * Setup protocol - https - * @param {object} app - * @param {function} callback - */ -function setupHttpsProtocol(app, callback) { - const options = { - key: fs.readFileSync(getMust("HTTPS_KEY_PATH")), - cert: fs.readFileSync(getMust("HTTPS_CERT_PATH")), - }; - const httpsServer = https.createServer(options, app); - const port = parseInt(getMust("HTTPS_PORT")); - httpsServer.listen(port, getMust("HTTPS_HOSTNAME")); - callback({protocol: "https", hostname: getMust("HTTPS_HOSTNAME"), port}); -} - -// Prepare application and detect protocols automatically -module.exports = async function(app, prepareHandlers, callback) { - // Waiting for prepare handlers - if (prepareHandlers.length > 0) { - const preparingPromises = prepareHandlers.map((c) => c()); - await Promise.all(preparingPromises); - } - - // Get enabled protocols - const enabledProtocols = getSplited("ENABLED_PROTOCOLS"); - - // Setup HTTP - if (enabledProtocols.includes("http")) { - setupHttpProtocol(app, callback); - } - - // Setup HTTPS - if (enabledProtocols.includes("https")) { - setupHttpsProtocol(app, callback); - } -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d9ca51b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,73 @@ +import {Elysia} from "elysia"; +import {cors} from "@elysiajs/cors"; +import {swagger} from "@elysiajs/swagger"; +import { + runLoader, + getMust, + getFallback, + getEnvironmentOverview, +} from "./config"; +import {APP_NAME, APP_DESCRIPTION, APP_VERSION} from "./init/const"; +import {prepare as prepareDatabase} from "./init/database"; +import {tokensRoutes} from "./routes/tokens"; +import {usersRoutes} from "./routes/users"; +import {adminRoutes} from "./routes/admin"; + +// Load config +runLoader(); + +// Initialize application +export const app = new Elysia() + .use(cors()) + .use(swagger({ + path: "/swagger", + documentation: { + info: { + title: APP_NAME, + description: APP_DESCRIPTION, + version: APP_VERSION, + }, + }, + })) + .use(tokensRoutes) + .use(usersRoutes) + .use(adminRoutes) + .get("/", ({set}) => { + const redirectCode = getMust("INDEX_REDIRECT_TYPE") === "permanent" ? + 301 : 302; + const redirectUrl = getMust("INDEX_REDIRECT_URL"); + + set.status = redirectCode; + set.redirect = redirectUrl; + }) + .get("/robots.txt", ({set}) => { + set.headers["content-type"] = "text/plain"; + return "User-agent: *\nDisallow: /"; + }); + +// Start server +const start = async () => { + try { + // Initialize database + await prepareDatabase(); + console.info("Database connected"); + + const port = parseInt(getFallback("HTTP_PORT", "8080")); + const hostname = getFallback("HTTP_HOSTNAME", "0.0.0.0"); + + app.listen({port, hostname}, ({hostname: h, port: p}) => { + const {node, runtime} = getEnvironmentOverview(); + console.info("===="); + console.info(`${APP_NAME} (environment: ${node}, ${runtime})`); + console.info(`Server is listening at http://${h}:${p}`); + console.info("===="); + }); + } catch (error) { + console.error("Failed to start server:", error); + process.exit(1); + } +}; + +if (import.meta.main) { + start(); +} diff --git a/src/init/api_doc.js b/src/init/api_doc.js deleted file mode 100644 index a5d7ff0..0000000 --- a/src/init/api_doc.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -// swagger is an api documentation generator (OpenAPI spec.) - -// Import modules -const {getMust} = require("../config"); - -const { - APP_NAME, - APP_DESCRIPTION, - APP_VERSION, - APP_AUTHOR_NAME, - APP_AUTHOR_URL, -} = require("./const"); - -const {join: pathJoin} = require("node:path"); - -const swaggerJSDoc = require("swagger-jsdoc"); -const toDocSchema = require("mongoose-to-swagger"); - -const {routerFiles} = require("../routes"); -const {modelFiles} = require("../models"); - -const routerFilePathPrefix = pathJoin(__dirname, "..", "routes"); -const modelFilePathPrefix = pathJoin(__dirname, "..", "models"); - -// Config options -const options = { - definition: { - openapi: "3.0.0", - info: { - title: APP_NAME, - version: APP_VERSION, - description: APP_DESCRIPTION, - contact: { - name: APP_AUTHOR_NAME, - url: APP_AUTHOR_URL, - }, - }, - servers: [{ - description: getMust("SWAGGER_SERVER_DESCRIPTION"), - url: getMust("SWAGGER_SERVER_URL"), - }], - components: { - securitySchemes: { - XaraToken: { - type: "apiKey", - in: "header", - name: "Authorization", - }, - }, - schemas: Object.fromEntries(modelFiles.map( - (f) => { - const filename = pathJoin(modelFilePathPrefix, f); - const model = require(filename); - const schema = toDocSchema(model); - return [schema.title, schema]; - }, - )), - }, - }, - apis: routerFiles.map( - (f) => pathJoin(routerFilePathPrefix, f), - ), -}; - -// Export as useFunction -exports.useApiDoc = () => swaggerJSDoc(options); diff --git a/src/init/cache.js b/src/init/cache.js deleted file mode 100644 index 1767f98..0000000 --- a/src/init/cache.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -// node-cache is an in-memory cache. - -// Import node-cache -const NodeCache = require("node-cache"); - -// Initialize node-cache -const cache = new NodeCache({stdTTL: 100}); - -// Export as useFunction -exports.useCache = () => cache; diff --git a/src/init/cache.ts b/src/init/cache.ts new file mode 100644 index 0000000..a4dab65 --- /dev/null +++ b/src/init/cache.ts @@ -0,0 +1,6 @@ +import NodeCache from "node-cache"; + +const cache = new NodeCache({stdTTL: 100}); + +export const useCache = () => cache; +export default cache; diff --git a/src/init/const.js b/src/init/const.js deleted file mode 100644 index 336fe32..0000000 --- a/src/init/const.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -// Constants - -exports.APP_NAME = "Sara Hoshikawa"; -exports.APP_DESCRIPTION = "無密碼式身分認證系統。A passwordless authentication system."; - -exports.APP_VERSION = "latest"; - -exports.APP_AUTHOR_NAME = "Taiwan Web Technology Promotion Organization"; -exports.APP_AUTHOR_URL = "https://web-tech.tw"; - -exports.HEADER_REFRESH_TOKEN = "x-sara-refresh"; - -exports.SESSION_TYPE_CREATE_USER = "create_user"; -exports.SESSION_TYPE_CREATE_TOKEN = "create_token"; -exports.SESSION_TYPE_CREATE_PASSKEY = "create_passkey"; -exports.SESSION_TYPE_UPDATE_EMAIL = "update_email"; - -exports.OPENAPI_EXPORTED_FILENAME = "openapi_exported.json"; -exports.PUBLIC_KEY_FILENAME = "keypair_public.pem"; -exports.PRIVATE_KEY_FILENAME = "keypair_private.pem"; - -exports.TEST_EMAIL_DOMAIN = "web-tech-tw.github.io"; diff --git a/src/init/const.ts b/src/init/const.ts new file mode 100644 index 0000000..e1de3cf --- /dev/null +++ b/src/init/const.ts @@ -0,0 +1,23 @@ +// Constants + +export const APP_NAME = "Sara Hoshikawa"; +export const APP_DESCRIPTION = + "無密碼式身分認證系統。A passwordless authentication system."; + +export const APP_VERSION = "latest"; + +export const APP_AUTHOR_NAME = "Taiwan Web Technology Promotion Organization"; +export const APP_AUTHOR_URL = "https://web-tech.tw"; + +export const HEADER_REFRESH_TOKEN = "x-sara-refresh"; + +export const SESSION_TYPE_CREATE_USER = "create_user"; +export const SESSION_TYPE_CREATE_TOKEN = "create_token"; +export const SESSION_TYPE_CREATE_PASSKEY = "create_passkey"; +export const SESSION_TYPE_UPDATE_EMAIL = "update_email"; + +export const OPENAPI_EXPORTED_FILENAME = "openapi_exported.json"; +export const PUBLIC_KEY_FILENAME = "keypair_public.pem"; +export const PRIVATE_KEY_FILENAME = "keypair_private.pem"; + +export const TEST_EMAIL_DOMAIN = "web-tech-tw.github.io"; diff --git a/src/init/database.js b/src/init/database.ts similarity index 53% rename from src/init/database.js rename to src/init/database.ts index 4ef6494..f718c75 100644 --- a/src/init/database.js +++ b/src/init/database.ts @@ -1,18 +1,18 @@ -"use strict"; // mongoose is an ODM library for MongoDB. // Import config -const {getMust} = require("../config"); +import {getMust} from "../config"; // Import mongoose -const database = require("mongoose"); +import database from "mongoose"; -// Configure mongose +// Configure mongoose database.set("strictQuery", true); // Connect to MongoDB -exports.prepare = () => +export const prepare = () => database.connect(getMust("MONGODB_URI")); // Export as useFunction -exports.useDatabase = () => database; +export const useDatabase = () => database; +export default database; diff --git a/src/init/express.js b/src/init/express.js deleted file mode 100644 index 703efd3..0000000 --- a/src/init/express.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -// express.js is a web framework. - -// Import config -const {getSplited, getEnabled} = require("../config"); - -// Import express.js -const express = require("express"); - -// Create middleware handlers -const middlewareAuth = require("../middleware/auth"); - -// Initialize app engine -const app = express(); - -// Register global middleware -app.use(middlewareAuth); - -// Read config -const trustProxy = getSplited("TRUST_PROXY", ","); - -const isEnabledRedirectHttpHttps = getEnabled("ENABLED_REDIRECT_HTTP_HTTPS"); -const isEnabledCors = getEnabled("ENABLED_CORS"); -const isEnabledCorsOriginCheck = getEnabled("ENABLED_CORS_ORIGIN_CHECK"); - -// Optional settings -if (trustProxy.length) { - app.set("trust proxy", trustProxy); -} - -// Optional middleware -if (isEnabledRedirectHttpHttps) { - const middlewareHttpsRedirect = require("../middleware/https_redirect"); - // Do https redirects - app.use(middlewareHttpsRedirect); -} -if (isEnabledCors) { - const middlewareCORS = require("../middleware/cors"); - // Do CORS handles - app.use(middlewareCORS); -} -if (isEnabledCors && isEnabledCorsOriginCheck) { - const middlewareOrigin = require("../middleware/origin"); - // Check header "Origin" for CORS - app.use(middlewareOrigin); -} - -// Export useFunction -exports.useApp = () => app; - -// Export withAwait -exports.withAwait = (fn) => (req, res, next) => - Promise.resolve(fn(req, res, next)).catch(next); - -// Export express for shortcut -exports.express = express; diff --git a/src/init/keypair.js b/src/init/keypair.js deleted file mode 100644 index 0c1c95c..0000000 --- a/src/init/keypair.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -// Reading curve keypair. - -// Import fs -const {readFileSync} = require("node:fs"); - -// Import constant -const constants = require("./const"); - -// Export as useFunction -exports.usePublicKey = () => - readFileSync(constants.PUBLIC_KEY_FILENAME); -exports.usePrivateKey = () => - readFileSync(constants.PRIVATE_KEY_FILENAME); diff --git a/src/init/keypair.ts b/src/init/keypair.ts new file mode 100644 index 0000000..93e9a03 --- /dev/null +++ b/src/init/keypair.ts @@ -0,0 +1,9 @@ +// Reading curve keypair. +import {readFileSync} from "node:fs"; +import * as constants from "./const"; + +export const usePublicKey = () => + readFileSync(constants.PUBLIC_KEY_FILENAME); + +export const usePrivateKey = () => + readFileSync(constants.PRIVATE_KEY_FILENAME); diff --git a/src/middleware/access.js b/src/middleware/access.js deleted file mode 100644 index 8da1065..0000000 --- a/src/middleware/access.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -// Check the role for the request required, -// and interrupt if the requirement is not satisfied. -// (for Sara only) - -// Import isProduction -const {isProduction} = require("../config"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Export (function) -// requiredRole can be string or null, -// set as string, it will find the role whether satisfied, -// set as null, will check the user whether login only. -module.exports = (requiredRole) => (req, res, next) => { - if (!isProduction()) { - // Debug message - console.warn( - "An access required request detected:", - `role "${requiredRole}"`, - req.auth, - "\n", - ); - } - - // Check auth exists - if (!(req.auth && req.auth.id)) { - res.sendStatus(StatusCodes.UNAUTHORIZED); - return; - } - - // Accept XARA or TEST only - if ( - req.auth.method !== "XARA" && - !(req.auth.method === "TEST" && !isProduction()) - ) { - res.sendStatus(StatusCodes.METHOD_NOT_ALLOWED); - return; - } - - // Read roles from metadata - const userRoles = req.auth.metadata?.profile?.roles; - const isUserRolesValid = Array.isArray(userRoles); - - // Check permission - if ( - requiredRole && - (!isUserRolesValid || !userRoles.includes(requiredRole)) - ) { - if (!isProduction()) { - const displayUserRoles = isUserRolesValid ? - userRoles.join(", ") : - userRoles; - // Debug message - console.warn( - "An access required request forbidden:", - `actual "${displayUserRoles}"`, - `expected "${requiredRole}"`, - "\n", - ); - } - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Call next middleware - next(); -}; diff --git a/src/middleware/auth.js b/src/middleware/auth.js deleted file mode 100644 index 04c3b5f..0000000 --- a/src/middleware/auth.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -// Validate "Authorization" header, but it will not interrupt the request. - -// To interrupt the request which without the request, -// please use "access.js" middleware. - -// Import isProduction -const {isProduction} = require("../config"); - -// Import isObjectPropExists -const {isObjectPropExists} = require("../utils/native"); - -const xaraTokenAuth = require("../utils/xara_token"); -const testTokenAuth = require("../utils/test_token"); - -// Import authMethods -const authMethods = { - "XARA": xaraTokenAuth.validate, - "TEST": testTokenAuth.validate, -}; - -// Export (function) -module.exports = async (req, _, next) => { - const authCode = req.header("authorization"); - if (!authCode) { - next(); - return; - } - - const params = authCode.split(" "); - if (params.length !== 2) { - next(); - return; - } - const [method, secret] = params; - - req.auth = { - id: null, - metadata: null, - method, - secret, - }; - if (!isObjectPropExists(authMethods, req.auth.method)) { - next(); - return; - } - - const authMethod = authMethods[method]; - const authResult = await authMethod(secret); - - if (!isProduction()) { - // Debug message - console.warn( - "An authentication detected:", - authMethod, - authResult, - "\n", - ); - } - - const { - userId, - payload, - isAborted, - } = authResult; - if (isAborted) { - next(); - return; - } - - req.auth.id = userId; - req.auth.metadata = payload; - next(); -}; diff --git a/src/middleware/cors.js b/src/middleware/cors.js deleted file mode 100644 index 3d339b6..0000000 --- a/src/middleware/cors.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; -// Cross-Origin Resource Sharing - -// Import config -const {getEnabled, getMust} = require("../config"); - -// Import cors -const cors = require("cors"); - -// Import const -const { - HEADER_REFRESH_TOKEN: headerRefreshToken, -} = require("../init/const"); - -// Read config -const corsOrigin = getMust("CORS_ORIGIN"); -const swaggerCorsOrigin = getMust("SWAGGER_CORS_ORIGIN"); - -// Export (function) -module.exports = cors({ - origin: getEnabled("ENABLED_SWAGGER") ? - [corsOrigin, swaggerCorsOrigin]: - corsOrigin, - exposedHeaders: [headerRefreshToken], -}); diff --git a/src/middleware/https_redirect.js b/src/middleware/https_redirect.js deleted file mode 100644 index f0f3d76..0000000 --- a/src/middleware/https_redirect.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -// Redirect http to https. - -// Import isProduction -const {isProduction} = require("../config"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Export (function) -module.exports = (req, res, next) => { - if (req.protocol === "http") { - if (!isProduction()) { - // Debug message - console.warn( - "Pure HTTP protocol detected:", - `from "${req.hostname}"`, - `with host header "${req.headers.host}"`, - `with origin header "${req.headers.origin}"`, - ); - } - res.redirect(StatusCodes.MOVED_PERMANENTLY, `https://${req.headers.host}${req.url}`); - } - - // Call next middleware - next(); -}; diff --git a/src/middleware/inspector.js b/src/middleware/inspector.js deleted file mode 100644 index 2438735..0000000 --- a/src/middleware/inspector.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -// Interrupt the request -// which is not satisfied the result from express-validator. - -// Import isProduction -const {isProduction} = require("../config"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Import validatorResult -const {validationResult} = require("express-validator"); - -// Export (function) -module.exports = (req, res, next) => { - const errors = validationResult(req); - if (errors.isEmpty()) { - next(); - } else { - if (!isProduction()) { - // Debug message - console.warn( - "A bad request received:", - errors, - ); - } - res. - status(StatusCodes.BAD_REQUEST). - json({errors: errors.array()}); - } -}; diff --git a/src/middleware/origin.js b/src/middleware/origin.js deleted file mode 100644 index 0fb2499..0000000 --- a/src/middleware/origin.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -// Check the header "Origin" in request is equal to CORS_ORIGIN, -// if not, interrupt it. - -// Import config -const {isProduction, getMust, getEnabled} = require("../config"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Import isObjectPropExists -const {isObjectPropExists} = require("../utils/native"); - -// Export (function) -module.exports = (req, res, next) => { - // Check the request is CORS - if (!isObjectPropExists(req.headers, "origin")) { - if (!isProduction()) { - // Debug message - console.warn("CORS origin header is not detected"); - } - next(); - return; - } - - // Get URLs - const actualUrl = req.header("origin"); - const expectedUrl = getMust("CORS_ORIGIN"); - - // Origin match - if (actualUrl === expectedUrl) { - if (!isProduction()) { - // Debug message - console.warn( - "CORS origin header match:", - `actual "${actualUrl}"`, - `expected "${expectedUrl}"`, - ); - } - next(); - return; - } - - // Get URLs - const isEnabledSwagger = getEnabled("ENABLED_SWAGGER"); - const expectedSwaggerUrl = getMust("SWAGGER_CORS_ORIGIN"); - - // Origin from Swagger match - if (isEnabledSwagger && actualUrl === expectedSwaggerUrl) { - if (!isProduction()) { - // Debug message - console.warn( - "CORS origin header from Swagger match:", - `actual "${actualUrl}"`, - `expected "${expectedUrl}"`, - ); - } - next(); - return; - } - - // Origin mismatch - if (!isProduction()) { - // Debug message - console.warn( - "CORS origin header mismatch:", - `actual "${actualUrl}"`, - `expected "${expectedUrl}"`, - ); - } - res.sendStatus(StatusCodes.FORBIDDEN); -}; diff --git a/src/middleware/restrictor.js b/src/middleware/restrictor.js deleted file mode 100644 index 1abd89a..0000000 --- a/src/middleware/restrictor.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; -// The solution to defense from brute-force attacks, - -// Import isProduction -const {isProduction} = require("../config"); - -// Import StatusCodes -const {StatusCodes} = require("http-status-codes"); - -// Import useCache -const {useCache} = require("../init/cache"); - -// Import getIPAddress -const {getIPAddress} = require("../utils/visitor"); - -/** - * Get path key from request. - * @module restrictor - * @function - * @param {object} req the request - * @param {boolean} isParam is param mode - * @return {array} - */ -function getPathKey(req, isParam) { - const pathArray = req.originalUrl.split("/").filter((i) => !!i); - if (isParam) { - pathArray.pop(); - } - return pathArray.join("."); -} - -// Export (function) -// max is the maximum number of requests allowed every IP addresss. -// ttl is the seconds to unblock the IP address if there no request comes. -// if ttl set as 0, it will be blocked forever until the software restarted. -// isParam is the flag to remove the last path from the key. -// customForbiddenStatus is the custom status code -// for forbidden request, optional. -module.exports = (max, ttl, isParam, customForbiddenStatus=null) => - (req, res, next) => { - const pathKey = getPathKey(req, isParam); - const visitorKey = getIPAddress(req); - const queryKey = ["restrictor", pathKey, visitorKey].join(":"); - - const cache = useCache(); - - const keyValue = cache.get(queryKey); - - const increaseValue = () => { - const offset = keyValue ? keyValue + 1 : 1; - cache.set(queryKey, offset, ttl); - }; - - if (keyValue > max) { - if (!isProduction()) { - // Debug message - console.warn( - "Too many forbidden requests received:", - `actual "${keyValue}"`, - `expect "${max}"`, - ); - } - res.sendStatus(StatusCodes.TOO_MANY_REQUESTS); - increaseValue(); - return; - } - - let forbiddenStatus = StatusCodes.FORBIDDEN; - if (customForbiddenStatus) { - forbiddenStatus = customForbiddenStatus; - } - - res.on("finish", () => { - if (res.statusCode !== forbiddenStatus) { - return; - } - if (!isProduction()) { - // Debug message - console.warn( - "An forbidden request detected:", - forbiddenStatus, - queryKey, - ); - } - increaseValue(); - }); - - // Call next middleware - next(); - }; diff --git a/src/models/index.js b/src/models/index.js deleted file mode 100644 index 52a4854..0000000 --- a/src/models/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; - -// Models -exports.modelFiles = [ - "./user.js", -]; diff --git a/src/models/token.js b/src/models/token.js deleted file mode 100644 index f85c4c9..0000000 --- a/src/models/token.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; - -const {useDatabase} = require("../init/database"); -const database = useDatabase(); - -const schema = require("../schemas/token"); -module.exports = database.model("Token", schema); diff --git a/src/models/token.ts b/src/models/token.ts new file mode 100644 index 0000000..1d2677a --- /dev/null +++ b/src/models/token.ts @@ -0,0 +1,18 @@ +import mongoose, {Schema, Document, Model, Types} from "mongoose"; + +export interface IToken extends Document { + userId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const TokenSchema = new Schema({ + userId: {type: Schema.Types.ObjectId, required: true}, +}, { + timestamps: true, + expires: "1d", +}); + +export const Token: Model = mongoose.models.Token || + mongoose.model("Token", TokenSchema); +export default Token; diff --git a/src/models/user.js b/src/models/user.js deleted file mode 100644 index 48c5619..0000000 --- a/src/models/user.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; - -const {useDatabase} = require("../init/database"); -const database = useDatabase(); - -const schema = require("../schemas/user"); -module.exports = database.model("User", schema); diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..59db098 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,51 @@ +import mongoose, {Schema, Document, Model} from "mongoose"; + +export interface IPasskey { + id: string; + label: string; + publicKey: Buffer; + counter: number; + transports: string[]; + createdAt?: Date; + updatedAt?: Date; +} + +export interface IUser extends Document { + email: string; + nickname: string; + roles: string[]; + revision: number; + passkeys: IPasskey[]; + avatar_hash?: string; + createdAt: Date; + updatedAt: Date; +} + +const PasskeySchema = new Schema({ + id: {type: String, required: true}, + label: {type: String, required: true}, + publicKey: { + type: Buffer, + required: true, + set: (val: any) => Buffer.from(val), + }, + counter: {type: Number, required: true}, + transports: {type: [String], required: true}, +}, { + _id: false, + timestamps: true, +}); + +const UserSchema = new Schema({ + email: {type: String, required: true, unique: true}, + nickname: {type: String, required: true}, + roles: {type: [String], default: []}, + revision: {type: Number, default: 0}, + passkeys: {type: [PasskeySchema], default: []}, +}, { + timestamps: true, +}); + +export const User: Model = mongoose.models.User || + mongoose.model("User", UserSchema); +export default User; diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts new file mode 100644 index 0000000..fab70f9 --- /dev/null +++ b/src/plugins/auth.ts @@ -0,0 +1,77 @@ +import {Elysia} from "elysia"; +import * as xaraToken from "../utils/xara_token"; +import * as testTokenAuth from "../utils/test_token"; +import {isProduction} from "../config"; +import {isObjectPropExists} from "../utils/native"; + +const authMethods: Record Promise> = { + "XARA": xaraToken.validate, + "TEST": (token: string) => Promise.resolve(testTokenAuth.validate(token)), +}; + +export interface AuthContext { + id: string; + metadata: any; + method: string; + secret: string; +} + +export const authPlugin = new Elysia({name: "auth"}) + .derive({as: "global"}, async ({ + headers, + }): Promise<{ auth: AuthContext | null }> => { + const authHeader = headers["authorization"]; + if (!authHeader) return {auth: null}; + + const params = authHeader.split(" "); + if (params.length !== 2) return {auth: null}; + + const [method, secret] = params; + if (!isObjectPropExists(authMethods, method)) { + return {auth: null}; + } + + const validateFn = authMethods[method]; + const result = await validateFn(secret); + + if (!isProduction()) { + console.warn("An authentication detected:", method, result); + } + + if (result.isAborted) { + return {auth: null}; + } + + return { + auth: { + id: result.userId as string, + metadata: result.payload as any, + method, + secret, + }, + }; + }) + .macro(({onBeforeHandle}) => ({ + access(requiredRole: string | null) { + onBeforeHandle(({auth, status}: any) => { + if (!auth || !auth.id) { + return status(401); + } + + if ( + auth.method !== "XARA" && + !(auth.method === "TEST" && !isProduction()) + ) { + return status(405); + } + + const userRoles = auth.metadata?.profile?.roles; + const isUserRolesValid = Array.isArray(userRoles); + + if (requiredRole && + (!isUserRolesValid || !userRoles.includes(requiredRole))) { + return status(403); + } + }); + }, + })); diff --git a/src/plugins/restrictor.ts b/src/plugins/restrictor.ts new file mode 100644 index 0000000..f284115 --- /dev/null +++ b/src/plugins/restrictor.ts @@ -0,0 +1,89 @@ +import {useCache} from "../init/cache"; +import {getIPAddress} from "../utils/visitor"; +import {isProduction} from "../config"; + +const cache = useCache(); + +/** + * Get path key for restrictor. + * @param {string} urlStr - The URL string. + * @param {boolean} isParam - Whether the path has a parameter at the end. + * @return {string} The path key. + */ +function getPathKey(urlStr: string, isParam: boolean) { + const url = new URL(urlStr); + const pathArray = url.pathname.split("/").filter((i) => !!i); + if (isParam && pathArray.length > 0) { + pathArray.pop(); + } + return pathArray.join("."); +} + +/** + * Restrictor before handle. + * @param {number} max - The maximum allowed requests. + * @param {number} ttl - The time to live in seconds. + * @param {boolean} isParam - Whether the path has a parameter. + * @return {Function} The before handle function. + */ +export const restrictorBefore = ( + max: number, + ttl: number, + isParam: boolean = false, +) => + ({request, server, status}: any) => { + const pathKey = getPathKey(request.url, isParam); + const visitorKey = getIPAddress(request, server); + const queryKey = ["restrictor", pathKey, visitorKey].join(":"); + + const keyValue = (cache.get(queryKey) as number) || 0; + + if (keyValue > max) { + if (!isProduction()) { + console.warn( + "Too many forbidden requests received:", + `actual "${keyValue}"`, + `expect "${max}"`, + ); + } + cache.set(queryKey, keyValue + 1, ttl); + return status(429); + } + }; + +/** + * Restrictor after response. + * @param {number} ttl - The time to live in seconds. + * @param {boolean} isParam - Whether the path has a parameter. + * @param {number|null} customForbiddenStatus - Custom forbidden status. + * @return {Function} The after response function. + */ +export const restrictorAfter = ( + ttl: number, + isParam: boolean = false, + customForbiddenStatus: number | null = null, +) => + ({request, server, set}: any) => { + const forbiddenStatus = customForbiddenStatus || 403; + const currentStatus = typeof set.status === "string" ? + parseInt(set.status) : set.status; + + if (currentStatus !== forbiddenStatus) { + return; + } + + const pathKey = getPathKey(request.url, isParam); + const visitorKey = getIPAddress(request, server); + const queryKey = ["restrictor", pathKey, visitorKey].join(":"); + + const keyValue = (cache.get(queryKey) as number) || 0; + cache.set(queryKey, keyValue + 1, ttl); + + if (!isProduction()) { + console.warn( + "An forbidden request detected:", + forbiddenStatus, + queryKey, + ); + } + }; diff --git a/src/routes/admin.js b/src/routes/admin.js deleted file mode 100644 index ccddf5f..0000000 --- a/src/routes/admin.js +++ /dev/null @@ -1,213 +0,0 @@ -"use strict"; - -const {StatusCodes} = require("http-status-codes"); -const {useApp, withAwait, express} = require("../init/express"); - -const User = require("../models/user"); - -const middlewareAccess = require("../middleware/access"); -const middlewareInspector = require("../middleware/inspector"); -const middlewareValidator = require("express-validator"); - -// Create router -const {Router: newRouter} = express; -const router = newRouter(); - -router.use(express.json()); - -/** - * @openapi - * /admin/users/{user_id}: - * get: - * summary: Get user by ID - * description: Get user information by user ID. - * tags: - * - admin - * security: - * - XaraToken: [] - * parameters: - * - name: user_id - * in: path - * description: ID of the user to retrieve - * required: true - * schema: - * type: string - * format: objectId - * responses: - * 200: - * description: User information - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 404: - * description: User not found - */ -router.get("/users/:user_id", - middlewareAccess("admin"), - middlewareValidator.param("user_id").isMongoId().notEmpty(), - middlewareInspector, - withAwait(async (req, res) => { - // Assign shortcuts - const userId = req.params.user_id; - - // Check user exists by the ID - const user = await User.findById(userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle conversion - const userData = user.toObject(); - - // Send response - res.send(userData); - }), -); - -/** - * @openapi - * /admin/users/{user_id}/roles: - * post: - * summary: Add role to user - * description: Add a role to a user by user ID. - * tags: - * - admin - * security: - * - XaraToken: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * role_name: - * type: string - * parameters: - * - name: user_id - * in: path - * description: ID of the user to whom the role will be added - * required: true - * schema: - * type: string - * format: objectId - * responses: - * 201: - * description: Role added successfully - * 404: - * description: User not found - * 409: - * description: Role already exists for this user - */ -router.post("/users/:user_id/roles", - middlewareAccess("admin"), - middlewareValidator.param("user_id").isMongoId().notEmpty(), - middlewareValidator.body("role_name").isString().notEmpty(), - middlewareInspector, - withAwait(async (req, res) => { - // Assign shortcuts - const userId = req.params.user_id; - const roleName = req.body.role_name; - - // Check user exists by the ID - const user = await User.findById(userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Check values - if (!Array.isArray(user.roles)) { - user.roles = []; - } - if (user.roles.includes(roleName)) { - res.sendStatus(StatusCodes.CONFLICT); - return; - } - - // Update values - user.roles = [...user.roles, roleName]; - await user.save(); - - // Send response - res.sendStatus(StatusCodes.CREATED); - }), -); - -/** - * @openapi - * /admin/users/{user_id}/roles/{role_name}: - * delete: - * summary: Remove role from user - * description: Remove a role from a user by user ID. - * tags: - * - admin - * security: - * - XaraToken: [] - * parameters: - * - in: path - * name: user_id - * schema: - * type: string - * format: objectId - * required: true - * description: The user ID - * - in: path - * name: role_name - * schema: - * type: string - * required: true - * description: The role name - * responses: - * 204: - * description: Role removed successfully - * 404: - * description: User not found or role not found - * 410: - * description: Role does not exist for this user - */ -router.delete("/users/:user_id/roles/:role_name", - middlewareAccess("admin"), - middlewareValidator.param("user_id").isMongoId().notEmpty(), - middlewareValidator.param("role_name").isString().notEmpty(), - middlewareInspector, - withAwait(async (req, res) => { - // Assign shortcuts - const userId = req.params.user_id; - const roleName = req.body.role_name; - - // Check user exists by the ID - const user = await User.findById(userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Check values - if (!Array.isArray(user.roles)) { - user.roles = []; - } - if (!user.roles.includes(roleName)) { - res.sendStatus(StatusCodes.GONE); - return; - } - - // Update values - user.roles = user.roles.filter((name) => name !== roleName); - await user.save(); - - // Send response - res.sendStatus(StatusCodes.NO_CONTENT); - }), -); - -// Export routes mapper (function) -module.exports = () => { - // Use application - const app = useApp(); - - // Mount the router - app.use("/admin", router); -}; diff --git a/src/routes/admin.ts b/src/routes/admin.ts new file mode 100644 index 0000000..ccee06d --- /dev/null +++ b/src/routes/admin.ts @@ -0,0 +1,72 @@ +import {Elysia, t} from "elysia"; +import {authPlugin} from "../plugins/auth"; +import User from "../models/user"; + +/** + * Admin management routes + */ +export const adminRoutes = new Elysia({prefix: "/admin"}) + .use(authPlugin) + /** + * Get user by ID (Admin view) + */ + .get("/users/:user_id", async ({params, status}) => { + const user = await User.findById(params.user_id).exec(); + if (!user) return status(404); + + return user.toObject(); + }, { + access: "admin", + params: t.Object({user_id: t.String()}), + }) + /** + * Add role to user + */ + .post("/users/:user_id/roles", async ({params, body, set, status}) => { + const user = await User.findById(params.user_id).exec(); + if (!user) return status(404); + + if (!Array.isArray(user.roles)) { + user.roles = []; + } + if (user.roles.includes(body.role_name)) { + return status(409); + } + + user.roles = [...user.roles, body.role_name]; + await user.save(); + + set.status = 201; + return {message: "Role added"}; + }, { + access: "admin", + params: t.Object({user_id: t.String()}), + body: t.Object({role_name: t.String()}), + }) + /** + * Remove role from user + */ + .delete("/users/:user_id/roles/:role_name", async ({ + params, + set, + status, + }) => { + const user = await User.findById(params.user_id).exec(); + if (!user) return status(404); + + if (!Array.isArray(user.roles) || + !user.roles.includes(params.role_name)) { + return status(410); // GONE + } + + user.roles = user.roles.filter((name) => name !== params.role_name); + await user.save(); + + set.status = 204; + }, { + access: "admin", + params: t.Object({ + user_id: t.String(), + role_name: t.String(), + }), + }); diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index baf0df4..0000000 --- a/src/routes/index.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -// Routers -exports.routerFiles = [ - "./admin.js", - "./swagger.js", - "./tokens.js", - "./users.js", -]; - -// Load routes -exports.load = () => { - const routerMappers = exports.routerFiles.map((n) => require(n)); - routerMappers.forEach((c) => c()); -}; diff --git a/src/routes/swagger.js b/src/routes/swagger.js deleted file mode 100644 index 772e79b..0000000 --- a/src/routes/swagger.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -const {getEnabled} = require("../config"); -const {useApiDoc} = require("../init/api_doc"); - -const {useApp, express} = require("../init/express"); - -const swaggerUi = require("swagger-ui-express"); - -const {Router: newRouter} = express; -const router = newRouter(); - -const apiDoc = useApiDoc(); -router.use("/", swaggerUi.serve, swaggerUi.setup(apiDoc)); - -// Export routes mapper (function) -module.exports = () => { - // Skip if swagger disabled - if (!getEnabled("ENABLED_SWAGGER")) { - return; - } - - // Use application - const app = useApp(); - - // Mount the router - app.use("/swagger", router); -}; diff --git a/src/routes/tokens.js b/src/routes/tokens.js deleted file mode 100644 index 92bbe98..0000000 --- a/src/routes/tokens.js +++ /dev/null @@ -1,601 +0,0 @@ -"use strict"; - -const {getMust} = require("../config"); -const {StatusCodes} = require("http-status-codes"); -const {useApp, withAwait, express} = require("../init/express"); -const {useCache} = require("../init/cache"); - -const { - HEADER_REFRESH_TOKEN: headerRefreshToken, - SESSION_TYPE_CREATE_TOKEN: sessionTypeCreateToken, -} = require("../init/const"); - -const User = require("../models/user"); -const Token = require("../models/token"); - -const { - generateAuthenticationOptions, - verifyAuthenticationResponse, -} = require("@simplewebauthn/server"); - -const utilMailSender = require("../utils/mail_sender"); -const utilXaraToken = require("../utils/xara_token"); -const utilCodeSession = require("../utils/code_session"); -const utilPasskeySession = require("../utils/passkey_session"); -const utilVisitor = require("../utils/visitor"); -const utilNative = require("../utils/native"); - -const middlewareInspector = require("../middleware/inspector"); -const middlewareValidator = require("express-validator"); -const middlewareRestrictor = require("../middleware/restrictor"); - -// Create router -const {Router: newRouter} = express; -const router = newRouter(); - -router.use(express.json()); - -const cache = useCache(); - -/** - * @openapi - * /tokens/{token_id_prefix}/{token_id_suffix}: - * head: - * summary: Validate a token is valid or not - * description: This endpoint is used to validate a token is valid or not. - * tags: - * - tokens - * parameters: - * - name: token_id_prefix - * in: path - * description: ID of the token to validate, included in jti. - * required: true - * schema: - * type: string - * format: objectId - * - name: token_id_suffix - * in: path - * description: Revision of the user to validate, included in jti. - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Token is valid - * 400: - * description: Token is malformed - * 404: - * description: Token is invalid - */ -router.head("/:token_id_prefix/:token_id_suffix", - middlewareValidator.param("token_id_prefix").isMongoId().notEmpty(), - middlewareValidator.param("token_id_suffix").isInt().notEmpty(), - middlewareInspector, - withAwait(async (req, res) => { - // Assign shortcuts - const { - token_id_prefix: tokenIdPrefix, - token_id_suffix: tokenIdSuffix, - } = req.params; - - // Check token exists by the token ID - const tokenState = await Token.findById(tokenIdPrefix).exec(); - if (!tokenState) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Find user by the user ID - const user = await User.findById(tokenState.userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Check token ID suffix - if (parseInt(tokenIdSuffix) !== user.revision) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Return response - res.sendStatus(StatusCodes.OK); - }), -); - -/** - * @openapi - * /tokens: - * post: - * tags: - * - tokens - * summary: Issue a token session for a user - * description: Issues a token session for a user - * by sending an email with a code. - * The user can use the code to verify their identity later. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * description: The email address of the user. - * format: email - * example: fake_user@web-tech-tw.github.io - * responses: - * 201: - * description: Returns a session ID for the user - * to verify their identity later. - * content: - * application/json: - * schema: - * type: object - * properties: - * session_type: - * type: string - * description: The type of the session. - * example: token - * session_id: - * type: string - * description: The ID of the session. - * 404: - * description: Returns "Not Found" if the user cannot be found. - * 429: - * description: Returns "Too Many Requests" - * if the rate limit is exceeded. - */ -router.post("/", - middlewareValidator.body("email").isEmail(), - middlewareInspector, - middlewareRestrictor(10, 3600, false, StatusCodes.NOT_FOUND), - withAwait(async (req, res) => { - // Let email address be case-insensitive - const email = req.body.email.toLowerCase(); - - // Check user exists by the email address - const user = await User.findOne({email}).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle code and metadata - const metadata = { - userId: user.id, - email: user.email, - }; - const {code, sessionId} = utilCodeSession. - createOne("create_token", metadata, 6, 1800); - - // Handle conversion - const userData = user.toObject(); - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const userId = userData._id; - const userNickname = userData.nickname; - const userEmail = userData.email; - - const sessionTm = new Date().toISOString(); - const sessionUa = utilVisitor.getUserAgent(req, true); - const sessionIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("verify_create_token", { - to: userEmail, - audienceUrl, - userId, - userNickname, - userEmail, - sessionIp, - sessionId, - sessionUa, - sessionTm, - code, - }); - if (getMust("NODE_ENV") === "testing") { - cache.set("_testing_code", code); - } - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - - // Send response - res. - status(StatusCodes.CREATED). - send({ - session_type: sessionTypeCreateToken, - session_ip: sessionIp, - session_id: sessionId, - session_ua: sessionUa, - session_tm: sessionTm, - }); - }), -); - -/** - * @openapi - * /tokens: - * patch: - * tags: - * - tokens - * summary: Verify user's identity and issue an access token by a code - * description: Verify user's identity by checking the session_id and - * code that the user provides. - * If the session_id and code are valid, - * the server issues an access token. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * description: The code that the user receives in the email. - * session_id: - * type: string - * description: The ID of the session that - * the user receives in the email. - * responses: - * 201: - * description: Returns a header named - * "x-sara-refresh" that contains the access token. - * 403: - * description: Returns "Forbidden" - * if the user's identity cannot be verified. - * 404: - * description: Returns "Not Found" if the user cannot be found. - */ -router.patch("/", - middlewareValidator.body("code").isNumeric().notEmpty(), - middlewareValidator.body("code").isLength({min: 6, max: 6}).notEmpty(), - middlewareValidator.body("session_id").notEmpty(), - middlewareInspector, - middlewareRestrictor(10, 3600, false), - withAwait(async (req, res) => { - // Get metadata back by the code - const metadata = utilCodeSession. - getOne("create_token", req.body.session_id, req.body.code); - - if (metadata === null) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } else { - // Remove session - metadata.deleteIt(); - } - - // Check user exists by the email address - const user = await User.findOne({email: metadata.email}).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Check metadata user id - if (user.id !== metadata.userId) { - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Handle conversion - const userData = user.toObject(); - - // Handle avatar - const avatarRaw = userData.email.toLowerCase(); - const avatarHash = utilNative.sha256hex(avatarRaw); - userData.avatar_hash = avatarHash; - - // Generate token - const token = await utilXaraToken. - issue(userData); - - // Send response - res. - header(headerRefreshToken, token). - sendStatus(StatusCodes.CREATED); - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const userId = userData._id; - const userNickname = userData.nickname; - const userEmail = userData.email; - - const sessionId = req.body.session_id; - - const accessMethod = "Email Code"; - const accessTm = new Date().toISOString(); - const accessUa = utilVisitor.getUserAgent(req, true); - const accessIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("notify_create_token", { - to: userEmail, - audienceUrl, - userId, - userNickname, - userEmail, - sessionId, - accessMethod, - accessTm, - accessUa, - accessIp, - }); - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - }), -); - -/** - * @openapi - * /tokens/passkeys: - * post: - * tags: - * - tokens - * summary: Issue a passkey session for a user - * description: Issues a passkey session for a user by using - * the WebAuthn standard. - * The user can use the passkey to verify - * their identity later. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * description: The email address of the user. - * format: email - * example: fake_user@web-tech-tw.github.io - * responses: - * 201: - * description: Returns a session ID for the user - * to verify their identity later. - * content: - * application/json: - * schema: - * type: object - * properties: - * session_type: - * type: string - * description: The type of the session. - * example: token - * session_id: - * type: string - * description: The ID of the session. - * 404: - * description: Returns "Not Found" - * if the user cannot be found or - * the user has no passkeys. - * 429: - * description: Returns "Too Many Requests" - * if the rate limit is exceeded. - */ -router.post("/passkeys", - middlewareValidator.body("email").isEmail(), - middlewareInspector, - middlewareRestrictor(10, 3600, false), - withAwait(async (req, res) => { - // Let email address be case-insensitive - const email = req.body.email.toLowerCase(); - - // Check user exists by the email address - const user = await User.findOne({email}).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Fetch audience variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - const {hostname: audienceHost} = new URL(audienceUrl); - - // Check user has passkeys - if (!user.passkeys.length) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Fetch allowed credentials - const allowCredentials = user.passkeys.map((passkey) => ({ - id: passkey.id, - })); - - // Handle code and metadata - const sessionOptions = await generateAuthenticationOptions({ - rpID: audienceHost, - allowCredentials, - }); - - // Create session - const metadata = { - userId: user.id, - challenge: sessionOptions.challenge, - }; - const {sessionId} = utilPasskeySession. - createOne("create_token", metadata, 1800); - - // Send response - res. - status(StatusCodes.CREATED). - send({ - session_id: sessionId, - session_options: sessionOptions, - }); - }), -); - -/** - * @openapi - * /tokens/passkeys: - * patch: - * tags: - * - tokens - * summary: Verify user's identity and issue an access token by passkey - * description: Verify user's identity by checking the session_id and - * code that the user provides. - * If the session_id and credential are valid, - * the server issues an access token. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * description: The code that the user receives in the email. - * session_id: - * type: string - * description: The ID of the session that - * the user receives in the email. - * responses: - * 201: - * description: Returns a header named - * "x-sara-refresh" that contains the access token. - * 403: - * description: Returns "Forbidden" - * if the user's identity cannot be verified. - * 404: - * description: Returns "Not Found" if the user cannot be found. - */ -router.patch("/passkeys", - middlewareValidator.body("session_id").notEmpty(), - middlewareValidator.body("credential").isObject().notEmpty(), - middlewareInspector, - middlewareRestrictor(10, 3600, false), - withAwait(async (req, res) => { - // Get metadata back by the session ID - const metadata = utilPasskeySession. - getOne("create_token", req.body.session_id); - - if (metadata === null) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } else { - // Remove session - metadata.deleteIt(); - } - - // Fetch audience variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - const {hostname: audienceHost} = new URL(audienceUrl); - - // Check user exists by the email address - const user = await User.findById(metadata.userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - const {credential} = req.body; - const passkey = user.passkeys.find((passkey) => { - return passkey.id === credential.id; - }); - - let verification; - try { - verification = await verifyAuthenticationResponse({ - response: credential, - credential: passkey, - expectedChallenge: metadata.challenge, - expectedOrigin: audienceUrl, - expectedRPID: audienceHost, - }); - } catch (error) { - console.error(error); - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - if (!verification.verified) { - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Handle conversion - const userData = user.toObject(); - - // Handle avatar - const avatarRaw = userData.email.toLowerCase(); - const avatarHash = utilNative.sha256hex(avatarRaw); - userData.avatar_hash = avatarHash; - - // Generate token - const token = await utilXaraToken. - issue(userData); - - // Send response - res. - header(headerRefreshToken, token). - sendStatus(StatusCodes.CREATED); - - // Fetch email variables - const userId = userData._id; - const userNickname = userData.nickname; - const userEmail = userData.email; - - const sessionId = req.body.session_id; - - const accessMethod = `Passkey (${passkey.label})`; - const accessTm = new Date().toISOString(); - const accessUa = utilVisitor.getUserAgent(req, true); - const accessIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("notify_create_token", { - to: userEmail, - audienceUrl, - userId, - userNickname, - userEmail, - sessionId, - accessMethod, - accessTm, - accessUa, - accessIp, - }); - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - }), -); - -// Export routes mapper (function) -module.exports = () => { - // Use application - const app = useApp(); - - // Mount the router - app.use("/tokens", router); -}; diff --git a/src/routes/tokens.ts b/src/routes/tokens.ts new file mode 100644 index 0000000..fe3d924 --- /dev/null +++ b/src/routes/tokens.ts @@ -0,0 +1,286 @@ +import {Elysia, t} from "elysia"; +import {authPlugin} from "../plugins/auth"; +import {restrictorBefore, restrictorAfter} from "../plugins/restrictor"; +import User, {IPasskey} from "../models/user"; +import Token from "../models/token"; +import * as xaraToken from "../utils/xara_token"; +import * as codeSession from "../utils/code_session"; +import * as passkeySession from "../utils/passkey_session"; +import sendMail from "../utils/mail_sender"; +import {getIPAddress, getUserAgent} from "../utils/visitor"; +import {getMust} from "../config"; +import { + generateAuthenticationOptions, + verifyAuthenticationResponse, +} from "@simplewebauthn/server"; +import {HEADER_REFRESH_TOKEN, SESSION_TYPE_CREATE_TOKEN} from "../init/const"; +import {sha256hex} from "../utils/native"; +import {useCache} from "../init/cache"; +const cache = useCache(); + +/** + * Token management routes + */ +export const tokensRoutes = new Elysia({prefix: "/tokens"}) + .use(authPlugin) + /** + * Validate a token is valid or not + */ + .head("/:token_id_prefix/:token_id_suffix", + async ({params, status}) => { + const { + token_id_prefix: tokenIdPrefix, + token_id_suffix: tokenIdSuffix, + } = params; + + const tokenState = await Token.findById(tokenIdPrefix).exec(); + if (!tokenState) return status(404); + + const user = await User.findById(tokenState.userId).exec(); + if (!user) return status(404); + + if (parseInt(tokenIdSuffix) !== user.revision) return status(404); + + return new Response(null, {status: 200}); + }, { + params: t.Object({ + token_id_prefix: t.String(), + token_id_suffix: t.String(), + }), + }) + /** + * Issue a token session for a user (Email Code) + */ + .post("/", async ({body, set, request, server, status}) => { + const email = body.email.toLowerCase(); + + const user = await User.findOne({email}).exec(); + if (!user) return status(404); + + const metadata = { + userId: user.id, + email: user.email, + }; + const {code, sessionId} = codeSession.createOne( + "create_token", metadata, 6, 1800, + ); + + const userData = user.toObject(); + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + + const sessionTm = new Date().toISOString(); + const sessionUa = getUserAgent(request.headers, true); + const sessionIp = getIPAddress(request, server); + + try { + await sendMail("verify_create_token", { + to: userData.email, + audienceUrl, + userId: userData._id, + userNickname: userData.nickname, + userEmail: userData.email, + sessionIp, + sessionId, + sessionUa, + sessionTm, + code, + }); + if (getMust("NODE_ENV") === "testing") { + cache.set("_testing_code", code); + } + } catch (e) { + console.error(e); + return status(500); + } + + set.status = 201; + return { + session_type: SESSION_TYPE_CREATE_TOKEN, + session_ip: sessionIp, + session_id: sessionId, + session_ua: sessionUa, + session_tm: sessionTm, + }; + }, { + body: t.Object({ + email: t.String({format: "email"}), + }), + beforeHandle: restrictorBefore(10, 3600, false), + afterResponse: restrictorAfter(3600, false, 404), + }) + /** + * Verify user's identity and issue an access token by a code + */ + .patch("/", async ({body, set, request, server, status}) => { + const metadata = codeSession.getOne( + "create_token", + body.session_id, + body.code, + ); + + if (!metadata) return status(403); + metadata.deleteIt(); + + const user = await User.findOne({email: metadata.email}).exec(); + if (!user) return status(404); + + if (user.id !== metadata.userId) return status(403); + + const userData = user.toObject(); + userData.avatar_hash = sha256hex(userData.email.toLowerCase()); + + const token = await xaraToken.issue(userData); + + set.headers[HEADER_REFRESH_TOKEN] = token; + set.status = 201; + + // Notify user via email + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const sessionIp = getIPAddress(request, server); + const accessUa = getUserAgent(request.headers, true); + + sendMail("notify_create_token", { + to: userData.email, + audienceUrl, + userId: userData._id, + userNickname: userData.nickname, + userEmail: userData.email, + sessionId: body.session_id, + accessMethod: "Email Code", + accessTm: new Date().toISOString(), + accessUa, + accessIp: sessionIp, + }).catch(console.error); + + return {message: "Token issued"}; + }, { + body: t.Object({ + code: t.String({minLength: 6, maxLength: 6}), + session_id: t.String(), + }), + beforeHandle: restrictorBefore(10, 3600, false), + afterResponse: restrictorAfter(3600, false), + }) + /** + * Issue a passkey session for a user + */ + .post("/passkeys", async ({body, set, status}) => { + const email = body.email.toLowerCase(); + + const user = await User.findOne({email}).exec(); + if (!user || !user.passkeys.length) return status(404); + + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const {hostname: audienceHost} = new URL(audienceUrl); + + const allowCredentials = user.passkeys.map((passkey: IPasskey) => ({ + id: passkey.id, + })); + + const sessionOptions = await generateAuthenticationOptions({ + rpID: audienceHost, + allowCredentials, + }); + + const metadata = { + userId: user.id, + challenge: sessionOptions.challenge, + }; + const {sessionId} = passkeySession.createOne( + "create_token", metadata, 1800, + ); + + set.status = 201; + return { + session_type: SESSION_TYPE_CREATE_TOKEN, + session_id: sessionId, + session_options: sessionOptions, + }; + }, { + body: t.Object({ + email: t.String({format: "email"}), + }), + beforeHandle: restrictorBefore(10, 3600, false), + afterResponse: restrictorAfter(3600, false), + }) + /** + * Verify user's identity and issue an access token by passkey + */ + .patch("/passkeys", async ({body, set, request, server, status}) => { + const metadata = passkeySession.getOne("create_token", body.session_id); + + if (!metadata) return status(403); + metadata.deleteIt(); + + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const {hostname: audienceHost} = new URL(audienceUrl); + + const user = await User.findById(metadata.userId).exec(); + if (!user) return status(404); + + const {credential} = body; + const passkey = user.passkeys.find( + (pk: IPasskey) => pk.id === credential.id, + ); + + if (!passkey) return status(403); + + let verification; + try { + verification = await verifyAuthenticationResponse({ + response: credential as any, + credential: passkey as any, + expectedChallenge: metadata.challenge, + expectedOrigin: audienceUrl, + expectedRPID: audienceHost, + }); + } catch (e) { + console.error(e); + return status(403); + } + + if (!verification.verified) return status(403); + + const userData = user.toObject(); + userData.avatar_hash = sha256hex(userData.email.toLowerCase()); + + const token = await xaraToken.issue(userData); + + set.headers[HEADER_REFRESH_TOKEN] = token; + set.status = 201; + + // Notify user + sendMail("notify_create_token", { + to: userData.email, + audienceUrl, + userId: userData._id, + userNickname: userData.nickname, + userEmail: userData.email, + sessionId: body.session_id, + accessMethod: `Passkey (${passkey.label})`, + accessTm: new Date().toISOString(), + accessUa: getUserAgent(request.headers, true), + accessIp: getIPAddress(request, server), + }).catch(console.error); + + return {message: "Token issued"}; + }, { + body: t.Object({ + session_id: t.String(), + credential: t.Object({ + id: t.String(), + rawId: t.String(), + response: t.Object({ + authenticatorData: t.String(), + clientDataJSON: t.String(), + signature: t.String(), + userHandle: t.Optional(t.String()), + }), + type: t.String(), + clientExtensionResults: t.Object({}), + authenticatorAttachment: t.Optional(t.String()), + }), + }), + beforeHandle: restrictorBefore(10, 3600, false), + afterResponse: restrictorAfter(3600, false), + }); diff --git a/src/routes/users.js b/src/routes/users.js deleted file mode 100644 index 80b72ca..0000000 --- a/src/routes/users.js +++ /dev/null @@ -1,1063 +0,0 @@ -"use strict"; - -const {getMust} = require("../config"); - -const {StatusCodes} = require("http-status-codes"); -const {useApp, withAwait, express} = require("../init/express"); -const {useCache} = require("../init/cache"); - -const { - APP_NAME: issuerIdentity, - HEADER_REFRESH_TOKEN: headerRefreshToken, - SESSION_TYPE_CREATE_USER: sessionTypeCreateUser, - SESSION_TYPE_CREATE_PASSKEY: sessionTypeCreatePasskey, - SESSION_TYPE_UPDATE_EMAIL: sessionTypeUpdateEmail, -} = require("../init/const"); - -const User = require("../models/user"); - -const { - generateRegistrationOptions, - verifyRegistrationResponse, -} = require("@simplewebauthn/server"); - -const utilMailSender = require("../utils/mail_sender"); -const utilXaraToken = require("../utils/xara_token"); -const utilCodeSession = require("../utils/code_session"); -const utilPasskeySession = require("../utils/passkey_session"); -const utilVisitor = require("../utils/visitor"); -const utilNative = require("../utils/native"); - -const middlewareAccess = require("../middleware/access"); -const middlewareInspector = require("../middleware/inspector"); -const middlewareValidator = require("express-validator"); -const middlewareRestrictor = require("../middleware/restrictor"); - -// Create router -const {Router: newRouter} = express; -const router = newRouter(); - -router.use(express.json()); - -const cache = useCache(); - -/** - * @openapi - * /users/me: - * get: - * summary: Get user profile - * description: Returns the authenticated user's profile. - * tags: - * - users - * security: - * - XaraToken: [] - * responses: - * 200: - * description: User profile retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * profile: - * $ref: '#/components/schemas/User' - */ -router.get("/me", - middlewareAccess(null), - withAwait(async (req, res) => { - const userId = req.auth.id; - - const user = await User.findById(userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - const profile = user.toObject(); - - const avatarRaw = profile.email.toLowerCase(); - const avatarHash = utilNative.sha256hex(avatarRaw); - profile.avatar_hash = avatarHash; - - res.send({profile}); - }), -); - -/** - * @openapi - * /users/me: - * put: - * summary: Update user profile - * description: Updates the authenticated user's profile. - * tags: - * - users - * security: - * - XaraToken: [] - * requestBody: - * description: User object to be updated - * content: - * application/json: - * schema: - * type: object - * properties: - * nickname: - * type: string - * responses: - * 201: - * description: User profile updated successfully - * headers: - * x-sara-refresh: - * description: Bearer token for the updated profile - * schema: - * type: string - * 403: - * description: Reserved words are not allowed - * 404: - * description: User not found - */ -router.put("/me", - middlewareAccess(null), - withAwait(async (req, res) => { - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - user.nickname = req.body?.nickname || - req.auth.metadata.profile.nickname; - - // Check reserved words - if (user.nickname === issuerIdentity) { - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Increment revision - user.revision++; - - // Save user data - const userData = (await user.save()).toObject(); - - // Generate token - const token = utilXaraToken. - update(req.auth.secret, userData); - - // Send response - res. - header(headerRefreshToken, token). - sendStatus(StatusCodes.CREATED); - }), -); - -/** - * @openapi - * /users/me: - * delete: - * summary: Delete user profile - * description: Deletes the authenticated user's profile. (soft delete) - * tags: - * - users - * security: - * - XaraToken: [] - * responses: - * 204: - * description: User profile deleted successfully - * headers: - * x-sara-refresh: - * description: Use a character to empty the token - * schema: - * type: string - * 404: - * description: User not found - */ -router.delete("/me", - middlewareAccess(null), - withAwait(async (req, res) => { - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - user.nickname = issuerIdentity; - user.email = new Date().toISOString(); - user.passkeys = []; - - // Update values - await user.save(); - - // Send response - res. - header(headerRefreshToken, "|"). - sendStatus(StatusCodes.NO_CONTENT); - }), -); - -/** - * @openapi - * /users/me/email: - * put: - * summary: Update user's email - * description: Updates the authenticated user's email. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * tags: - * - users - * security: - * - XaraToken: [] - * requestBody: - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * description: New email address of the user - * responses: - * 200: - * description: Session created to update user's email - * content: - * application/json: - * schema: - * type: object - * properties: - * session_type: - * type: string - * description: Type of the session created - * example: update_email - * session_id: - * type: string - * description: ID of the session created - * example: 62159db19d393b330e57ca63 - * 400: - * description: Invalid request body - * 409: - * description: Email address already in use - * 500: - * description: Internal server error - */ -router.put("/me/email", - middlewareAccess(null), - middlewareValidator.body("email").isEmail().notEmpty(), - middlewareInspector, - middlewareRestrictor(10, 60, false, StatusCodes.CONFLICT), - withAwait(async (req, res) => { - // Let email address be case-insensitive - const email = req.body.email.toLowerCase(); - - // Handle code and metadata - const metadata = { - userId: req.auth.id, - email, - }; - const {code, sessionId} = utilCodeSession. - createOne("create_email", metadata, 8, 1800); - - // Handle conflict - if (await User.findOne({email}).exec()) { - res.sendStatus(StatusCodes.CONFLICT); - return; - } - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const {profile: userData} = req.auth.metadata; - - const userId = userData._id; - const userNickname = userData.nickname; - const userEmailOriginal = userData.email; - const userEmailUpdated = metadata.email; - - const sessionTm = new Date().toISOString(); - const sessionUa = utilVisitor.getUserAgent(req, true); - const sessionIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("verify_update_email", { - to: userEmailUpdated, - audienceUrl, - userId, - userNickname, - userEmailOriginal, - userEmailUpdated, - sessionIp, - sessionId, - sessionUa, - sessionTm, - code, - }); - if (getMust("NODE_ENV") === "testing") { - cache.set("_testing_code", code); - } - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - - // Send response - res. - status(StatusCodes.CREATED). - send({ - session_type: sessionTypeUpdateEmail, - session_ip: sessionIp, - session_id: sessionId, - session_ua: sessionUa, - session_tm: sessionTm, - }); - }), -); - -/** - * @openapi - * /users/me/email: - * patch: - * summary: Update user email by verification code. - * description: Update the authenticated user's email by verification code. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * tags: - * - users - * security: - * - XaraToken: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * minLength: 8 - * maxLength: 8 - * session_id: - * type: string - * responses: - * 201: - * description: The email is updated successfully. - * 403: - * description: Invalid verification code or session ID. - * 404: - * description: The user is not found. - */ -router.patch("/me/email", - middlewareAccess(null), - middlewareValidator.body("code").isNumeric().notEmpty(), - middlewareValidator.body("code").isLength({min: 8, max: 8}).notEmpty(), - middlewareValidator.body("session_id").isString().notEmpty(), - middlewareInspector, - middlewareRestrictor(10, 60, false), - withAwait(async (req, res) => { - // Get metadata back by the code - const metadata = utilCodeSession. - getOne("create_email", req.body.session_id, req.body.code); - - if (metadata === null) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } else { - // Remove session - metadata.deleteIt(); - } - - if (req.auth.id !== metadata.userId) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - const userEmailOriginal = user.email; - const userEmailUpdated = metadata.email; - - user.email = userEmailUpdated; - - // Increment revision - user.revision++; - - // Save user data - const userData = (await user.save()).toObject(); - - // Generate token - const token = utilXaraToken. - update(req.auth.secret, userData); - - // Send response - res. - header(headerRefreshToken, token). - sendStatus(StatusCodes.CREATED); - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const userId = userData._id; - const userNickname = userData.nickname; - - const sessionId = req.body.session_id; - - const accessTm = new Date().toISOString(); - const accessUa = utilVisitor.getUserAgent(req, true); - const accessIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("notify_update_email", { - to: userEmailUpdated, - cc: [userEmailOriginal], - audienceUrl, - userId, - userNickname, - userEmailOriginal, - userEmailUpdated, - sessionId, - accessIp, - accessUa, - accessTm, - }); - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - }), -); - - -/** - * @openapi - * /users/me/passkeys: - * post: - * tags: - * - users - * security: - * - XaraToken: [] - * summary: Add a passkey to the user - * description: Issues a passkey session for a user - * to add a passkey to their account. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * responses: - * 201: - * description: Returns a session ID for the user - * to verify their identity later. - * content: - * application/json: - * schema: - * type: object - * properties: - * session_type: - * type: string - * description: The type of the session. - * example: token - * session_id: - * type: string - * description: The ID of the session. - * 404: - * description: Returns "Not Found" if the user cannot be found. - * 429: - * description: Returns "Too Many Requests" - * if the rate limit is exceeded. - */ -router.post("/me/passkeys", - middlewareAccess(null), - withAwait(async (req, res) => { - // Fetch audience variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - const {hostname: audienceHost} = new URL(audienceUrl); - - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Fetch exclude credentials - const {passkeys} = user; - const excludeCredentials = passkeys.map((passkey) => ({ - id: passkey.id, - })); - - // Generate options - const sessionOptions = await generateRegistrationOptions({ - rpName: issuerIdentity, - rpID: audienceHost, - userName: req.auth.metadata.profile.email, - userDisplayName: req.auth.metadata.profile.nickname, - excludeCredentials, - authenticatorSelection: { - residentKey: "preferred", - userVerification: "preferred", - }, - }); - - const metadata = { - userId: req.auth.id, - challenge: sessionOptions.challenge, - }; - const {sessionId} = utilPasskeySession. - createOne(sessionTypeCreatePasskey, metadata, 1800); - - // Send response - res. - status(StatusCodes.CREATED). - send({ - session_type: sessionTypeCreatePasskey, - session_id: sessionId, - session_options: sessionOptions, - }); - }), -); - -/** - * @openapi - * /users/me/passkeys: - * patch: - * tags: - * - users - * security: - * - XaraToken: [] - * summary: Verify user's passkey to add it into the account - * description: Verify user's passkey to add it into the account. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * description: The code that the user receives in the email. - * session_id: - * type: string - * description: The ID of the session that - * the user receives in the email. - * responses: - * 201: - * description: Returns a header named - * "x-sara-refresh" that contains the access token. - * 403: - * description: Returns "Forbidden" - * if the user's identity cannot be verified. - * 404: - * description: Returns "Not Found" if the user cannot be found. - */ -router.patch("/me/passkeys", - middlewareAccess(null), - middlewareValidator.body("session_id").isString().notEmpty(), - middlewareValidator.body("credential").isObject().notEmpty(), - middlewareInspector, - withAwait(async (req, res) => { - // Fetch audience variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - const {hostname: audienceHost} = new URL(audienceUrl); - - // Get metadata back by the session ID - const metadata = utilPasskeySession. - getOne(sessionTypeCreatePasskey, req.body.session_id); - - if (metadata === null) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } else { - // Remove session - metadata.deleteIt(); - } - - if (req.auth.id !== metadata.userId) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Verify registration response - const verification = await verifyRegistrationResponse({ - response: req.body.credential, - expectedChallenge: metadata.challenge, - expectedOrigin: audienceUrl, - expectedRPID: audienceHost, - }); - - if (!verification.verified || !verification.registrationInfo) { - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - if (!user.passkeys) { - user.passkeys = []; - } - - const {registrationInfo} = verification; - const {credential} = registrationInfo; - - const labelPrefix = utilVisitor.getUserAgent(req, true); - const labelSuffix = utilNative.generateRandomCode(4); - const label = [labelPrefix, labelSuffix].join(" - "); - user.passkeys.push({...credential, label}); - - // Save user data - await user.save(); - - // Send response - res.sendStatus(StatusCodes.CREATED); - }), -); - -/** - * @openapi - * /users/me/passkeys/{passkey_id}: - * put: - * summary: Update user passkey details - * description: Updates the authenticated user's passkey details. - * tags: - * - users - * security: - * - XaraToken: [] - * parameters: - * - name: passkey_id - * in: path - * description: ID of the passkey to update - * required: true - * schema: - * type: string - * requestBody: - * description: Passkey object to be updated - * content: - * application/json: - * schema: - * type: object - * properties: - * label: - * type: string - * responses: - * 204: - * description: Passkey details updated successfully - * 404: - * description: Passkey not found - */ -router.put("/me/passkeys/:passkey_id", - middlewareValidator.param("passkey_id").isString().notEmpty(), - middlewareValidator.body("label").isString().notEmpty(), - middlewareInspector, - middlewareAccess(null), - withAwait(async (req, res) => { - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Find passkey by ID - const passkeyId = req.params.passkey_id; - const passkey = user.passkeys.find( - (passkey) => passkey.id === passkeyId, - ); - if (!passkey) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - passkey.label = req.body.label; - - // Save user data - await user.save(); - - // Send response - res.sendStatus(StatusCodes.NO_CONTENT); - }), -); - -/** - * @openapi - * /users/me/passkeys/{passkey_id}: - * delete: - * summary: Remove user passkey - * description: Remove the authenticated user's passkey. - * tags: - * - users - * security: - * - XaraToken: [] - * parameters: - * - name: passkey_id - * in: path - * description: ID of the passkey to delete - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Passkey removed successfully - * 404: - * description: Passkey not found - */ -router.delete("/me/passkeys/:passkey_id", - middlewareValidator.param("passkey_id").isString().notEmpty(), - middlewareInspector, - middlewareAccess(null), - withAwait(async (req, res) => { - // Check user exists by the ID - const user = await User.findById(req.auth.id).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Find passkey by ID - const passkeyId = req.params.passkey_id; - const passkeyIndex = user.passkeys.findIndex( - (passkey) => passkey.id === passkeyId, - ); - if (passkeyIndex === -1) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle updates - user.passkeys.splice(passkeyIndex, 1); - - // Save user data - await user.save(); - - // Send response - res.sendStatus(StatusCodes.NO_CONTENT); - }), -); - -/** - * @openapi - * /users/{user_id}: - * get: - * summary: Get user by ID - * description: Get user public profile by ID - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * tags: - * - users - * parameters: - * - name: user_id - * in: path - * description: ID of the user to retrieve - * required: true - * schema: - * type: string - * format: objectId - * responses: - * 200: - * description: User public profile - * content: - * application/json: - * schema: - * type: object - * properties: - * profile: - * $ref: '#/components/schemas/User' - * 404: - * description: User not found - */ -router.get("/:user_id", - middlewareValidator.param("user_id").isMongoId().notEmpty(), - middlewareInspector, - middlewareRestrictor(10, 60, true, StatusCodes.NOT_FOUND), - withAwait(async (req, res) => { - // Assign shortcuts - const userId = req.params.user_id; - - // Check user exists by the ID - const user = await User.findById(userId).exec(); - if (!user) { - res.sendStatus(StatusCodes.NOT_FOUND); - return; - } - - // Handle conversion - const userData = user.toObject(); - - const avatarRaw = userData.email.toLowerCase(); - const avatarHash = utilNative.sha256hex(avatarRaw); - - // Send response - res.send({ - profile: { - nickname: userData.nickname, - avatar_hash: avatarHash, - }, - }); - }), -); - -/** - * @openapi - * /users: - * post: - * tags: - * - users - * summary: Register a user - * description: This API endpoint registers a new user. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - nickname - * - email - * properties: - * nickname: - * type: string - * email: - * type: string - * example: - * nickname: Fake User - * email: fake_user@web-tech-tw.github.io - * responses: - * 201: - * description: Returns the session ID - * if the user is registered successfully. - * 400: - * description: Returns an error message if the request is invalid. - * 403: - * description: Reserved words are not allowed. - * 409: - * description: Returns an error message - * if the user's email already exists in the system. - */ -router.post("/", - middlewareValidator.body("nickname").notEmpty(), - middlewareValidator.body("email").isEmail(), - middlewareInspector, - middlewareRestrictor(20, 3600, false, StatusCodes.CONFLICT), - withAwait(async (req, res) => { - // Let email address be case-insensitive - const email = req.body.email.toLowerCase(); - - // Handle code and metadata - const metadata = { - nickname: req.body.nickname, - email, - created_at: Date.now(), - updated_at: Date.now(), - }; - const {code, sessionId} = utilCodeSession. - createOne(sessionTypeCreateUser, metadata, 7, 1800); - - // Check reserved words - if (metadata.nickname === issuerIdentity) { - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const userNickname = metadata.nickname; - const userEmail = metadata.email; - - const sessionTm = new Date().toISOString(); - const sessionUa = utilVisitor.getUserAgent(req, true); - const sessionIp = utilVisitor.getIPAddress(req); - - // Send email - try { - await utilMailSender("verify_create_user", { - to: userEmail, - audienceUrl, - userNickname, - userEmail, - sessionIp, - sessionId, - sessionUa, - sessionTm, - code, - }); - if (getMust("NODE_ENV") === "testing") { - cache.set("_testing_code", code); - } - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - - // Handle conflict - if (await User.findOne({email}).exec()) { - res.sendStatus(StatusCodes.CONFLICT); - return; - } - - // Send response - res. - status(StatusCodes.CREATED). - send({ - session_type: sessionTypeCreateUser, - session_ip: sessionIp, - session_id: sessionId, - session_ua: sessionUa, - session_tm: sessionTm, - }); - }), -); - -/** - * @openapi - * /users: - * patch: - * tags: - * - users - * summary: Verify user's registration via code sent to email - * description: This API endpoint verifies the user's registration using - * a code that was sent to their email address. - * It also includes a restrictor middleware that limits - * the rate at which the endpoint can be accessed. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - code - * - session_id - * properties: - * code: - * type: string - * session_id: - * type: string - * example: - * code: "1234567" - * session_id: "abc123" - * responses: - * 201: - * description: Returns a 201 status code with - * a 'x-sara-refresh' token in the header. - * 403: - * description: Returns a 403 status code - * if the provided code and session ID - * do not match or are invalid. - * 409: - * description: Returns a 409 status code if a user - * with the provided email address already exists. - */ -router.patch("/", - middlewareValidator.body("code").isNumeric(), - middlewareValidator.body("code").isLength({min: 7, max: 7}), - middlewareValidator.body("session_id").notEmpty(), - middlewareInspector, - middlewareRestrictor(20, 3600, false), - withAwait(async (req, res) => { - // Get metadata back by the code - const metadata = utilCodeSession. - getOne(sessionTypeCreateUser, req.body.session_id, req.body.code); - - if (metadata === null) { - // Check metadata - res.sendStatus(StatusCodes.FORBIDDEN); - return; - } else { - // Remove session - metadata.deleteIt(); - } - - // Handle conflict - if (await User.findOne({email: metadata.email}).exec()) { - res.sendStatus(StatusCodes.CONFLICT); - return; - } - - // Handle creation - const user = new User(metadata); - - // Save user data - const userData = (await user.save()).toObject(); - - // Handle avatar - const avatarRaw = userData.email.toLowerCase(); - const avatarHash = utilNative.sha256hex(avatarRaw); - userData.avatar_hash = avatarHash; - - // Generate token - const token = await utilXaraToken. - issue(userData); - - // Send response - res. - header(headerRefreshToken, token). - sendStatus(StatusCodes.CREATED); - - // Fetch email variables - const audienceUrl = getMust("SARA_AUDIENCE_URL"); - - const userId = userData._id; - const userNickname = userData.nickname; - const userEmail = userData.email; - - const sessionId = req.body.session_id; - const accessIp = utilVisitor.getIPAddress(req); - const accessUa = utilVisitor.getUserAgent(req, true); - const accessTm = new Date().toISOString(); - - // Send email - try { - await utilMailSender("notify_create_user", { - to: userEmail, - audienceUrl, - userId, - userNickname, - userEmail, - sessionId, - accessIp, - accessUa, - accessTm, - }); - } catch (e) { - console.error(e); - res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR); - return; - } - }), -); - -// Export routes mapper (function) -module.exports = () => { - // Use application - const app = useApp(); - - // Mount the router - app.use("/users", router); -}; diff --git a/src/routes/users.ts b/src/routes/users.ts new file mode 100644 index 0000000..8374fd9 --- /dev/null +++ b/src/routes/users.ts @@ -0,0 +1,471 @@ +import {Elysia, t} from "elysia"; +import {authPlugin} from "../plugins/auth"; +import {restrictorBefore, restrictorAfter} from "../plugins/restrictor"; +import User, {IPasskey} from "../models/user"; +import * as xaraToken from "../utils/xara_token"; +import * as codeSession from "../utils/code_session"; +import * as passkeySession from "../utils/passkey_session"; +import sendMail from "../utils/mail_sender"; +import {getIPAddress, getUserAgent} from "../utils/visitor"; +import {getMust} from "../config"; +import { + generateRegistrationOptions, + verifyRegistrationResponse, +} from "@simplewebauthn/server"; +import { + APP_NAME, + HEADER_REFRESH_TOKEN, + SESSION_TYPE_CREATE_USER, + SESSION_TYPE_CREATE_PASSKEY, + SESSION_TYPE_UPDATE_EMAIL, +} from "../init/const"; +import {sha256hex, generateRandomCode} from "../utils/native"; +import {useCache} from "../init/cache"; + +const cache = useCache(); + +/** + * User management routes + */ +export const usersRoutes = new Elysia({prefix: "/users"}) + .use(authPlugin) + /** + * Get user profile + */ + .get("/me", async ({auth, status}) => { + const userId = auth!.id; + const user = await User.findById(userId).exec(); + if (!user) return status(404); + + const profile = user.toObject(); + profile.avatar_hash = sha256hex(profile.email.toLowerCase()); + + return {profile}; + }, { + access: null, + }) + /** + * Update user profile + */ + .put("/me", async ({auth, body, set, status}) => { + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + user.nickname = body.nickname || auth!.metadata.profile.nickname; + + if (user.nickname === APP_NAME) return status(403); + + user.revision++; + const updatedUser = (await user.save()).toObject(); + + const token = xaraToken.update(auth!.secret, updatedUser); + set.headers[HEADER_REFRESH_TOKEN] = token; + set.status = 201; + return {message: "Profile updated"}; + }, { + access: null, + body: t.Object({ + nickname: t.Optional(t.String()), + }), + }) + /** + * Delete user profile (soft delete) + */ + .delete("/me", async ({auth, set, status}) => { + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + user.nickname = APP_NAME; + user.email = new Date().toISOString(); + user.passkeys = []; + + await user.save(); + + set.headers[HEADER_REFRESH_TOKEN] = "|"; + set.status = 204; + }, { + access: null, + }) + /** + * Update user's email (request session) + */ + .put("/me/email", async ({auth, body, request, server, status}) => { + const email = body.email.toLowerCase(); + + const metadata = { + userId: auth!.id, + email, + }; + const {code, sessionId} = codeSession.createOne( + "create_email", + metadata, + 8, + 1800, + ); + + if (await User.findOne({email}).exec()) return status(409); + + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const userData = auth!.metadata.profile; + + const sessionTm = new Date().toISOString(); + const sessionUa = getUserAgent(request.headers, true); + const sessionIp = getIPAddress(request, server); + + try { + await sendMail("verify_update_email", { + to: email, + audienceUrl, + userId: userData._id, + userNickname: userData.nickname, + userEmailOriginal: userData.email, + userEmailUpdated: email, + sessionIp, + sessionId, + sessionUa, + sessionTm, + code, + }); + if (getMust("NODE_ENV") === "testing") { + cache.set("_testing_code", code); + } + } catch (e) { + console.error(e); + return status(500); + } + + return { + session_type: SESSION_TYPE_UPDATE_EMAIL, + session_ip: sessionIp, + session_id: sessionId, + session_ua: sessionUa, + session_tm: sessionTm, + }; + }, { + access: null, + body: t.Object({email: t.String({format: "email"})}), + beforeHandle: restrictorBefore(10, 60, false), + afterResponse: restrictorAfter(60, false, 409), + }) + /** + * Update user email by verification code + */ + .patch("/me/email", async ({ + auth, + body, + set, + request, + server, + status, + }) => { + const metadata = codeSession.getOne( + "create_email", + body.session_id, + body.code, + ); + if (!metadata) return status(403); + metadata.deleteIt(); + + if (auth!.id !== metadata.userId) return status(403); + + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + const userEmailOriginal = user.email; + const userEmailUpdated = metadata.email; + + user.email = userEmailUpdated; + user.revision++; + const userData = (await user.save()).toObject(); + + const token = xaraToken.update(auth!.secret, userData); + set.headers[HEADER_REFRESH_TOKEN] = token; + set.status = 201; + + // Notify + sendMail("notify_update_email", { + to: userEmailUpdated, + cc: [userEmailOriginal], + audienceUrl: getMust("SARA_AUDIENCE_URL"), + userId: userData._id, + userNickname: userData.nickname, + userEmailOriginal, + userEmailUpdated, + sessionId: body.session_id, + accessIp: getIPAddress(request, server), + accessUa: getUserAgent(request.headers, true), + accessTm: new Date().toISOString(), + }).catch(console.error); + + return {message: "Email updated"}; + }, { + access: null, + body: t.Object({ + code: t.String({minLength: 8, maxLength: 8}), + session_id: t.String(), + }), + beforeHandle: restrictorBefore(10, 60, false), + afterResponse: restrictorAfter(60, false), + }) + /** + * Add a passkey (request options) + */ + .post("/me/passkeys", async ({auth, status}) => { + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const {hostname: audienceHost} = new URL(audienceUrl); + + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + const excludeCredentials = user.passkeys.map( + (pk: IPasskey) => ({id: pk.id}), + ); + + const sessionOptions = await generateRegistrationOptions({ + rpName: APP_NAME, + rpID: audienceHost, + userName: auth!.metadata.profile.email, + userDisplayName: auth!.metadata.profile.nickname, + excludeCredentials, + authenticatorSelection: { + residentKey: "preferred", + userVerification: "preferred", + }, + }); + + const metadata = { + userId: auth!.id, + challenge: sessionOptions.challenge, + }; + const {sessionId} = passkeySession.createOne( + SESSION_TYPE_CREATE_PASSKEY, + metadata, + 1800, + ); + + return { + session_type: SESSION_TYPE_CREATE_PASSKEY, + session_id: sessionId, + session_options: sessionOptions, + }; + }, { + access: null, + }) + /** + * Verify and add passkey + */ + .patch("/me/passkeys", async ({auth, body, request, status}) => { + const audienceUrl = getMust("SARA_AUDIENCE_URL"); + const {hostname: audienceHost} = new URL(audienceUrl); + + const metadata = passkeySession.getOne( + SESSION_TYPE_CREATE_PASSKEY, + body.session_id, + ); + if (!metadata) return status(403); + metadata.deleteIt(); + + if (auth!.id !== metadata.userId) return status(403); + + const verification = await verifyRegistrationResponse({ + response: body.credential, + expectedChallenge: metadata.challenge, + expectedOrigin: audienceUrl, + expectedRPID: audienceHost, + }); + + if (!verification.verified || !verification.registrationInfo) { + return status(403); + } + + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + const {credential} = verification.registrationInfo; + const labelPrefix = getUserAgent(request.headers, true); + const labelSuffix = generateRandomCode(4); + const label = `${labelPrefix} - ${labelSuffix}`; + + user.passkeys.push({...credential, label} as any); + await user.save(); + + return {message: "Passkey added"}; + }, { + access: null, + body: t.Object({ + session_id: t.String(), + credential: t.Any(), + }), + }) + /** + * Update passkey label + */ + .put("/me/passkeys/:passkey_id", async ({ + auth, + params, + body, + status, + }) => { + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + const passkey = user.passkeys.find( + (pk: IPasskey) => pk.id === params.passkey_id, + ); + if (!passkey) return status(404); + + passkey.label = body.label; + await user.save(); + return {message: "Passkey label updated"}; + }, { + access: null, + body: t.Object({label: t.String()}), + params: t.Object({passkey_id: t.String()}), + }) + /** + * Delete passkey + */ + .delete("/me/passkeys/:passkey_id", async ({auth, params, status}) => { + const user = await User.findById(auth!.id).exec(); + if (!user) return status(404); + + const index = user.passkeys.findIndex( + (pk: IPasskey) => pk.id === params.passkey_id, + ); + if (index === -1) return status(404); + + user.passkeys.splice(index, 1); + await user.save(); + return {message: "Passkey removed"}; + }, { + access: null, + params: t.Object({passkey_id: t.String()}), + }) + /** + * Get user by ID (Public profile) + */ + .get("/:user_id", async ({params, status}) => { + const user = await User.findById(params.user_id).exec(); + if (!user) return status(404); + + const userData = user.toObject(); + return { + profile: { + nickname: userData.nickname, + avatar_hash: sha256hex(userData.email.toLowerCase()), + }, + }; + }, { + params: t.Object({user_id: t.String()}), + beforeHandle: restrictorBefore(10, 60, true), + afterResponse: restrictorAfter(60, true, 404), + }) + /** + * Register a user (Request session) + */ + .post("/", async ({body, set, request, server, status}) => { + const email = body.email.toLowerCase(); + + const metadata = { + nickname: body.nickname, + email, + created_at: Date.now(), + updated_at: Date.now(), + }; + + if (metadata.nickname === APP_NAME) return status(403); + if (await User.findOne({email}).exec()) return status(409); + + const {code, sessionId} = codeSession.createOne( + SESSION_TYPE_CREATE_USER, + metadata, + 7, + 1800, + ); + + const sessionTm = new Date().toISOString(); + const sessionUa = getUserAgent(request.headers, true); + const sessionIp = getIPAddress(request, server); + + try { + await sendMail("verify_create_user", { + to: email, + audienceUrl: getMust("SARA_AUDIENCE_URL"), + userNickname: metadata.nickname, + userEmail: email, + sessionIp, + sessionId, + sessionUa, + sessionTm, + code, + }); + if (getMust("NODE_ENV") === "testing") { + cache.set("_testing_code", code); + } + } catch (e) { + console.error(e); + return status(500); + } + + set.status = 201; + return { + session_type: SESSION_TYPE_CREATE_USER, + session_ip: sessionIp, + session_id: sessionId, + session_ua: sessionUa, + session_tm: sessionTm, + }; + }, { + body: t.Object({ + nickname: t.String(), + email: t.String({format: "email"}), + }), + beforeHandle: restrictorBefore(20, 3600, false), + afterResponse: restrictorAfter(3600, false, 409), + }) + /** + * Verify registration via code + */ + .patch("/", async ({body, set, request, server, status}) => { + const metadata = codeSession.getOne( + SESSION_TYPE_CREATE_USER, + body.session_id, + body.code, + ); + if (!metadata) return status(403); + metadata.deleteIt(); + + if (await User.findOne({email: metadata.email}).exec()) { + return status(409); + } + + const user = new User(metadata); + const userData = (await user.save()).toObject(); + userData.avatar_hash = sha256hex(userData.email.toLowerCase()); + + const token = await xaraToken.issue(userData); + set.headers[HEADER_REFRESH_TOKEN] = token; + set.status = 201; + + // Notify + sendMail("notify_create_user", { + to: userData.email, + audienceUrl: getMust("SARA_AUDIENCE_URL"), + userId: userData._id, + userNickname: userData.nickname, + userEmail: userData.email, + sessionId: body.session_id, + accessIp: getIPAddress(request, server), + accessUa: getUserAgent(request.headers, true), + accessTm: new Date().toISOString(), + }).catch(console.error); + + return {message: "User registered"}; + }, { + body: t.Object({ + code: t.String({minLength: 7, maxLength: 7}), + session_id: t.String(), + }), + beforeHandle: restrictorBefore(20, 3600, false), + afterResponse: restrictorAfter(3600, false), + }); diff --git a/src/schemas/passkey.js b/src/schemas/passkey.js deleted file mode 100644 index f730715..0000000 --- a/src/schemas/passkey.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -const mongoose = require("mongoose"); -const {Schema} = mongoose; - -const schema = new Schema({ - id: { - type: String, - required: true, - }, - label: { - type: String, - required: true, - }, - publicKey: { - type: Buffer, - required: true, - set: (val) => Buffer.from(val), - }, - counter: { - type: Number, - required: true, - }, - transports: { - type: [String], - required: true, - }, -}, { - _id: false, - timestamps: true, -}); - -module.exports = schema; diff --git a/src/schemas/token.js b/src/schemas/token.js deleted file mode 100644 index c265579..0000000 --- a/src/schemas/token.js +++ /dev/null @@ -1,17 +0,0 @@ -"use strict"; - -const mongoose = require("mongoose"); -const {Schema} = mongoose; -const {ObjectId} = Schema.Types; - -const schema = new Schema({ - userId: { - type: ObjectId, - required: true, - }, -}, { - timestamps: true, - expires: "1d", -}); - -module.exports = schema; diff --git a/src/schemas/user.js b/src/schemas/user.js deleted file mode 100644 index ba5e5d9..0000000 --- a/src/schemas/user.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -const mongoose = require("mongoose"); -const {Schema} = mongoose; - -const Passkey = require("./passkey"); - -const schema = new Schema({ - email: { - type: String, - required: true, - unique: true, - }, - nickname: { - type: String, - required: true, - }, - roles: { - type: [String], - default: [], - }, - revision: { - type: Number, - default: 0, - }, - passkeys: { - type: [Passkey], - default: [], - }, -}, { - timestamps: true, -}); - -module.exports = schema; diff --git a/src/templates/mail/notify_create_token.js b/src/templates/mail/notify_create_token.js deleted file mode 100644 index 03ca5a6..0000000 --- a/src/templates/mail/notify_create_token.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `於 ${data.audienceUrl} 上的 Sara 系統登入成功`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 登入成功。 - - 以下為授權成功的登入資訊: - 申請識別碼: - ${data.sessionId} - 授權方法: - ${data.accessMethod} - 授權時間: - ${data.accessTm} - 授權目標裝置: - ${data.accessUa} - 授權目標 IP 位址: - ${data.accessIp} - - Sara 系統使用者識別碼: - ${data.userId} - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。
- 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 登入成功。 -

-

- 以下為授權成功的登入資訊:
-

    -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 授權方法:
    - ${data.accessMethod} -
  • -
  • - 授權時間:
    - ${data.accessTm} -
  • -
  • - 授權目標裝置:
    - ${data.accessUa} -
  • -
  • - 授權目標 IP 位址:
    - ${data.accessIp} -
  • -
-

-

- Sara 系統使用者識別碼:
- ${data.userId} -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/notify_create_token.ts b/src/templates/mail/notify_create_token.ts new file mode 100644 index 0000000..e22061d --- /dev/null +++ b/src/templates/mail/notify_create_token.ts @@ -0,0 +1,69 @@ +export const subject = (data: any) => `於 ${data.audienceUrl} 上的 Sara 系統登入成功`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 登入成功。 + + 以下為授權成功的登入資訊: + 申請識別碼: + ${data.sessionId} + 授權方法: + ${data.accessMethod} + 授權時間: + ${data.accessTm} + 授權目標裝置: + ${data.accessUa} + 授權目標 IP 位址: + ${data.accessIp} + + Sara 系統使用者識別碼: + ${data.userId} + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。
+ 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 登入成功。 +

+

+ 以下為授權成功的登入資訊:
+

    +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 授權方法:
    + ${data.accessMethod} +
  • +
  • + 授權時間:
    + ${data.accessTm} +
  • +
  • + 授權目標裝置:
    + ${data.accessUa} +
  • +
  • + 授權目標 IP 位址:
    + ${data.accessIp} +
  • +
+

+

+ Sara 系統使用者識別碼:
+ ${data.userId} +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/templates/mail/notify_create_user.js b/src/templates/mail/notify_create_user.js deleted file mode 100644 index 2179c16..0000000 --- a/src/templates/mail/notify_create_user.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `於 ${data.audienceUrl} 上的 Sara 系統註冊成功`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 註冊成功。 - - 以下為授權成功的註冊資訊: - 申請識別碼: - ${data.sessionId} - 授權時間: - ${data.accessTm} - 授權目標裝置: - ${data.accessUa} - 授權目標 IP 位址: - ${data.accessIp} - 電子郵件地址: - ${data.userEmail} - - Sara 系統使用者識別碼: - ${data.userId} - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。
- 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 註冊成功。 -

-

- 以下為授權成功的註冊資訊:
-

    -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 授權時間:
    - ${data.accessTm} -
  • -
  • - 授權目標裝置:
    - ${data.accessUa} -
  • -
  • - 授權目標 IP 位址:
    - ${data.accessIp} -
  • -
  • - 電子郵件地址:
    - ${data.userEmail} -
  • -
-

-

- Sara 系統使用者識別碼:
- ${data.userId} -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/notify_create_user.ts b/src/templates/mail/notify_create_user.ts new file mode 100644 index 0000000..7e1a6af --- /dev/null +++ b/src/templates/mail/notify_create_user.ts @@ -0,0 +1,69 @@ +export const subject = (data: any) => `於 ${data.audienceUrl} 上的 Sara 系統註冊成功`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 註冊成功。 + + 以下為授權成功的註冊資訊: + 申請識別碼: + ${data.sessionId} + 授權時間: + ${data.accessTm} + 授權目標裝置: + ${data.accessUa} + 授權目標 IP 位址: + ${data.accessIp} + 電子郵件地址: + ${data.userEmail} + + Sara 系統使用者識別碼: + ${data.userId} + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。
+ 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmail} 註冊成功。 +

+

+ 以下為授權成功的註冊資訊:
+

    +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 授權時間:
    + ${data.accessTm} +
  • +
  • + 授權目標裝置:
    + ${data.accessUa} +
  • +
  • + 授權目標 IP 位址:
    + ${data.accessIp} +
  • +
  • + 電子郵件地址:
    + ${data.userEmail} +
  • +
+

+

+ Sara 系統使用者識別碼:
+ ${data.userId} +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/templates/mail/notify_update_email.js b/src/templates/mail/notify_update_email.js deleted file mode 100644 index d4792ee..0000000 --- a/src/templates/mail/notify_update_email.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `於 ${data.audienceUrl} 上的 Sara 系統轉移成功`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - 使用者 ${data.userNickname} 已使用新的信箱 ${data.userEmailUpdated} 轉移成功。 - - 以下為授權成功的轉移資訊: - 申請識別碼: - ${data.sessionId} - 授權時間: - ${data.accessTm} - 授權目標裝置: - ${data.accessUa} - 授權目標 IP 位址: - ${data.accessIp} - 原電子郵件地址: - ${data.userEmailOriginal} - 新電子郵件地址: - ${data.userEmailUpdated} - - Sara 系統使用者識別碼: - ${data.userId} - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。
- 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmailUpdated} 轉移成功。 -

-

- 以下為授權成功的轉移資訊:
-

    -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 授權時間:
    - ${data.accessTm} -
  • -
  • - 授權目標裝置:
    - ${data.accessUa} -
  • -
  • - 授權目標 IP 位址:
    - ${data.accessIp} -
  • -
  • - 原電子郵件地址:
    - ${data.userEmailOriginal} -
  • -
  • - 新電子郵件地址:
    - ${data.userEmailUpdated} -
  • -
-

-

- Sara 系統使用者識別碼:
- ${data.userId} -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/notify_update_email.ts b/src/templates/mail/notify_update_email.ts new file mode 100644 index 0000000..26306f7 --- /dev/null +++ b/src/templates/mail/notify_update_email.ts @@ -0,0 +1,75 @@ +export const subject = (data: any) => `於 ${data.audienceUrl} 上的 Sara 系統轉移成功`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + 使用者 ${data.userNickname} 已使用新的信箱 ${data.userEmailUpdated} 轉移成功。 + + 以下為授權成功的轉移資訊: + 申請識別碼: + ${data.sessionId} + 授權時間: + ${data.accessTm} + 授權目標裝置: + ${data.accessUa} + 授權目標 IP 位址: + ${data.accessIp} + 原電子郵件地址: + ${data.userEmailOriginal} + 新電子郵件地址: + ${data.userEmailUpdated} + + Sara 系統使用者識別碼: + ${data.userId} + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。
+ 使用者 ${data.userNickname} 已使用您的信箱 ${data.userEmailUpdated} 轉移成功。 +

+

+ 以下為授權成功的轉移資訊:
+

    +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 授權時間:
    + ${data.accessTm} +
  • +
  • + 授權目標裝置:
    + ${data.accessUa} +
  • +
  • + 授權目標 IP 位址:
    + ${data.accessIp} +
  • +
  • + 原電子郵件地址:
    + ${data.userEmailOriginal} +
  • +
  • + 新電子郵件地址:
    + ${data.userEmailUpdated} +
  • +
+

+

+ Sara 系統使用者識別碼:
+ ${data.userId} +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/templates/mail/verify_create_token.js b/src/templates/mail/verify_create_token.js deleted file mode 100644 index 6d6fdc2..0000000 --- a/src/templates/mail/verify_create_token.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `登入 ${data.audienceUrl} 上的 Sara 系統帳號`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行登入。 - - 這裡是您的登入代碼: - ${data.code} - - 這份請求來自於: - 申請時間: - ${data.sessionTm} - 申請識別碼: - ${data.sessionId} - 申請來源裝置: - ${data.sessionUa} - 申請來源 IP 位址: - ${data.sessionIp} - - Sara 系統使用者識別碼: - ${data.userId} - - 若您未曾請求過該代碼,請您無視本電子郵件。 - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。
- 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行登入。 -

-

- 這裡是您的登入代碼:
- ${data.code} -

-

- 這份請求來自於: -

    -
  • - 申請時間:
    - ${data.sessionTm} -
  • -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 申請來源裝置:
    - ${data.sessionUa} -
  • -
  • - 申請來源 IP 位址:
    - ${data.sessionIp} -
  • -
-

-

- Sara 系統使用者識別碼:
- ${data.userId} -

-

- 若您未曾請求過該代碼,請您無視本電子郵件。 -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/verify_create_token.ts b/src/templates/mail/verify_create_token.ts new file mode 100644 index 0000000..7e12a4c --- /dev/null +++ b/src/templates/mail/verify_create_token.ts @@ -0,0 +1,75 @@ +export const subject = (data: any) => `登入 ${data.audienceUrl} 上的 Sara 系統帳號`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行登入。 + + 這裡是您的登入代碼: + ${data.code} + + 這份請求來自於: + 申請時間: + ${data.sessionTm} + 申請識別碼: + ${data.sessionId} + 申請來源裝置: + ${data.sessionUa} + 申請來源 IP 位址: + ${data.sessionIp} + + Sara 系統使用者識別碼: + ${data.userId} + + 若您未曾請求過該代碼,請您無視本電子郵件。 + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。
+ 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行登入。 +

+

+ 這裡是您的登入代碼:
+ ${data.code} +

+

+ 這份請求來自於: +

    +
  • + 申請時間:
    + ${data.sessionTm} +
  • +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 申請來源裝置:
    + ${data.sessionUa} +
  • +
  • + 申請來源 IP 位址:
    + ${data.sessionIp} +
  • +
+

+

+ Sara 系統使用者識別碼:
+ ${data.userId} +

+

+ 若您未曾請求過該代碼,請您無視本電子郵件。 +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/templates/mail/verify_create_user.js b/src/templates/mail/verify_create_user.js deleted file mode 100644 index 00dd969..0000000 --- a/src/templates/mail/verify_create_user.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `註冊 ${data.audienceUrl} 上的 Sara 系統帳號`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行註冊。 - - 這裡是您的註冊代碼: - ${data.code} - - 這份請求來自於: - 申請時間: - ${data.sessionTm} - 申請識別碼: - ${data.sessionId} - 申請來源裝置: - ${data.sessionUa} - 申請來源 IP 位址: - ${data.sessionIp} - - 若您未曾請求過該代碼,請您無視本電子郵件。 - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。
- 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行註冊。 -

-

- 這裡是您的註冊代碼:
- ${data.code} -

-

- 這份請求來自於: -

    -
  • - 申請時間:
    - ${data.sessionTm} -
  • -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 申請來源裝置:
    - ${data.sessionUa} -
  • -
  • - 申請來源 IP 位址:
    - ${data.sessionIp} -
  • -
-

-

- 若您未曾請求過該代碼,請您無視本電子郵件。 -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/verify_create_user.ts b/src/templates/mail/verify_create_user.ts new file mode 100644 index 0000000..1cbf378 --- /dev/null +++ b/src/templates/mail/verify_create_user.ts @@ -0,0 +1,68 @@ +export const subject = (data: any) => `註冊 ${data.audienceUrl} 上的 Sara 系統帳號`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行註冊。 + + 這裡是您的註冊代碼: + ${data.code} + + 這份請求來自於: + 申請時間: + ${data.sessionTm} + 申請識別碼: + ${data.sessionId} + 申請來源裝置: + ${data.sessionUa} + 申請來源 IP 位址: + ${data.sessionIp} + + 若您未曾請求過該代碼,請您無視本電子郵件。 + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。
+ 使用者 ${data.userNickname} 申請使用您的信箱 ${data.userEmail} 進行註冊。 +

+

+ 這裡是您的註冊代碼:
+ ${data.code} +

+

+ 這份請求來自於: +

    +
  • + 申請時間:
    + ${data.sessionTm} +
  • +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 申請來源裝置:
    + ${data.sessionUa} +
  • +
  • + 申請來源 IP 位址:
    + ${data.sessionIp} +
  • +
+

+

+ 若您未曾請求過該代碼,請您無視本電子郵件。 +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/templates/mail/verify_update_email.js b/src/templates/mail/verify_update_email.js deleted file mode 100644 index 5f03f97..0000000 --- a/src/templates/mail/verify_update_email.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; - -module.exports = { - subject: (data) => `轉移 ${data.audienceUrl} 上的 Sara 系統帳號`, - text: (data) => ` - 您好,這裡是 ${data.audienceUrl} 網站。 - - 使用者 ${data.userNickname}, - 請求將帳號自電子郵件地址: - ${data.userEmailOriginal}, - 轉移至本電子郵件地址: - ${data.userEmailUpdated} - 上。 - - 這裡是您的轉移代碼: - ${data.code} - - 這份請求來自於: - 申請時間: - ${data.sessionIp} - 申請識別碼: - ${data.sessionId} - 申請來源裝置: - ${data.sessionUa} - 申請來源 IP 位址: - ${data.sessionIp} - - Sara 系統使用者識別碼: - ${data.userId} - - 若您未曾請求過該代碼,請您無視本電子郵件。 - - 本電子郵件由系統自動發送,請勿回覆。 - - 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, - 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 - `, - html: (data) => ` -

- 您好,這裡是 ${data.audienceUrl} 網站。 -

-

- 使用者 ${data.userNickname}, - 請求將帳號自電子郵件地址: - ${data.userEmailOriginal}, - 轉移至本電子郵件地址: - ${data.userEmailUpdated} - 上。 -

-

- 這裡是您的轉移代碼:
- ${data.code} -

-

- 這份請求來自於: -

    -
  • - 申請時間:
    - ${data.sessionTm} -
  • -
  • - 申請識別碼:
    - ${data.sessionId} -
  • -
  • - 申請來源裝置:
    - ${data.sessionUa} -
  • -
  • - 申請來源 IP 位址:
    - ${data.sessionIp} -
  • -
-

-

- Sara 系統使用者識別碼:
- ${data.userId} -

-

- 若您未曾請求過該代碼,請您無視本電子郵件。 -

-

- 本電子郵件由系統自動發送,請勿回覆。 -

-

- 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
- 由 臺灣網際網路技術推廣組織 提供技術支援。 -

- `, -}; diff --git a/src/templates/mail/verify_update_email.ts b/src/templates/mail/verify_update_email.ts new file mode 100644 index 0000000..e347608 --- /dev/null +++ b/src/templates/mail/verify_update_email.ts @@ -0,0 +1,88 @@ +export const subject = (data: any) => `轉移 ${data.audienceUrl} 上的 Sara 系統帳號`; + +export const text = (data: any) => ` + 您好,這裡是 ${data.audienceUrl} 網站。 + + 使用者 ${data.userNickname}, + 請求將帳號自電子郵件地址: + ${data.userEmailOriginal}, + 轉移至本電子郵件地址: + ${data.userEmailUpdated} + 上。 + + 這裡是您的轉移代碼: + ${data.code} + + 這份請求來自於: + 申請時間: + ${data.sessionTm} + 申請識別碼: + ${data.sessionId} + 申請來源裝置: + ${data.sessionUa} + 申請來源 IP 位址: + ${data.sessionIp} + + Sara 系統使用者識別碼: + ${data.userId} + + 若您未曾請求過該代碼,請您無視本電子郵件。 + + 本電子郵件由系統自動發送,請勿回覆。 + + 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案, + 由臺灣網際網路技術推廣組織(https://web-tech.tw)提供技術支援。 +`; + +export const html = (data: any) => ` +

+ 您好,這裡是 ${data.audienceUrl} 網站。 +

+

+ 使用者 ${data.userNickname}, + 請求將帳號自電子郵件地址: + ${data.userEmailOriginal}, + 轉移至本電子郵件地址: + ${data.userEmailUpdated} + 上。 +

+

+ 這裡是您的轉移代碼:
+ ${data.code} +

+

+ 這份請求來自於: +

    +
  • + 申請時間:
    + ${data.sessionTm} +
  • +
  • + 申請識別碼:
    + ${data.sessionId} +
  • +
  • + 申請來源裝置:
    + ${data.sessionUa} +
  • +
  • + 申請來源 IP 位址:
    + ${data.sessionIp} +
  • +
+

+

+ Sara 系統使用者識別碼:
+ ${data.userId} +

+

+ 若您未曾請求過該代碼,請您無視本電子郵件。 +

+

+ 本電子郵件由系統自動發送,請勿回覆。 +

+

+ 「Sara 系統」是一個開放原始碼的無密碼式身份認證解決方案。
+ 由 臺灣網際網路技術推廣組織 提供技術支援。 +

+`; diff --git a/src/utils/code_session.js b/src/utils/code_session.js deleted file mode 100644 index 2824f9c..0000000 --- a/src/utils/code_session.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; - -const {generateRandomCode} = require("./native"); -const {nanoid: generateNanoId} = require("nanoid"); - -const {useCache} = require("../init/cache"); - -const cache = useCache(); - -const getSessionCodeName = - (type, sessionId, code) => - `code:${type}:${sessionId}@${code}`; - -/** - * Create a code session with data. - * @param {string} type the type of the session - * @param {any} metadata the data - * @param {number} codeLength length of the code - * @param {number} ttl the time to live in seconds - * @return {object} - */ -function createOne(type, metadata, codeLength, ttl) { - const sessionId = generateNanoId(); - const code = generateRandomCode(codeLength); - const sessionCodeName = getSessionCodeName( - type, sessionId, code, - ); - - cache.set(sessionCodeName, metadata, ttl); - const deleteIt = () => { - cache.del(sessionCodeName); - }; - - return { - code, - sessionId, - deleteIt, - }; -} - -/** - * Get data of the code session - * @param {string} type the type of the session - * @param {string} sessionId the session ID - * @param {string} code the code - * @return {object|null} - */ -function getOne(type, sessionId, code) { - const sessionCodeName = getSessionCodeName( - type, sessionId, code, - ); - if (!cache.has(sessionCodeName)) { - return null; - } - - const metadata = cache.get(sessionCodeName); - const deleteIt = () => { - cache.del(sessionCodeName); - }; - - return { - ...metadata, - deleteIt, - }; -} - -module.exports = { - createOne, - getOne, -}; diff --git a/src/utils/code_session.ts b/src/utils/code_session.ts new file mode 100644 index 0000000..cacc65c --- /dev/null +++ b/src/utils/code_session.ts @@ -0,0 +1,69 @@ +import {generateRandomCode} from "./native"; +import {nanoid as generateNanoId} from "nanoid"; +import {useCache} from "../init/cache"; + +const cache = useCache(); + +/** + * Get session code name. + * @param {string} type - The session type. + * @param {string} sessionId - The session ID. + * @param {string} code - The code. + * @return {string} The cache key. + */ +const getSessionCodeName = (type: string, sessionId: string, code: string) => + `code:${type}:${sessionId}@${code}`; + +/** + * Create a new code session. + * @param {string} type - The session type. + * @param {any} metadata - The session metadata. + * @param {number} codeLength - The code length. + * @param {number} ttl - The time to live in seconds. + * @return {object} The session data. + */ +export function createOne( + type: string, + metadata: any, + codeLength: number, + ttl: number, +) { + const sessionId = generateNanoId(); + const code = generateRandomCode(codeLength); + const sessionCodeName = getSessionCodeName(type, sessionId, code); + + cache.set(sessionCodeName, metadata, ttl); + const deleteIt = () => { + cache.del(sessionCodeName); + }; + + return { + code, + sessionId, + deleteIt, + }; +} + +/** + * Get a code session. + * @param {string} type - The session type. + * @param {string} sessionId - The session ID. + * @param {string} code - The code. + * @return {object|null} The session data or null if not found. + */ +export function getOne(type: string, sessionId: string, code: string) { + const sessionCodeName = getSessionCodeName(type, sessionId, code); + if (!cache.has(sessionCodeName)) { + return null; + } + + const metadata = cache.get(sessionCodeName) as any; + const deleteIt = () => { + cache.del(sessionCodeName); + }; + + return { + ...metadata, + deleteIt, + }; +} diff --git a/src/utils/mail_sender.js b/src/utils/mail_sender.js deleted file mode 100644 index 0162320..0000000 --- a/src/utils/mail_sender.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; -// Mail Sender of Sara - -const {getMust, getEnabled} = require("../config"); - -const constant = require("../init/const"); - -const nodemailer = require("nodemailer"); - -const transporter = nodemailer.createTransport({ - host: getMust("MAIL_SMTP_HOST"), - port: getMust("MAIL_SMTP_PORT"), - secure: getEnabled("MAIL_SMTP_SECURE"), - auth: getMust("MAIL_SMTP_USERNAME") ? { - user: getMust("MAIL_SMTP_USERNAME"), - pass: getMust("MAIL_SMTP_PASSWORD"), - } : null, -}); - -const isTestMailAddress = (addr) => - addr.endsWith("@" + constant.TEST_EMAIL_DOMAIN); - -module.exports = function(template, data) { - if (isTestMailAddress(data.to)) { - return new Promise((resolve) => { - if (getMust("NODE_ENV") === "development") { - console.info("new_mail", {template, data}); - } - resolve(); - }); - } - - const {subject, text, html} = require(`../templates/mail/${template}.js`); - return transporter.sendMail({ - from: getMust("MAIL_SMTP_FROM"), - to: data.to, - cc: data.cc, - subject: subject(data), - text: text(data), - html: html(data), - }); -}; diff --git a/src/utils/mail_sender.ts b/src/utils/mail_sender.ts new file mode 100644 index 0000000..b218383 --- /dev/null +++ b/src/utils/mail_sender.ts @@ -0,0 +1,54 @@ +// Mail Sender of Sara +import {getMust, getEnabled, get, getFallback} from "../config"; +import * as constant from "../init/const"; +import nodemailer from "nodemailer"; + +const transporter = nodemailer.createTransport({ + host: getMust("MAIL_SMTP_HOST"), + port: parseInt(getMust("MAIL_SMTP_PORT")), + secure: getEnabled("MAIL_SMTP_SECURE"), + auth: get("MAIL_SMTP_USERNAME") ? { + user: get("MAIL_SMTP_USERNAME") as string, + pass: get("MAIL_SMTP_PASSWORD") as string, + } : undefined, +}); + +/** + * Check if address is a test mail address. + * @param {string} addr - The address to check. + * @return {boolean} Whether it is a test mail address. + */ +const isTestMailAddress = (addr: string) => + addr.endsWith("@" + constant.TEST_EMAIL_DOMAIN); + +/** + * Send an email using a template. + * @param {string} template - The template name. + * @param {any} data - The data for the template. + * @return {Promise} The result of sending the mail. + */ +export default async function sendMail( + template: string, + data: any, +): Promise { + const nodeEnv = getFallback("NODE_ENV", "development"); + if (isTestMailAddress(data.to) || nodeEnv === "testing") { + if (nodeEnv === "development" || nodeEnv === "testing") { + console.info("new_mail", {template, data}); + } + return Promise.resolve(); + } + + // Use path relative to the file + const templateModule = await import(`../templates/mail/${template}.ts`); + const {subject, text, html} = templateModule; + + return transporter.sendMail({ + from: getMust("MAIL_SMTP_FROM"), + to: data.to, + cc: data.cc, + subject: subject(data), + text: text(data), + html: html(data), + }); +} diff --git a/src/utils/native.js b/src/utils/native.js deleted file mode 100644 index 19be721..0000000 --- a/src/utils/native.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; -// The simple toolbox for Node.js - -const crypto = require("node:crypto"); - -/** - * Shortcut for hasOwnProperty with safe. - * @module native - * @function - * @param {object} srcObject - * @param {string} propName - * @return {boolean} - */ -function isObjectPropExists(srcObject, propName) { - return Object.hasOwn(srcObject, propName); -} - -/** - * Generate random code with length. - * @param {number} length length of code - * @return {string} - */ -function generateRandomCode(length) { - const maxValue = (10 ** length) - 1; - return crypto. - randomInt(0, maxValue). - toString(). - padStart(length, "0"); -} - -/** - * Hash string into sha256 hex. - * @param {string} data - * @return {string} - */ -function sha256hex(data) { - return crypto. - createHash("sha256"). - update(data). - digest("hex"); -} - -// Export (object) -module.exports = { - isObjectPropExists, - generateRandomCode, - sha256hex, -}; diff --git a/src/utils/native.ts b/src/utils/native.ts new file mode 100644 index 0000000..73962d1 --- /dev/null +++ b/src/utils/native.ts @@ -0,0 +1,39 @@ +import crypto from "node:crypto"; + +/** + * Shortcut for hasOwnProperty with safe. + * @param {object} srcObject - The source object. + * @param {string} propName - The property name. + * @return {boolean} Whether the property exists. + */ +export function isObjectPropExists( + srcObject: object, + propName: string, +): boolean { + return Object.hasOwn(srcObject, propName); +} + +/** + * Generate random code with length. + * @param {number} length - The length of the code. + * @return {string} The generated code. + */ +export function generateRandomCode(length: number): string { + const maxValue = Math.pow(10, length) - 1; + return crypto. + randomInt(0, maxValue). + toString(). + padStart(length, "0"); +} + +/** + * Hash string into sha256 hex. + * @param {string} data - The data to hash. + * @return {string} The sha256 hex string. + */ +export function sha256hex(data: string): string { + return crypto. + createHash("sha256"). + update(data). + digest("hex"); +} diff --git a/src/utils/passkey_session.js b/src/utils/passkey_session.js deleted file mode 100644 index 6f7f8a7..0000000 --- a/src/utils/passkey_session.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; - -const {nanoid: generateNanoId} = require("nanoid"); - -const {useCache} = require("../init/cache"); - -const cache = useCache(); - -const getSessionCodeName = - (type, sessionId) => `passkey:${type}:${sessionId}`; - -/** - * Create a passkey session with data. - * @param {string} type the type of the session - * @param {any} metadata the data - * @param {number} ttl the time to live in seconds - * @return {object} - */ -function createOne(type, metadata, ttl) { - const sessionId = generateNanoId(); - const sessionCodeName = getSessionCodeName(type, sessionId); - - cache.set(sessionCodeName, metadata, ttl); - const deleteIt = () => { - cache.del(sessionCodeName); - }; - - return { - sessionId, - deleteIt, - }; -} - -/** - * Get data of the passkey session - * @param {string} type the type of the session - * @param {string} sessionId the session ID - * @return {object|null} - */ -function getOne(type, sessionId) { - const sessionCodeName = getSessionCodeName(type, sessionId); - if (!cache.has(sessionCodeName)) { - return null; - } - - const metadata = cache.get(sessionCodeName); - const deleteIt = () => { - cache.del(sessionCodeName); - }; - - return { - ...metadata, - deleteIt, - }; -} - -module.exports = { - createOne, - getOne, -}; diff --git a/src/utils/passkey_session.ts b/src/utils/passkey_session.ts new file mode 100644 index 0000000..47d99dd --- /dev/null +++ b/src/utils/passkey_session.ts @@ -0,0 +1,58 @@ +import {nanoid as generateNanoId} from "nanoid"; +import {useCache} from "../init/cache"; + +const cache = useCache(); + +/** + * Get session code name. + * @param {string} type - The session type. + * @param {string} sessionId - The session ID. + * @return {string} The cache key. + */ +const getSessionCodeName = (type: string, sessionId: string) => + `passkey:${type}:${sessionId}`; + +/** + * Create a new passkey session. + * @param {string} type - The session type. + * @param {any} metadata - The session metadata. + * @param {number} ttl - The time to live in seconds. + * @return {object} The session data. + */ +export function createOne(type: string, metadata: any, ttl: number) { + const sessionId = generateNanoId(); + const sessionCodeName = getSessionCodeName(type, sessionId); + + cache.set(sessionCodeName, metadata, ttl); + const deleteIt = () => { + cache.del(sessionCodeName); + }; + + return { + sessionId, + deleteIt, + }; +} + +/** + * Get a passkey session. + * @param {string} type - The session type. + * @param {string} sessionId - The session ID. + * @return {object|null} The session data or null if not found. + */ +export function getOne(type: string, sessionId: string) { + const sessionCodeName = getSessionCodeName(type, sessionId); + if (!cache.has(sessionCodeName)) { + return null; + } + + const metadata = cache.get(sessionCodeName) as any; + const deleteIt = () => { + cache.del(sessionCodeName); + }; + + return { + ...metadata, + deleteIt, + }; +} diff --git a/src/utils/test_token.js b/src/utils/test_token.ts similarity index 56% rename from src/utils/test_token.js rename to src/utils/test_token.ts index b4a34f3..f76eff6 100644 --- a/src/utils/test_token.js +++ b/src/utils/test_token.ts @@ -1,11 +1,6 @@ -"use strict"; // Token utils for testing/debugging or developing. - -// Import config -const {isProduction} = require("../config"); - -// Import utils -const {sha256hex} = require("./native"); +import {isProduction} from "../config"; +import {sha256hex} from "./native"; // Default fake user const DEFAULT_FAKE_USER = { @@ -13,41 +8,37 @@ const DEFAULT_FAKE_USER = { nickname: "Fake User", email: "fake_user@web-tech-tw.github.io", avatar_hash: sha256hex("fake_user@web-tech-tw.github.io"), - roles: [], + roles: [] as string[], }; /** * Returns a new user profile - * @module test_token - * @function - * @return {object} + * @return {object} The fake user profile. */ -function newProfile() { +export function newProfile() { return structuredClone(DEFAULT_FAKE_USER); } /** * Issue token - * @module test_token - * @function - * @param {object} userData - The user data to generate the token for. - * @return {string} + * @param {any} userData - The user data. + * @return {string} The issued token. */ -function issue(userData) { +export function issue(userData?: any): string { if (isProduction()) { throw new Error("test_token is not allowed in production"); } - userData = userData || DEFAULT_FAKE_USER; + const data = userData || DEFAULT_FAKE_USER; const user = { - _id: userData._id, - email: userData.email, - nickname: userData.nickname, - avatar_hash: userData.avatar_hash, - roles: userData.roles, - created_at: userData.created_at, - updated_at: userData.updated_at, + _id: data._id, + email: data.email, + nickname: data.nickname, + avatar_hash: data.avatar_hash, + roles: data.roles, + created_at: data.created_at, + updated_at: data.updated_at, }; const userJson = JSON.stringify(user); @@ -58,17 +49,19 @@ function issue(userData) { /** * Validate token - * @module test_token - * @function - * @param {string} token - The token to valid. - * @return {object} + * @param {string} token - The token to validate. + * @return {object} The validation result. */ -function validate(token) { +export function validate(token: string) { if (isProduction()) { throw new Error("test_token is not allowed in production"); } - const result = { + const result: { + userId: string | null; + payload: any; + isAborted: boolean; + } = { userId: null, payload: null, isAborted: false, @@ -92,10 +85,3 @@ function validate(token) { return result; } - -// Export (object) -module.exports = { - newProfile, - issue, - validate, -}; diff --git a/src/utils/visitor.js b/src/utils/visitor.js deleted file mode 100644 index b0a28dd..0000000 --- a/src/utils/visitor.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; -// The simple toolbox for fetch visitor information from HTTP request. - -const { - isProduction, -} = require("../config"); - -const uaParser = require("ua-parser-js"); - -/** - * Get IP Address. - * @module visitor - * @function - * @param {object} req the request - * @return {string} the IP Address - */ -function getIPAddress(req) { - if (!isProduction()) { - return "127.0.0.1"; - } - return req.ip; -} - -/** - * Get User-Agent. - * @module visitor - * @function - * @param {object} req the request - * @param {boolean} isShort return short code instead - * @return {string} the User-Agent - */ -function getUserAgent(req, isShort=false) { - const userAgent = req.header("user-agent"); - if (!userAgent) { - return "Unknown"; - } - - if (!isShort) { - return userAgent; - } - - const uaParsed = uaParser(userAgent); - const {name: browserName} = uaParsed.browser; - const {name: osName} = uaParsed.os; - return [browserName, osName].join(" "); -} - -// Export (object) -module.exports = { - getIPAddress, - getUserAgent, -}; diff --git a/src/utils/visitor.ts b/src/utils/visitor.ts new file mode 100644 index 0000000..38d802e --- /dev/null +++ b/src/utils/visitor.ts @@ -0,0 +1,41 @@ +import {isProduction} from "../config"; +import uaParser from "ua-parser-js"; + +/** + * Get IP Address. + * @param {Request} request - The request object. + * @param {any} server - The server object. + * @return {string} The IP address. + */ +export function getIPAddress(request: Request, server: any): string { + if (!isProduction()) { + return "127.0.0.1"; + } + // Bun specific IP retrieval + return server?.requestIP(request)?.address || "unknown"; +} + +/** + * Get User-Agent. + * @param {Headers} headers - The headers object. + * @param {boolean} isShort - Whether to return a short version. + * @return {string} The user agent. + */ +export function getUserAgent( + headers: Headers, + isShort: boolean = false, +): string { + const userAgent = headers.get("user-agent"); + if (!userAgent) { + return "Unknown"; + } + + if (!isShort) { + return userAgent; + } + + const uaParsed = uaParser(userAgent); + const {name: browserName} = uaParsed.browser; + const {name: osName} = uaParsed.os; + return [browserName, osName].filter(Boolean).join(" "); +} diff --git a/src/utils/xara_token.js b/src/utils/xara_token.js deleted file mode 100644 index c06ad39..0000000 --- a/src/utils/xara_token.js +++ /dev/null @@ -1,235 +0,0 @@ -"use strict"; -// Token utils of Sara. - -// Import config -const {getMust} = require("../config"); - -// Import createHmac from crypto -const {createHmac} = require("node:crypto"); - -// Import jsonwebtoken -const {sign, verify} = require("jsonwebtoken"); - -// Import const -const { - APP_NAME: issuerIdentity, -} = require("../init/const"); - -// Import usePublicKey and usePrivateKey -const { - usePublicKey, - usePrivateKey, -} = require("../init/keypair"); - -// Import user model -const User = require("../models/user"); - -// Import token model -const Token = require("../models/token"); - -// Define hmac function - SHA256 -const hmac256hex = (data, key) => - createHmac("sha256", key).update(data).digest("hex"); - -// Define issueOptions -const issueOptions = { - algorithm: "ES256", - expiresIn: "1d", - notBefore: "500ms", - issuer: issuerIdentity, - audience: getMust("SARA_AUDIENCE_URL"), - noTimestamp: false, - mutatePayload: false, -}; - -// Define updateOptions -const updateOptions = { - algorithm: "ES256", - noTimestamp: false, - mutatePayload: false, -}; - -// Define validateOptions -const validateOptions = { - algorithms: ["ES256"], - issuer: issuerIdentity, - audience: getMust("SARA_AUDIENCE_URL"), - complete: true, -}; - -/** - * Issue token - * @module sara_token - * @function - * @async - * @param {object} userData - The user data to generate the token for. - * @return {Promise} - */ -async function issue(userData) { - const user = { - _id: userData._id, - email: userData.email, - nickname: userData.nickname, - avatar_hash: userData.avatar_hash, - roles: userData.roles, - created_at: userData.created_at, - updated_at: userData.updated_at, - }; - - const userId = userData._id; - const userRevision = userData.revision; - - const privateKey = usePrivateKey(); - const guardSecret = getMust("SARA_GUARD_SECRET"); - - const token = new Token({userId}); - const tokenIdPrefix = (await token.save()).id; - const tokenIdSuffix = userRevision; - const tokenId = [ - tokenIdPrefix, - tokenIdSuffix, - ].join("/"); - - const saraTokenPayload = {user, sub: userId, jti: tokenId}; - const saraToken = sign(saraTokenPayload, privateKey, issueOptions); - const guardToken = hmac256hex(tokenId, guardSecret); - - return [saraToken, guardToken].join("|"); -} - -/** - * Update token - * @module sara_token - * @function - * @param {object} token - The token to update. - * @param {object} userData - The user data to update. - * @return {string} - */ -function update(token, userData) { - const user = { - _id: userData._id, - email: userData.email, - nickname: userData.nickname, - avatar_hash: userData.avatar_hash, - roles: userData.roles, - created_at: userData.created_at, - updated_at: userData.updated_at, - }; - - const userId = userData._id.toString(); - const userRevision = userData.revision; - - const publicKey = usePublicKey(); - const privateKey = usePrivateKey(); - const guardSecret = getMust("SARA_GUARD_SECRET"); - - const [ - originalSaraToken, - originalGuardToken, - ] = token.split("|", 2); - - const {payload: saraTokenPayload} = - verify(originalSaraToken, publicKey, validateOptions); - - if (userId !== saraTokenPayload.sub) { - throw new Error("unexpect user id"); - } - - const expectedOriginalGuardToken = hmac256hex( - saraTokenPayload.jti, - guardSecret, - ); - if (originalGuardToken !== expectedOriginalGuardToken) { - throw new Error("unexpect guard token"); - } - - const [ - originalTokenIdPrefix, - originalTokenIdSuffix, - ] = saraTokenPayload.jti.split("/", 2); - const tokenId = [ - originalTokenIdPrefix, - userRevision, - ].join("/"); - - if (userRevision <= parseInt(originalTokenIdSuffix)) { - throw new Error("unexpect user version"); - } - - saraTokenPayload.jti = tokenId; - saraTokenPayload.user = { - ...saraTokenPayload.user, - ...user, - }; - - const saraToken = sign(saraTokenPayload, privateKey, updateOptions); - const guardToken = hmac256hex(tokenId, guardSecret); - - return [saraToken, guardToken].join("|"); -} - -/** - * Validate token - * @module sara_token - * @function - * @async - * @param {string} token - The token to valid. - * @return {Promise} - */ -async function validate(token) { - const publicKey = usePublicKey(); - const result = { - userId: null, - payload: null, - isAborted: false, - }; - - try { - const [saraToken, guardToken] = token.split("|", 2); - const {payload} = verify( - saraToken, publicKey, validateOptions, - ); - - const guardSecret = getMust("SARA_GUARD_SECRET"); - const guardTokenExpected = hmac256hex(payload.jti, guardSecret); - if (guardToken !== guardTokenExpected) { - throw new Error("unexpect guard token"); - } - - const [ - tokenIdPrefix, - tokenIdSuffix, - ] = payload.jti.split("/", 2); - - const tokenState = await Token.findById(tokenIdPrefix); - if (!tokenState) { - throw new Error("token not found"); - } - - const user = await User.findById(tokenState.userId); - if (!user) { - throw new Error("user not found"); - } - - if (user.revision !== parseInt(tokenIdSuffix)) { - throw new Error("user revision mismatch"); - } - - result.userId = payload.sub; - result.payload = { - profile: payload.user, - }; - } catch (e) { - result.isAborted = true; - result.payload = e; - } - - return result; -} - -// Export (object) -module.exports = { - issue, - update, - validate, -}; diff --git a/src/utils/xara_token.ts b/src/utils/xara_token.ts new file mode 100644 index 0000000..4dde16a --- /dev/null +++ b/src/utils/xara_token.ts @@ -0,0 +1,195 @@ +import {getMust} from "../config"; +import {createHmac} from "node:crypto"; +import jwt, {SignOptions, VerifyOptions} from "jsonwebtoken"; +import {APP_NAME as issuerIdentity} from "../init/const"; +import {usePublicKey, usePrivateKey} from "../init/keypair"; +import User from "../models/user"; +import Token from "../models/token"; + +const hmac256hex = (data: string, key: string) => + createHmac("sha256", key).update(data).digest("hex"); + +const issueOptions: SignOptions = { + algorithm: "ES256", + expiresIn: "1d", + notBefore: "500ms", + issuer: issuerIdentity, + audience: getMust("SARA_AUDIENCE_URL"), + noTimestamp: false, +}; + +const updateOptions: SignOptions = { + algorithm: "ES256", + noTimestamp: false, +}; + +const validateOptions: VerifyOptions = { + algorithms: ["ES256"], + issuer: issuerIdentity, + audience: getMust("SARA_AUDIENCE_URL"), + complete: true, +}; + +export interface TokenPayload { + user: any; + sub: string; + jti: string; +} + +/** + * Issue a new token. + * @param {any} userData - The user data. + * @return {Promise} The issued token. + */ +export async function issue(userData: any): Promise { + const user = { + _id: userData._id, + email: userData.email, + nickname: userData.nickname, + avatar_hash: userData.avatar_hash, + roles: userData.roles, + created_at: userData.createdAt, + updated_at: userData.updatedAt, + }; + + const userId = userData._id; + const userRevision = userData.revision; + + const privateKey = usePrivateKey(); + const guardSecret = getMust("SARA_GUARD_SECRET"); + + const token = new Token({userId}); + const tokenIdPrefix = (await token.save()).id; + const tokenIdSuffix = userRevision; + const tokenId = `${tokenIdPrefix}/${tokenIdSuffix}`; + + const saraTokenPayload: TokenPayload = {user, sub: userId, jti: tokenId}; + const saraToken = jwt.sign(saraTokenPayload, privateKey, issueOptions); + const guardToken = hmac256hex(tokenId, guardSecret); + + return `${saraToken}|${guardToken}`; +} + +/** + * Update an existing token. + * @param {string} token - The original token. + * @param {any} userData - The user data. + * @return {string} The updated token. + */ +export function update(token: string, userData: any): string { + const user = { + _id: userData._id, + email: userData.email, + nickname: userData.nickname, + avatar_hash: userData.avatar_hash, + roles: userData.roles, + created_at: userData.createdAt, + updated_at: userData.updatedAt, + }; + + const userId = userData._id.toString(); + const userRevision = userData.revision; + + const publicKey = usePublicKey(); + const privateKey = usePrivateKey(); + const guardSecret = getMust("SARA_GUARD_SECRET"); + + const [originalSaraToken, originalGuardToken] = token.split("|", 2); + + const verified = jwt.verify( + originalSaraToken, + publicKey, + validateOptions, + ) as any; + const saraTokenPayload = verified.payload as TokenPayload; + + if (userId !== saraTokenPayload.sub) { + throw new Error("unexpected user id"); + } + + const expectedOriginalGuardToken = hmac256hex( + saraTokenPayload.jti, + guardSecret, + ); + if (originalGuardToken !== expectedOriginalGuardToken) { + throw new Error("unexpected guard token"); + } + + const [tokenIdPrefix, tokenIdSuffix] = saraTokenPayload.jti.split("/", 2); + const tokenId = `${tokenIdPrefix}/${userRevision}`; + + if (userRevision <= parseInt(tokenIdSuffix)) { + throw new Error("unexpected user version"); + } + + saraTokenPayload.jti = tokenId; + saraTokenPayload.user = { + ...saraTokenPayload.user, + ...user, + }; + + const saraToken = jwt.sign(saraTokenPayload, privateKey, updateOptions); + const guardToken = hmac256hex(tokenId, guardSecret); + + return `${saraToken}|${guardToken}`; +} + +/** + * Validate a token. + * @param {string} token - The token to validate. + * @return {Promise} The validation result. + */ +export async function validate(token: string) { + const publicKey = usePublicKey(); + const result: { + userId: string | null; + payload: any; + isAborted: boolean; + } = { + userId: null, + payload: null, + isAborted: false, + }; + + try { + const [saraToken, guardToken] = token.split("|", 2); + const verified = jwt.verify( + saraToken, + publicKey, + validateOptions, + ) as any; + const payload = verified.payload as TokenPayload; + + const guardSecret = getMust("SARA_GUARD_SECRET"); + const guardTokenExpected = hmac256hex(payload.jti, guardSecret); + if (guardToken !== guardTokenExpected) { + throw new Error("unexpected guard token"); + } + + const [tokenIdPrefix, tokenIdSuffix] = payload.jti.split("/", 2); + + const tokenState = await Token.findById(tokenIdPrefix); + if (!tokenState) { + throw new Error("token not found"); + } + + const user = await User.findById(tokenState.userId); + if (!user) { + throw new Error("user not found"); + } + + if (user.revision !== parseInt(tokenIdSuffix)) { + throw new Error("user revision mismatch"); + } + + result.userId = payload.sub; + result.payload = { + profile: payload.user, + }; + } catch (e) { + result.isAborted = true; + result.payload = e; + } + + return result; +} diff --git a/test/elysia_smoke.test.ts b/test/elysia_smoke.test.ts new file mode 100644 index 0000000..3ae2cee --- /dev/null +++ b/test/elysia_smoke.test.ts @@ -0,0 +1,27 @@ +import {describe, expect, it} from "bun:test"; +import {app} from "../src/index"; + +describe("Sara RECV Elysia Smoke Test", () => { + it("should return 200 for robots.txt", async () => { + const response = await app.handle( + new Request("http://localhost/robots.txt"), + ); + expect(response.status).toBe(200); + expect(await response.text()).toContain("User-agent: *"); + }); + + it("should redirect for /", async () => { + const response = await app.handle( + new Request("http://localhost/"), + ); + expect(response.status).toBeGreaterThanOrEqual(301); + expect(response.status).toBeLessThanOrEqual(302); + }); + + it("should have swagger documentation", async () => { + const response = await app.handle( + new Request("http://localhost/swagger"), + ); + expect(response.status).toBe(200); + }); +}); diff --git a/test/integration.test.ts b/test/integration.test.ts new file mode 100644 index 0000000..1aed601 --- /dev/null +++ b/test/integration.test.ts @@ -0,0 +1,239 @@ +import {describe, expect, it, beforeAll} from "bun:test"; +import {nanoid} from "nanoid"; + +// ============================================================ +// Integration Tests: /users & /tokens API +// Requires: MongoDB running at MONGODB_URI in .env +// ============================================================ + +// Shared state across test steps +const state: { + fakeUser: { nickname: string; email: string } | null; + registerSessionId: string | null; + registerCode: string | null; + xaraToken: string | null; + loginSessionId: string | null; + loginCode: string | null; +} = { + fakeUser: null, + registerSessionId: null, + registerCode: null, + xaraToken: null, + loginSessionId: null, + loginCode: null, +}; + +const userAgent = "bun-test/1.0 sara-recv-integration"; + +/** + * Generate a fake user for testing + * @return {object} The fake user object. + */ +function generateFakeUser() { + const id = nanoid(8).toLowerCase(); + return { + nickname: `TestUser_${id}`, + email: `test_${id}@integration.local`, + }; +} + +// ============================================================ +// Setup +// ============================================================ + +let app: any; +let cache: any; +let HEADER_REFRESH_TOKEN: string; + +beforeAll(async () => { + // Dynamic import to ensure env is loaded first + const {app: _app} = await import("../src/index"); + const {prepare: prepareDatabase} = await import("../src/init/database"); + await prepareDatabase(); + const {useCache} = await import("../src/init/cache"); + const constants = await import("../src/init/const"); + + app = _app; + cache = useCache(); + HEADER_REFRESH_TOKEN = constants.HEADER_REFRESH_TOKEN; + + // Give DB connection a moment + await new Promise((r) => setTimeout(r, 500)); +}); + +// ============================================================ +// /users - Registration flow +// ============================================================ + +describe("/users - register", () => { + it("Step 1: POST /users/ → should return 201", async () => { + state.fakeUser = generateFakeUser(); + + const res = await app.handle(new Request("http://localhost/users/", { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify(state.fakeUser), + })); + + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.session_id).toBeTruthy(); + + state.registerSessionId = body.session_id; + state.registerCode = cache.get("_testing_code"); + + console.log( + "[register] session_id:", + state.registerSessionId, + "code:", + state.registerCode, + ); + }); + + it("Step 2: PATCH /users/ → should return 201", async () => { + expect(state.registerSessionId).toBeTruthy(); + expect(state.registerCode).toBeTruthy(); + + const res = await app.handle(new Request("http://localhost/users/", { + method: "PATCH", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify({ + code: state.registerCode, + session_id: state.registerSessionId, + }), + })); + + expect(res.status).toBe(201); + const refreshToken = res.headers.get(HEADER_REFRESH_TOKEN); + expect(refreshToken).toBeTruthy(); + state.xaraToken = refreshToken; + + console.log( + "[register verify] xaraToken prefix:", + state.xaraToken?.slice(0, 30) + "...", + ); + }); + + it("Step 3: POST /users/ with same email → 409 Conflict", async () => { + const res = await app.handle(new Request("http://localhost/users/", { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify(state.fakeUser!), + })); + + expect(res.status).toBe(409); + }); +}); + +// ============================================================ +// /tokens - Login flow +// ============================================================ + +describe("/tokens - login", () => { + it("Step 1: POST /tokens/ → 201 with session_id", async () => { + expect(state.fakeUser).toBeTruthy(); + + const res = await app.handle(new Request("http://localhost/tokens/", { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify({email: state.fakeUser!.email}), + })); + + expect(res.status).toBe(201); + const body = await res.json(); + expect(body.session_id).toBeTruthy(); + + state.loginSessionId = body.session_id; + state.loginCode = cache.get("_testing_code"); + + console.log( + "[login] session_id:", + state.loginSessionId, + "code:", + state.loginCode, + ); + }); + + it("Step 2: PATCH /tokens/ → return 201 and header", async () => { + expect(state.loginSessionId).toBeTruthy(); + expect(state.loginCode).toBeTruthy(); + + const res = await app.handle(new Request("http://localhost/tokens/", { + method: "PATCH", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify({ + code: state.loginCode, + session_id: state.loginSessionId, + }), + })); + + expect(res.status).toBe(201); + const refreshToken = res.headers.get(HEADER_REFRESH_TOKEN); + expect(refreshToken).toBeTruthy(); + + console.log( + "[login verify] xaraToken prefix:", + refreshToken?.slice(0, 30) + "...", + ); + }); + + it("Step 3: POST /tokens/ with non-existent email → 404", async () => { + const res = await app.handle(new Request("http://localhost/tokens/", { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": userAgent, + }, + body: JSON.stringify({email: "nobody@does.not.exist.local"}), + })); + + expect(res.status).toBe(404); + }); +}); + +// ============================================================ +// /users/me - Authenticated profile access +// ============================================================ + +describe("/users/me - authenticated profile", () => { + it("GET /users/me with valid XARA token → 200", async () => { + expect(state.xaraToken).toBeTruthy(); + + const res = await app.handle(new Request("http://localhost/users/me", { + method: "GET", + headers: { + "authorization": `XARA ${state.xaraToken}`, + }, + })); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.profile).toBeTruthy(); + expect(body.profile.email).toBe(state.fakeUser!.email); + expect(body.profile.nickname).toBe(state.fakeUser!.nickname); + expect(body.profile.avatar_hash).toBeTruthy(); + }); + + it("GET /users/me without token → return 401", async () => { + const res = await app.handle(new Request("http://localhost/users/me", { + method: "GET", + })); + + expect(res.status).toBe(401); + }); +}); diff --git a/test/kernel/init.js b/test/kernel/init.js deleted file mode 100644 index 8323543..0000000 --- a/test/kernel/init.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -const {runLoader} = require("../../src/config"); -runLoader(); - -process.env.NODE_ENV = "testing"; - -exports.USER_AGENT = - "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14"; diff --git a/test/kernel/utils.js b/test/kernel/utils.js deleted file mode 100644 index 78512b5..0000000 --- a/test/kernel/utils.js +++ /dev/null @@ -1,129 +0,0 @@ -"use strict"; - -const { - USER_AGENT: userAgent, -} = require("./init"); - -const {isProduction} = require("../../src/config"); - -const {nanoid: generateNanoId} = require("nanoid"); -const request = require("supertest"); - -const {StatusCodes} = require("http-status-codes"); - -const {useApp} = require("../../src/init/express"); -const {useCache} = require("../../src/init/cache"); - -/** - * Print message with testing notification. - * @param {any} messages - */ -function print(...messages) { - if (isProduction()) return; - const timestamp = new Date().toString(); - console.info( - "---\n", - "[!] *Test Message*\n", - `[!] ${timestamp}\n`, - ...messages, - ); -} - -/** - * Create a helper to merge base URL and path. - * @param {string} baseUrl - The base URL - * @return {function(string)} - */ -function urlGlue(baseUrl) { - return (path) => baseUrl + path; -} - -/** - * Return a function to run a task with arguments. - * @param {function} task - * @return {any} - */ -function toTest(task, ...args) { - return async function() { - try { - await task(...args); - } catch (error) { - console.error(error); - throw error; - } - }; -} - -/** - * Run prepare handlers. - * @param {function[]} handlers - * @return {function} - */ -function toPrepare(...handlers) { - return async function() { - try { - const promises = handlers.map((c) => c()); - await Promise.all(promises); - } catch (error) { - console.error(error); - throw error; - } - }; -} - -/** - * Generate fake user of the testing session. - * @return {object} - */ -function generateFakeUser() { - const userSerial = generateNanoId(); - const userId = `testing_${userSerial}`; - - const nickname = `Fake User - ${userId}`; - const email = `fake_user_${userId}@web-tech-tw.github.io`; - - return {nickname, email}; -} - -/** - * Do register, no matter if the user already exists - * @param {object} userData the user - * @return {Promise} - */ -async function registerFakeUser(userData) { - const app = useApp(); - const cache = useCache(); - - const verifyResponse = await request(app). - post("/users"). - set("user-agent", userAgent). - type("json"). - send(userData). - expect(StatusCodes.CREATED). - expect("content-type", /json/); - - const {session_id: sessionId} = verifyResponse.body; - const sessionCode = cache.take("_testing_code"); - - const statusResponse = await request(app). - patch("/users"). - set("user-agent", userAgent). - type("json"). - send({ - session_id: sessionId, - code: sessionCode, - }). - expect(StatusCodes.CREATED). - expect("content-type", /plain/); - - return statusResponse.status === StatusCodes.CREATED; -} - -module.exports = { - print, - urlGlue, - toTest, - toPrepare, - generateFakeUser, - registerFakeUser, -}; diff --git a/test/tokens.js b/test/tokens.js deleted file mode 100644 index 4ebb3f3..0000000 --- a/test/tokens.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; - -const { - USER_AGENT: userAgent, -} = require("./kernel/init"); - -const { - print, - urlGlue, - toTest, - toPrepare, - generateFakeUser, - registerFakeUser, -} = require("./kernel/utils"); - -const request = require("supertest"); -const {step} = require("mocha-steps"); - -const {StatusCodes} = require("http-status-codes"); - -const { - HEADER_REFRESH_TOKEN: headerRefreshToken, -} = require("../src/init/const"); - -const {useApp} = require("../src/init/express"); -const {useCache} = require("../src/init/cache"); - -const { - prepare: prepareDatabase, -} = require("../src/init/database"); - -// Initialize tests -const app = useApp(); -const cache = useCache(); - -const routerDispatcher = require("../src/routes/index"); -const to = urlGlue("/tokens"); -routerDispatcher.load(); - -// Define tests -describe("/tokens", function() { - const fakeUser = generateFakeUser(); - const fakeUserRegister = () => registerFakeUser(fakeUser); - - before(toPrepare( - prepareDatabase, - fakeUserRegister, - )); - - step("login", toTest(async function() { - const response = await request(app). - post(to("/")). - set("user-agent", userAgent). - type("json"). - send({email: fakeUser.email}). - expect(StatusCodes.CREATED). - expect("content-type", /json/); - - const { - session_id: sessionId, - } = response.body; - - const code = cache.get("_testing_code"); - cache.set("_testing_session_id", sessionId); - - print(code, response.body); - })); - - step("login verify", toTest(async function() { - const code = cache.take("_testing_code"); - const sessionId = cache.take("_testing_session_id"); - - const response = await request(app). - patch(to("/")). - set("user-agent", userAgent). - type("json"). - send({ - code: code, - session_id: sessionId, - }). - expect(StatusCodes.CREATED). - expect("content-type", /plain/); - - const { - [headerRefreshToken]: refreshToken, - } = response.headers; - - print(refreshToken); - })); -}); diff --git a/test/unit.test.ts b/test/unit.test.ts new file mode 100644 index 0000000..98d4eac --- /dev/null +++ b/test/unit.test.ts @@ -0,0 +1,183 @@ +import {describe, expect, it} from "bun:test"; +import {nanoid} from "nanoid"; + +// ============================================================ +// Unit Tests: Core utilities (no DB required) +// ============================================================ + +describe("native.ts - sha256hex", () => { + it("should produce a consistent 64-char hex string", async () => { + const {sha256hex} = await import("../src/utils/native"); + const result = sha256hex("test@example.com"); + expect(result).toHaveLength(64); + expect(result).toMatch(/^[a-f0-9]+$/); + }); + + it("should be deterministic for the same input", async () => { + const {sha256hex} = await import("../src/utils/native"); + expect(sha256hex("hello")).toBe(sha256hex("hello")); + }); + + it("should differ for different inputs", async () => { + const {sha256hex} = await import("../src/utils/native"); + expect(sha256hex("alice@test.com")).not.toBe(sha256hex("bob@test.com")); + }); +}); + +describe("native.ts - generateRandomCode", () => { + it("should produce a string of the correct length", async () => { + const {generateRandomCode} = await import("../src/utils/native"); + expect(generateRandomCode(6)).toHaveLength(6); + expect(generateRandomCode(7)).toHaveLength(7); + expect(generateRandomCode(8)).toHaveLength(8); + }); + + it("should be numeric only", async () => { + const {generateRandomCode} = await import("../src/utils/native"); + expect(generateRandomCode(6)).toMatch(/^\d+$/); + }); +}); + +describe("native.ts - isObjectPropExists", () => { + it("should return true for existing own properties", async () => { + const {isObjectPropExists} = await import("../src/utils/native"); + expect(isObjectPropExists({foo: 1}, "foo")).toBe(true); + }); + + it("should return false for missing properties", async () => { + const {isObjectPropExists} = await import("../src/utils/native"); + expect(isObjectPropExists({}, "bar")).toBe(false); + }); + + /** + * Test inherited properties + */ + it("should return false for inherited properties", async () => { + const {isObjectPropExists} = await import("../src/utils/native"); + expect(isObjectPropExists({}, "toString")).toBe(false); + }); +}); + +// ============================================================ +// Unit Tests: code_session.ts (in-memory, no DB) +// ============================================================ + +describe("code_session.ts - createOne / getOne", () => { + it("should create a session and retrieve it", async () => { + const {createOne, getOne} = await import("../src/utils/code_session"); + const metadata = {userId: "u1", email: "a@b.com"}; + const {code, sessionId} = createOne( + "create_token", + metadata, + 6, + 60, + ); + + expect(code).toHaveLength(6); + expect(sessionId).toBeTruthy(); + + const result = getOne("create_token", sessionId, code); + expect(result).not.toBeNull(); + expect(result!.userId).toBe("u1"); + expect(result!.email).toBe("a@b.com"); + }); + + it("should return null for wrong code", async () => { + const {createOne, getOne} = await import("../src/utils/code_session"); + const {sessionId} = createOne("test_type", {x: 1}, 6, 60); + const result = getOne("test_type", sessionId, "000000"); + expect(result).toBeNull(); + }); + + it("should delete the session after deleteIt()", async () => { + const {createOne, getOne} = await import("../src/utils/code_session"); + const {code, sessionId} = createOne("test_del", {x: 1}, 6, 60); + const session = getOne("test_del", sessionId, code); + expect(session).not.toBeNull(); + session!.deleteIt(); + expect(getOne("test_del", sessionId, code)).toBeNull(); + }); + + it("should support different session types", async () => { + const {createOne, getOne} = await import("../src/utils/code_session"); + const {code: c1, sessionId: s1} = createOne( + "type_a", + {val: "a"}, + 6, + 60, + ); + const {code: c2, sessionId: s2} = createOne( + "type_b", + {val: "b"}, + 6, + 60, + ); + + expect(getOne("type_a", s1, c1)!.val).toBe("a"); + expect(getOne("type_b", s2, c2)!.val).toBe("b"); + // Cross-type access should fail + expect(getOne("type_b", s1, c1)).toBeNull(); + }); +}); + +// ============================================================ +// Unit Tests: passkey_session.ts (in-memory, no DB) +// ============================================================ + +describe("passkey_session.ts - createOne / getOne", () => { + it("should create and retrieve a passkey session", async () => { + const { + createOne, + getOne, + } = await import("../src/utils/passkey_session"); + const {sessionId} = createOne("create_token", { + userId: "u1", + challenge: "abc123challenge", + }, 60); + + expect(sessionId).toBeTruthy(); + const result = getOne("create_token", sessionId); + expect(result).not.toBeNull(); + expect(result!.challenge).toBe("abc123challenge"); + }); + + it("should return null for unknown sessionId", async () => { + const {getOne} = await import("../src/utils/passkey_session"); + expect(getOne("create_token", nanoid())).toBeNull(); + }); +}); + +// ============================================================ +// Unit Tests: test_token.ts +// ============================================================ + +describe("test_token.ts - issue / validate roundtrip", () => { + it("should issue a base64 token and validate it back", async () => { + const testToken = await import("../src/utils/test_token"); + const token = testToken.issue(); + expect(typeof token).toBe("string"); + + const result = testToken.validate(token); + expect(result.isAborted).toBe(false); + expect(result.userId).toBeTruthy(); + expect(result.payload.profile._id).toBeTruthy(); + }); + + it("should abort on an invalid/tampered token", async () => { + const testToken = await import("../src/utils/test_token"); + const result = testToken.validate("!!!not_valid_base64!!!"); + expect(result.isAborted).toBe(true); + }); + + it("should return different token from different user", async () => { + const testToken = await import("../src/utils/test_token"); + const fakeUser = { + ...testToken.newProfile(), + _id: "different_id", + email: "other@example.com", + }; + const t1 = testToken.issue(); + const t2 = testToken.issue(fakeUser); + expect(t1).not.toBe(t2); + }); +}); diff --git a/test/users.js b/test/users.js deleted file mode 100644 index dfedd8a..0000000 --- a/test/users.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; - -const { - USER_AGENT: userAgent, -} = require("./kernel/init"); - -const { - print, - urlGlue, - toTest, - toPrepare, - generateFakeUser, -} = require("./kernel/utils"); - -const request = require("supertest"); -const {step} = require("mocha-steps"); - -const {StatusCodes} = require("http-status-codes"); - -const { - HEADER_REFRESH_TOKEN: headerRefreshToken, -} = require("../src/init/const"); - -const {useApp} = require("../src/init/express"); -const {useCache} = require("../src/init/cache"); - -const { - prepare: prepareDatabase, -} = require("../src/init/database"); - -// Initialize tests -const app = useApp(); -const cache = useCache(); - -const routerDispatcher = require("../src/routes/index"); -const to = urlGlue("/users"); -routerDispatcher.load(); - -// Define tests -describe("/users", function() { - const fakeUser = generateFakeUser(); - - before(toPrepare( - prepareDatabase, - )); - - step("register", toTest(async function() { - const response = await request(app). - post(to("/")). - set("user-agent", userAgent). - type("json"). - send(fakeUser). - expect(StatusCodes.CREATED). - expect("content-type", /json/); - - const { - session_id: sessionId, - } = response.body; - - const code = cache.get("_testing_code"); - cache.set("_testing_session_id", sessionId); - - print(code, response.body); - })); - - step("verify register", toTest(async function() { - const code = cache.take("_testing_code"); - const sessionId = cache.take("_testing_session_id"); - - const response = await request(app). - patch(to("/")). - set("user-agent", userAgent). - type("json"). - send({ - code: code, - session_id: sessionId, - }). - expect(StatusCodes.CREATED). - expect("content-type", /plain/); - - const { - [headerRefreshToken]: refreshToken, - } = response.headers; - - print(refreshToken); - })); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..99a54dc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun-types"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +}