From 0cab96d7e9c28f8605c147555a36791bb92b8431 Mon Sep 17 00:00:00 2001 From: Michael McQuade Date: Thu, 1 Oct 2026 17:42:20 -0500 Subject: [PATCH] fix: enforce route access rules consistently and add CI and npm releases Access control - apply roles and validation to method exports regardless of case - pass a real error to next when a handler throws a falsy value, 'route' or 'router' - reject roles that are not an array of role names, roles without a handler, non-boolean guestAccess/ensureAdmin, and guestAccess combined with ensureAdmin - do not return error messages and stack traces from defaultErrorHandler in production Routing - mount static routes before param routes for every segment - fix route paths for files that start with index, and the extension match - ignore declaration files, resolve relative routesPath, and do not treat the path to the routes as a glob pattern Packaging and CI - refresh the lockfile to clear known advisories, move to TypeScript 5.9 - publish only the library build, set the license to Apache-2.0 - add CI, CodeQL, dependency review, PR title check and dependabot - add release-please, publishing to npm with trusted publishing --- .github/.release-please-manifest.json | 3 + .github/dependabot.yml | 17 + .github/release-please-config.json | 12 + .github/workflows/ci.yml | 51 ++ .github/workflows/codeql.yml | 38 + .github/workflows/pr-title.yml | 32 + .github/workflows/release.yml | 58 ++ .gitignore | 4 +- .npmignore | 1 - README.md | 34 + package.json | 37 +- pnpm-lock.yaml | 802 +++++++++--------- src/index.test.ts | 105 ++- src/index.ts | 248 ++++-- src/middleware/errors.test.ts | 45 + src/middleware/errors.ts | 28 +- src/middleware/roles.ts | 7 +- src/testfixtures/invalid/guestAdmin.ts | 8 + src/testfixtures/invalid/notAHandler.ts | 1 + src/testfixtures/invalid/rolesNotArray.ts | 9 + .../invalid/rolesWithoutHandler.ts | 9 + src/testfixtures/lowercase.ts | 22 + src/testfixtures/ordering/_hidden.ts | 5 + src/testfixtures/ordering/index.ts | 5 + src/testfixtures/ordering/indexes.d.ts | 2 + src/testfixtures/ordering/indexes.ts | 5 + src/testfixtures/ordering/users/2fa.ts | 5 + src/testfixtures/ordering/users/:id.ts | 5 + src/testfixtures/ordering/users/me.ts | 5 + tsconfig.build.json | 10 + 30 files changed, 1120 insertions(+), 493 deletions(-) create mode 100644 .github/.release-please-manifest.json create mode 100644 .github/dependabot.yml create mode 100644 .github/release-please-config.json create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .npmignore create mode 100644 src/testfixtures/invalid/guestAdmin.ts create mode 100644 src/testfixtures/invalid/notAHandler.ts create mode 100644 src/testfixtures/invalid/rolesNotArray.ts create mode 100644 src/testfixtures/invalid/rolesWithoutHandler.ts create mode 100644 src/testfixtures/lowercase.ts create mode 100644 src/testfixtures/ordering/_hidden.ts create mode 100644 src/testfixtures/ordering/index.ts create mode 100644 src/testfixtures/ordering/indexes.d.ts create mode 100644 src/testfixtures/ordering/indexes.ts create mode 100644 src/testfixtures/ordering/users/2fa.ts create mode 100644 src/testfixtures/ordering/users/:id.ts create mode 100644 src/testfixtures/ordering/users/me.ts create mode 100644 tsconfig.build.json diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json new file mode 100644 index 0000000..949ce4c --- /dev/null +++ b/.github/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.2.2" +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..81be901 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: { interval: weekly } + # wait for a release to age before adopting it, security updates are not delayed + cooldown: { default-days: 7 } + commit-message: { prefix: "build(deps)" } + groups: + js: { patterns: ["*"], update-types: [minor, patch] } + - package-ecosystem: github-actions + directory: / + schedule: { interval: weekly } + cooldown: { default-days: 7 } + commit-message: { prefix: "ci(deps)" } + groups: + actions: { patterns: ["*"] } diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 0000000..a1e85cf --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "dbb1f33f9abdf1a09bf36a1172ea7e7aaf366289", + "bump-minor-pre-major": true, + "packages": { + ".": { + "release-type": "node", + "include-component-in-tag": false, + "pull-request-title-pattern": "chore: release ${version}" + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e88e491 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node-version: [22, 24, 26] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + # dependency install scripts are never run + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build + - name: Verify the package contents + run: npm pack --dry-run --ignore-scripts + + # Fails a pull request that adds a dependency with a known vulnerability. + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..30fa180 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,38 @@ +name: CodeQL + +on: + pull_request: + push: + branches: [master] + schedule: + - cron: '23 6 * * 1' + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [javascript-typescript, actions] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + languages: ${{ matrix.language }} + build-mode: none + - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..e0c357c --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,32 @@ +name: PR title + +# PRs are squash-merged, so the PR title becomes the commit on master and +# must follow Conventional Commits for release-please to version it. +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + conventional-commit: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6c8786c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release + +# Opens a release PR from the conventional-commit titles on master. Merging it +# tags the version and creates the GitHub release, which is then published to npm. +on: + push: + branches: [master] + +permissions: {} + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write + issues: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + id: release + with: + config-file: .github/release-please-config.json + manifest-file: .github/.release-please-manifest.json + + publish: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + # The npm trusted publisher is restricted to this environment. + environment: npm + permissions: + contents: read + # npm trusted publishing and provenance, there is no npm token to leak. + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + # No dependency cache here, so that a poisoned cache cannot reach a release. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 26 + registry-url: 'https://registry.npmjs.org' + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm typecheck + - run: pnpm test + - run: npm publish diff --git a/.gitignore b/.gitignore index b418ef5..3310fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ id_rsa_userhost !.yarn/sdks !.yarn/versions # yarn v1 node_modules -node_modules/ \ No newline at end of file +node_modules/ +# registry credentials +.npmrc diff --git a/.npmignore b/.npmignore deleted file mode 100644 index e831038..0000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -src \ No newline at end of file diff --git a/README.md b/README.md index 29857c0..bd6ccdf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # fsrouter Filesystem based router for express.js +# Installation + +```sh +npm install @parallelworks/fsrouter +``` + # Usage ```typescript @@ -8,6 +14,7 @@ import express, { RequestHandler } from 'express' import { initFsRouting } from '@parallelworks/fsrouter' import path from 'path' const port = 3000 +const app = express() const routesPath = path.join(__dirname, '_routes') @@ -81,6 +88,29 @@ To make a route public, add the following to the endpoint file: export const guestAccess = true ``` +A route cannot be both public and admin only, so exporting both is an error. + +To restrict a method to users with certain roles, export the roles for each method. The user needs at least one of the roles listed: + +```typescript +export const roles = { + POST: ['org:admin', 'org:settings'], +} +``` + +The roles of the user who made the request come from the `rolesResolver` that is passed to `initFsRouting`: + +```typescript +const router = await initFsRouting({ + ensureAdmin, + ensureAuthenticated, + routesPath, + rolesResolver: req => req.user.roles, +}) +``` + +The router refuses to start when these exports cannot be applied, e.g. when roles are set for a method that the file does not export a handler for. + # **Validation** When you’re using the @parallelworks/fsrouter package, validation is handled with a special export. The [full JSON-schema spec](https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.3.2.1) is available in these objects, so we can create some advanced validation if necessary. @@ -129,3 +159,7 @@ export const GET: Endpoint<{jid: string}, {}, {}, {}> = (req, res) => { ... } ``` + +# **Releasing** + +Pull requests are squash-merged, and their titles follow [Conventional Commits](https://www.conventionalcommits.org). From those titles, [release-please](https://github.com/googleapis/release-please) keeps a release pull request open. Merging the release pull request tags the version and publishes it to [npm](https://www.npmjs.com/package/@parallelworks/fsrouter). diff --git a/package.json b/package.json index 4f2b946..5204273 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,48 @@ { "name": "@parallelworks/fsrouter", "type": "commonjs", - "version": "0.1.8", + "version": "0.2.2", "description": "Filesystem based router for Express.js", "author": "Michael McQuade", + "license": "Apache-2.0", "main": "build/index.js", "types": "build/index.d.ts", - "repository": "git@github.com:parallelworks/fsrouter.git", + "files": [ + "build" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/parallelworks/fsrouter.git" + }, + "homepage": "https://github.com/parallelworks/fsrouter#readme", + "bugs": "https://github.com/parallelworks/fsrouter/issues", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "packageManager": "pnpm@10.33.4", "scripts": { - "compile": "tsc", - "test": "jest" + "clean": "node -e \"require('fs').rmSync('build', { recursive: true, force: true })\"", + "build": "pnpm clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "jest", + "prepack": "pnpm build" }, - "license": "UNLICENSED", "devDependencies": { "@types/express": "^4.17.25", - "@types/express-serve-static-core": "^4.19.8", + "@types/express-serve-static-core": "^4.19.9", "@types/jest": "^29.5.14", - "express": "^4.22.1", + "@types/node": "^22.20.4", + "express": "^4.22.3", "jest": "^29.7.0", - "ts-jest": "^29.4.6", - "typescript": "^4.9.5" + "ts-jest": "^29.4.14", + "typescript": "^5.9.3" }, "peerDependencies": { "express": "^4.17.1" }, "dependencies": { - "ajv": "^8.18.0", + "ajv": "^8.20.0", "ajv-errors": "^3.0.0", "glob": "^9.3.5", "json-schema-to-ts": "^2.12.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce33ec2..d22a2b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: ajv: - specifier: ^8.18.0 - version: 8.18.0 + specifier: ^8.20.0 + version: 8.20.0 ajv-errors: specifier: ^3.0.0 - version: 3.0.0(ajv@8.18.0) + version: 3.0.0(ajv@8.20.0) glob: specifier: ^9.3.5 version: 9.3.5 @@ -25,82 +25,85 @@ importers: specifier: ^4.17.25 version: 4.17.25 '@types/express-serve-static-core': - specifier: ^4.19.8 - version: 4.19.8 + specifier: ^4.19.9 + version: 4.19.9 '@types/jest': specifier: ^29.5.14 version: 29.5.14 + '@types/node': + specifier: ^22.20.4 + version: 22.20.4 express: - specifier: ^4.22.1 - version: 4.22.1 + specifier: ^4.22.3 + version: 4.22.3 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@25.3.3) + version: 29.7.0(@types/node@22.20.4) ts-jest: - specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.3.3))(typescript@4.9.5) + specifier: ^29.4.14 + version: 29.4.14(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.4))(typescript@5.9.3) typescript: - specifier: ^4.9.5 - version: 4.9.5 + specifier: ^5.9.3 + version: 5.9.3 packages: - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true @@ -125,8 +128,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -141,8 +144,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -189,26 +192,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -218,8 +221,8 @@ packages: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} '@jest/console@29.7.0': @@ -298,14 +301,14 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sinclair/typebox@0.27.12': + resolution: {integrity: sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -331,8 +334,8 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + '@types/express-serve-static-core@4.19.9': + resolution: {integrity: sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==} '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} @@ -361,11 +364,11 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@25.3.3': - resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} - '@types/qs@6.14.0': - resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -397,8 +400,8 @@ packages: peerDependencies: ajv: ^8.0.1 - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -454,27 +457,27 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.11.27: + resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==} engines: {node: '>=6.0.0'} hasBin: true - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + body-parser@1.20.8: + resolution: {integrity: sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -512,8 +515,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001776: - resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + caniuse-lite@1.0.30001814: + resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -630,8 +633,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron-to-chromium@1.5.443: + resolution: {integrity: sha512-TDJG36L9A3CWWwZ97HKaE+Iz1sW80pNp6sAU0owzCIV3Zc9eVyRqZK8Vz6NKuMIU8WjP0pIkoqCa0VRf/yFD0Q==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -655,8 +658,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} escalade@3.2.0: @@ -691,8 +694,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.22.3: + resolution: {integrity: sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==} engines: {node: '>= 0.10.0'} fast-deep-equal@3.1.3: @@ -701,8 +704,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -778,8 +781,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -791,8 +794,8 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} html-escaper@2.0.2: @@ -833,8 +836,8 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} engines: {node: '>= 0.4'} is-fullwidth-code-point@3.0.0: @@ -1012,8 +1015,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true jsesc@3.1.0: @@ -1146,8 +1149,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -1215,14 +1219,14 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} pirates@4.0.7: @@ -1241,15 +1245,15 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} range-parser@1.2.1: @@ -1283,8 +1287,8 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true @@ -1298,8 +1302,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -1322,8 +1326,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -1334,8 +1338,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} signal-exit@3.0.7: @@ -1420,19 +1424,19 @@ packages: ts-algebra@1.2.2: resolution: {integrity: sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==} - ts-jest@29.4.6: - resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + ts-jest@29.4.14: + resolution: {integrity: sha512-+943G6wCX5E3VpzTQhBGBFntCmdT+QAkTvUlp03edTONFBvqskuXPhpyX16xdUotO4P5HQKyppldSWjocoltLQ==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' + '@babel/core': '>=7.0.0-beta.0 <9' '@jest/transform': ^29.0.0 || ^30.0.0 '@jest/types': ^29.0.0 || ^30.0.0 babel-jest: ^29.0.0 || ^30.0.0 esbuild: '*' jest: ^29.0.0 || ^30.0.0 jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' + typescript: '>=4.3 <7' peerDependenciesMeta: '@babel/core': optional: true @@ -1463,9 +1467,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} hasBin: true uglify-js@3.19.3: @@ -1473,15 +1477,15 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1531,8 +1535,8 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yocto-queue@0.1.0: @@ -1541,25 +1545,25 @@ packages: snapshots: - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@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 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -1569,166 +1573,166 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.28.6': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.0': + '@babel/parser@7.29.9': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.7': {} - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.8': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.8': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@0.2.3': {} @@ -1737,15 +1741,15 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.2 + js-yaml: 3.15.2 resolve-from: 5.0.0 - '@istanbuljs/schema@0.1.3': {} + '@istanbuljs/schema@0.1.6': {} '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1758,14 +1762,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@25.3.3) + jest-config: 29.7.0(@types/node@22.20.4) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1790,7 +1794,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1808,7 +1812,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 25.3.3 + '@types/node': 22.20.4 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1830,7 +1834,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -1853,7 +1857,7 @@ snapshots: '@jest/schemas@29.6.3': dependencies: - '@sinclair/typebox': 0.27.10 + '@sinclair/typebox': 0.27.12 '@jest/source-map@29.6.3': dependencies: @@ -1877,7 +1881,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 @@ -1900,13 +1904,13 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/yargs': 17.0.35 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -1916,14 +1920,14 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - '@sinclair/typebox@0.27.10': {} + '@sinclair/typebox@0.27.12': {} '@sinonjs/commons@3.0.1': dependencies: @@ -1935,51 +1939,51 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/connect@3.4.38': dependencies: - '@types/node': 25.3.3 + '@types/node': 22.20.4 - '@types/express-serve-static-core@4.19.8': + '@types/express-serve-static-core@4.19.9': dependencies: - '@types/node': 25.3.3 - '@types/qs': 6.14.0 + '@types/node': 22.20.4 + '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 '@types/express@4.17.25': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.14.0 + '@types/express-serve-static-core': 4.19.9 + '@types/qs': 6.15.1 '@types/serve-static': 1.15.10 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/http-errors@2.0.5': {} @@ -2002,27 +2006,27 @@ snapshots: '@types/mime@1.3.5': {} - '@types/node@25.3.3': + '@types/node@22.20.4': dependencies: - undici-types: 7.18.2 + undici-types: 6.21.0 - '@types/qs@6.14.0': {} + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/send@1.2.1': dependencies: - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 25.3.3 + '@types/node': 22.20.4 '@types/send': 0.17.6 '@types/stack-utils@2.0.3': {} @@ -2038,14 +2042,14 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - ajv-errors@3.0.0(ajv@8.18.0): + ajv-errors@3.0.0(ajv@8.20.0): dependencies: - ajv: 8.18.0 + ajv: 8.20.0 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -2064,7 +2068,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 argparse@1.0.10: dependencies: @@ -2072,13 +2076,13 @@ snapshots: array-flatten@1.1.1: {} - babel-jest@29.7.0(@babel/core@7.29.0): + babel-jest@29.7.0(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) + babel-preset-jest: 29.6.3(@babel/core@7.29.7) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -2087,9 +2091,9 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 istanbul-lib-instrument: 5.2.1 test-exclude: 6.0.0 transitivePeerDependencies: @@ -2097,41 +2101,41 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@29.6.3(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) balanced-match@1.0.2: {} - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.11.27: {} - body-parser@1.20.4: + body-parser@1.20.8: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -2141,19 +2145,19 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.16.0 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - brace-expansion@1.1.12: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.1.7: dependencies: balanced-match: 1.0.2 @@ -2161,13 +2165,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.29.3: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001776 - electron-to-chromium: 1.5.307 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.11.27 + caniuse-lite: 1.0.30001814 + electron-to-chromium: 1.5.443 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.3) bs-logger@0.2.6: dependencies: @@ -2197,7 +2201,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001776: {} + caniuse-lite@1.0.30001814: {} chalk@4.1.2: dependencies: @@ -2240,13 +2244,13 @@ snapshots: cookie@0.7.2: {} - create-jest@29.7.0(@types/node@25.3.3): + create-jest@29.7.0(@types/node@22.20.4): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@25.3.3) + jest-config: 29.7.0(@types/node@22.20.4) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2289,7 +2293,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.443: {} emittery@0.13.1: {} @@ -2305,7 +2309,7 @@ snapshots: es-errors@1.3.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -2341,11 +2345,11 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@4.22.1: + express@4.22.3: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.8 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -2362,9 +2366,9 @@ snapshots: 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.2 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.8 + qs: 6.16.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2 @@ -2381,7 +2385,7 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-uri@3.1.0: {} + fast-uri@3.1.8: {} fb-watchman@2.0.2: dependencies: @@ -2428,12 +2432,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-package-type@0.1.0: {} @@ -2441,7 +2445,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-stream@6.0.1: {} @@ -2465,7 +2469,7 @@ snapshots: graceful-fs@4.2.11: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -2478,7 +2482,7 @@ snapshots: has-symbols@1.1.0: {} - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -2516,9 +2520,9 @@ snapshots: is-arrayish@0.2.1: {} - is-core-module@2.16.1: + is-core-module@2.17.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 is-fullwidth-code-point@3.0.0: {} @@ -2534,9 +2538,9 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: @@ -2544,11 +2548,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@istanbuljs/schema': 0.1.3 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -2583,7 +2587,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -2603,31 +2607,31 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@25.3.3): + jest-cli@29.7.0(@types/node@22.20.4): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@25.3.3) + create-jest: 29.7.0(@types/node@22.20.4) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@25.3.3) + jest-config: 29.7.0(@types/node@22.20.4) jest-util: 29.7.0 jest-validate: 29.7.0 - yargs: 17.7.2 + yargs: 17.7.3 transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest-config@29.7.0(@types/node@25.3.3): + jest-config@29.7.0(@types/node@22.20.4): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.7) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -2647,7 +2651,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 25.3.3 + '@types/node': 22.20.4 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2676,7 +2680,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2686,7 +2690,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 25.3.3 + '@types/node': 22.20.4 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2712,7 +2716,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -2725,7 +2729,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2749,7 +2753,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.11 + resolve: 1.22.12 resolve.exports: 2.0.3 slash: 3.0.0 @@ -2760,7 +2764,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2788,7 +2792,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -2808,15 +2812,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -2827,18 +2831,18 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.4 + semver: 7.8.5 transitivePeerDependencies: - supports-color jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 - picomatch: 2.3.1 + picomatch: 2.3.2 jest-validate@29.7.0: dependencies: @@ -2853,7 +2857,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.3.3 + '@types/node': 22.20.4 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -2862,17 +2866,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 25.3.3 + '@types/node': 22.20.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@25.3.3): + jest@29.7.0(@types/node@22.20.4): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@25.3.3) + jest-cli: 29.7.0(@types/node@22.20.4) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -2881,7 +2885,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 @@ -2892,7 +2896,7 @@ snapshots: json-schema-to-ts@2.12.0: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.7 '@types/json-schema': 7.0.15 ts-algebra: 1.2.2 @@ -2920,7 +2924,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.5 make-error@1.3.6: {} @@ -2941,7 +2945,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -2955,11 +2959,11 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.21 minimatch@8.0.7: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.7 minimist@1.2.8: {} @@ -2979,7 +2983,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.27: {} + node-releases@2.0.57: {} normalize-path@3.0.0: {} @@ -3017,7 +3021,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -3037,11 +3041,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 - path-to-regexp@0.1.12: {} + path-to-regexp@0.1.13: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} pirates@4.0.7: {} @@ -3060,16 +3064,17 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 pure-rand@6.1.0: {} - qs@6.14.2: + qs@6.16.0: dependencies: - side-channel: 1.1.0 + es-define-property: 1.0.1 + side-channel: 1.1.1 range-parser@1.2.1: {} @@ -3094,9 +3099,10 @@ snapshots: resolve.exports@2.0.3: {} - resolve@1.22.11: + resolve@1.22.12: dependencies: - is-core-module: 2.16.1 + es-errors: 1.3.0 + is-core-module: 2.17.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -3106,7 +3112,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.5: {} send@0.19.2: dependencies: @@ -3143,7 +3149,7 @@ snapshots: shebang-regex@3.0.0: {} - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -3163,11 +3169,11 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -3225,7 +3231,7 @@ snapshots: test-exclude@6.0.0: dependencies: - '@istanbuljs/schema': 0.1.3 + '@istanbuljs/schema': 0.1.6 glob: 7.2.3 minimatch: 3.1.5 @@ -3239,24 +3245,24 @@ snapshots: ts-algebra@1.2.2: {} - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.3.3))(typescript@4.9.5): + ts-jest@29.4.14(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.4))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@25.3.3) + handlebars: 4.7.9 + jest: 29.7.0(@types/node@22.20.4) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.4 + semver: 7.8.5 type-fest: 4.41.0 - typescript: 4.9.5 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.7) jest-util: 29.7.0 type-detect@4.0.8: {} @@ -3270,18 +3276,18 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - typescript@4.9.5: {} + typescript@5.9.3: {} uglify-js@3.19.3: optional: true - undici-types@7.18.2: {} + undici-types@6.21.0: {} unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.3.3(browserslist@4.29.3): dependencies: - browserslist: 4.28.1 + browserslist: 4.29.3 escalade: 3.2.0 picocolors: 1.1.1 @@ -3324,7 +3330,7 @@ snapshots: yargs-parser@21.1.1: {} - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 diff --git a/src/index.test.ts b/src/index.test.ts index dd90dca..ad2c102 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,8 @@ import { getFiles, initFsRouting, UserFacingError } from '.' -import type { Request, Response } from 'express' +import express from 'express' +import type { Request, RequestHandler, Response } from 'express' +import type { Server } from 'http' +import type { AddressInfo } from 'net' import path from 'path' // Express Route objects have a `methods` record at runtime @@ -10,11 +13,45 @@ declare module 'express-serve-static-core' { } const testroutesPath = path.join(__dirname, 'testroutes') const fullPath = relativePath => path.join(testroutesPath, relativePath) +const fixturePath = relativePath => + path.join(__dirname, 'testfixtures', relativePath) +const skipMiddleware: RequestHandler = (req, res, next) => next() + +// servers are closed after each test, so that a failing test does not leave jest hanging +const servers: Server[] = [] +afterEach(async () => { + await Promise.all( + servers.splice(0).map(server => new Promise(done => server.close(done))) + ) +}) + +// mounts the routes on a real server, and returns a function that sends a request to it +const serve = async ( + params: Partial[0]> & { routesPath: string } +) => { + const router = await initFsRouting({ + ensureAdmin: skipMiddleware, + ensureAuthenticated: skipMiddleware, + logMounts: false, + ...params, + }) + const app = express() + app.use(router) + const server = app.listen(0) + servers.push(server) + const { port } = server.address() as AddressInfo + const request = async (url: string, method = 'GET') => { + const res = await fetch(`http://127.0.0.1:${port}${url}`, { method }) + return { status: res.status, body: await res.json().catch(() => null) } + } + return { router, request } +} describe('routes', () => { it('should find all files in testroutes', async () => { const files = await getFiles(testroutesPath) - const expectedFiles = [fullPath('roles.ts'), fullPath('index.ts')] - expect(files).toEqual(expectedFiles) + const expectedFiles = [fullPath('index.ts'), fullPath('roles.ts')] + // the order that the filesystem returns files in is not guaranteed + expect(files.sort()).toEqual(expectedFiles) }) it('should mount all routes in testroutes', async () => { @@ -92,3 +129,65 @@ describe('roles', () => { }) }) }) + +describe('route paths', () => { + it('mounts static routes before param routes', async () => { + const { router, request } = await serve({ + routesPath: fixturePath('ordering'), + }) + const paths = router.stack.map(layer => layer.route!.path).flat() + expect(paths).toEqual([ + '/users/me', + '/users/2fa', + '/users/:id', + '/indexes', + '/', + ]) + + expect((await request('/users/2fa')).body).toEqual({ route: 'users/2fa' }) + expect((await request('/users/me')).body).toEqual({ route: 'users/me' }) + expect((await request('/users/7')).body).toEqual({ route: 'users/:id' }) + expect((await request('/indexes')).body).toEqual({ route: 'indexes' }) + expect((await request('/')).body).toEqual({ route: 'index' }) + expect((await request('/_hidden')).status).toBe(404) + }) + + it('does not return declaration files', async () => { + const files = await getFiles(fixturePath('ordering')) + expect(files.filter(file => file.endsWith('.d.ts'))).toEqual([]) + }) +}) + +describe('route configuration', () => { + it('applies roles and validation to lowercase method exports', async () => { + const denied = await serve({ routesPath: fixturePath('lowercase.ts') }) + expect((await denied.request('/?name=a', 'POST')).status).toBe(403) + + const allowed = await serve({ + routesPath: fixturePath('lowercase.ts'), + rolesResolver: () => ['org:admin'], + }) + expect((await allowed.request('/', 'POST')).status).toBe(400) + expect((await allowed.request('/?name=a', 'POST')).status).toBe(200) + }) + + it.each([ + ['rolesWithoutHandler.ts', 'no POST handler is exported'], + ['rolesNotArray.ts', 'roles must be an array of role names'], + ['guestAdmin.ts', 'guestAccess and ensureAdmin cannot both be set'], + ['notAHandler.ts', 'GET must be a request handler'], + ])('refuses to mount %s', async (file, message) => { + await expect( + serve({ routesPath: fixturePath(`invalid/${file}`) }) + ).rejects.toThrow(message) + }) + + it('refuses to start without auth middleware', async () => { + await expect( + serve({ + routesPath: testroutesPath, + ensureAuthenticated: undefined as unknown as RequestHandler, + }) + ).rejects.toThrow('must be middleware functions') + }) +}) diff --git a/src/index.ts b/src/index.ts index 80538b6..022a1f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ -import { Request, RequestHandler, Router } from 'express' +import { RequestHandler, Router } from 'express' import { glob } from 'glob' +import { resolve } from 'path' import { asyncErrorHandler } from './middleware/errors' import { createRolesMiddleware, TRolesResolver } from './middleware/roles' import { @@ -11,26 +12,48 @@ import { } from './types' import { createBodyValidator, createQueryValidator } from './validation' +type TValidation = Partial> + interface EndpointModule extends Partial> { - validation?: Partial>> + validation?: Partial> guestAccess?: boolean ensureAdmin?: boolean - path: string // this is added by the router - roles?: string[] + roles?: Partial> } const getRoutePath = (path: string, routesPath: string) => { // get the endpoint path for express by removing the base filesystem path - let routePath = path.replace(routesPath, '') - // remove the js portion - routePath = routePath.replace(/\.js|\.ts$/, '') - // replace index at beggining with / - routePath = routePath.replace(/^\/index/, '/') + let routePath = path.startsWith(routesPath) + ? path.slice(routesPath.length) + : path + // remove the file extension + routePath = routePath.replace(/\.(js|ts)$/, '') + // a top level index file is the root route + routePath = routePath.replace(/^\/index$/, '/') // remove index at end routePath = routePath.replace(/\/index$/, '') return routePath } +// Reverse alphabetical order, except that a static segment always sorts above a param segment. +// This makes it so we can override param routes, e.g. /users/me is mounted before /users/:id +const compareRoutePaths = (a: string, b: string) => { + const aSegments = a.split('/') + const bSegments = b.split('/') + const length = Math.min(aSegments.length, bSegments.length) + for (let i = 0; i < length; i++) { + const aSegment = aSegments[i] + const bSegment = bSegments[i] + if (aSegment === bSegment) continue + const aIsParam = aSegment.startsWith(':') + if (aIsParam !== bSegment.startsWith(':')) { + return aIsParam ? 1 : -1 + } + return aSegment < bSegment ? 1 : -1 + } + return bSegments.length - aSegments.length +} + interface IInitFsRoutingParams { ensureAdmin: RequestHandler ensureAuthenticated: RequestHandler @@ -49,43 +72,52 @@ export const initFsRouting: ( logMounts = true, rolesResolver = () => [], }) => { + // the auth middleware guards every route, so refuse to start without it + if ( + typeof ensureAuthenticated !== 'function' || + typeof ensureAdmin !== 'function' + ) { + throw new TypeError( + 'ensureAuthenticated and ensureAdmin must be middleware functions' + ) + } const router = Router({ caseSensitive: true }) // Apply middleware first if (logMounts) { console.log('Mounting routes') } let numberOfRoutes = 0 - let numberOfFiles = 0 let numberOfRoutesWithoutValidation = 0 + // resolve relative paths against the working directory, the same way the files are imported + const root = resolve(routesPath) // Get all of the files under the routes directory - const files = await getFiles(routesPath) - numberOfFiles = files.length + 1 - // Sort by reverse alphabetical order, so items with colons are below items without colons. This makes it so we can override param routes. + const files = await getFiles(root) + const numberOfFiles = files.length const modulePromises = files .sort() - .reverse() - .map(async path => { - const routePath = getRoutePath(path, routesPath) + .map(path => ({ path, routePath: getRoutePath(path, root) })) + .sort((a, b) => compareRoutePaths(a.routePath, b.routePath)) + .map(async ({ path, routePath }) => { // do not handle routes that begin with _ const lastSlash = routePath.lastIndexOf('/') const endpointName = routePath.slice(lastSlash) if (endpointName.startsWith('/_')) { - console.log('Skipping mounting:', routePath) + if (logMounts) { + console.log('Skipping mounting:', routePath) + } return } // import route const module: EndpointModule = await import(path) - module.path = routePath - - return module + return { module, routePath } }) const modules = await Promise.all(modulePromises) - modules.map(module => { - if (!module) return - const routePath = getRoutePath(module.path, routesPath) + modules.map(mod => { + if (!mod) return + const { module, routePath } = mod // here we have the chance to alias routes to different locations, by storing multiple paths in routePaths const routePaths = [routePath] @@ -108,7 +140,7 @@ export const initFsRouting: ( console.log('\t | No exported HTTP methods') }) - if (process.env.NODE_ENV !== 'node') { + if (logMounts) { console.log( `${numberOfFiles} route files processed, ${numberOfRoutes} routes mounted, ${numberOfRoutesWithoutValidation} routes do not have validation.` ) @@ -128,6 +160,32 @@ interface IMountEndpointsParams { rolesResolver: TRolesResolver } +// Reads a per-method export (roles, validation), matching the keys to HTTP methods regardless of case. +// A key that is not an HTTP method would otherwise be ignored silently, so it is rejected instead. +const getMethodConfig = ( + name: 'roles' | 'validation', + config: unknown, + path: string +): Partial> => { + const methodConfig: Partial> = {} + if (config === undefined) { + return methodConfig + } + if (typeof config !== 'object' || config === null || Array.isArray(config)) { + throw new TypeError( + `${path}: ${name} must be an object keyed by HTTP method` + ) + } + Object.entries(config).forEach(([key, value]) => { + const method = key.toUpperCase() + if (!isHttpMethod(method)) { + throw new TypeError(`${path}: ${name}.${key} is not an HTTP method`) + } + methodConfig[method] = value + }) + return methodConfig +} + const mountEndpoints = ({ paths, endpoints, @@ -138,12 +196,30 @@ const mountEndpoints = ({ rolesResolver, }: IMountEndpointsParams): [number, number] => { let mounted = 0 - let numberWithValidation = 0 + let numberWithoutValidation = 0 + const path = paths[0] - const validation = endpoints.validation + const validation = getMethodConfig( + 'validation', + endpoints.validation, + path + ) + const roles = getMethodConfig('roles', endpoints.roles, path) const guestAccess = endpoints.guestAccess const adminOnly = endpoints.ensureAdmin - const roles = endpoints.roles + // a value like 'false' is truthy, so only accept real booleans for the access exports + if (guestAccess !== undefined && typeof guestAccess !== 'boolean') { + throw new TypeError(`${path}: guestAccess must be a boolean`) + } + if (adminOnly !== undefined && typeof adminOnly !== 'boolean') { + throw new TypeError(`${path}: ensureAdmin must be a boolean`) + } + if (guestAccess && adminOnly) { + throw new TypeError( + `${path}: guestAccess and ensureAdmin cannot both be set` + ) + } + const mountedMethods = new Set() Object.entries(endpoints).map(([method, endpoint]: [string, Endpoint]) => { // skip exports that are not allowed Express methods @@ -151,6 +227,17 @@ const mountEndpoints = ({ if (!isExpressMethod(expressMethodName)) { return } + // roles and validation apply to the method however the export is cased + const httpMethod = method.toUpperCase() as AllowedMethod + const endpointHandlers = Array.isArray(endpoint) ? endpoint : [endpoint] + if ( + endpointHandlers.length === 0 || + !endpointHandlers.every(handler => typeof handler === 'function') + ) { + throw new TypeError( + `${path}: ${method} must be a request handler or an array of request handlers` + ) + } let handlers: RequestHandler[] = [] if (!guestAccess) { handlers.push(ensureAuthenticated) @@ -159,59 +246,53 @@ const mountEndpoints = ({ handlers.push(ensureAdmin) } } - if (roles?.hasOwnProperty(method)) { - if (isHttpMethod(method)) { - handlers.push(createRolesMiddleware(roles[method], rolesResolver)) - } + const requiredRoles = roles[httpMethod] + if (requiredRoles !== undefined) { + handlers.push(createRolesMiddleware(requiredRoles, rolesResolver)) } let validationMsg = '' + const methodValidation = validation[httpMethod] // check if it should have validation - if (validation?.hasOwnProperty(method)) { - if (isHttpMethod(method)) { - const hasBody = validation[method]?.body - const hasQuery = validation[method]?.query - if (logMounts) { - console.log(`\t | Mounting ${method} with validation`) - } - // verify that the validation object has the correct keys (query and body) - if (hasQuery) { - validationMsg = `\n\t\t | has query validation\n` - // add optional key property to all validators - const query = { - ...validation[method]?.query, - properties: { - ...validation[method]?.query?.properties, - key: { type: 'string', description: 'API Key' }, - }, - } - // Add query validation handler - handlers.push( - createQueryValidator({ - query: query, - // body: validation[method]?.body, - }) - ) - } - if (hasBody) { - // Add body validation handler - validationMsg += `\n\t\t | has body validation` - handlers.push( - createBodyValidator({ - body: validation[method]?.body, - }) - ) + if (methodValidation) { + const hasBody = methodValidation.body + const hasQuery = methodValidation.query + if (logMounts) { + console.log(`\t | Mounting ${method} with validation`) + } + // verify that the validation object has the correct keys (query and body) + if (hasQuery) { + validationMsg = `\n\t\t | has query validation\n` + // add optional key property to all validators + const query = { + ...methodValidation.query, + properties: { + ...methodValidation.query?.properties, + key: { type: 'string', description: 'API Key' }, + }, } + // Add query validation handler + handlers.push( + createQueryValidator({ + query: query, + }) + ) + } + if (hasBody) { + // Add body validation handler + validationMsg += `\n\t\t | has body validation` + handlers.push( + createBodyValidator({ + body: methodValidation.body, + }) + ) } } else { // this endpoint doesn't have validation yet validationMsg += '- no validation' - numberWithValidation++ + numberWithoutValidation++ } - const hasMiddleware = Array.isArray(endpoint) - handlers = hasMiddleware - ? [...handlers, ...endpoint] - : [...handlers, endpoint] + handlers = [...handlers, ...endpointHandlers] // loop over handlers and print warnings if they are not async handlers.forEach(handler => { if (handler.constructor.name !== 'AsyncFunction' && logMounts) { @@ -226,9 +307,26 @@ const mountEndpoints = ({ if (logMounts) { console.log(`\t | ${method} ${validationMsg}`) } + mountedMethods.add(httpMethod) mounted++ }) - return [mounted, numberWithValidation] + + // roles that are not attached to a handler protect nothing, which is most likely a typo + Object.keys(roles).forEach(method => { + if (!mountedMethods.has(method)) { + throw new TypeError( + `${path}: roles are set for ${method} but no ${method} handler is exported` + ) + } + }) + Object.keys(validation).forEach(method => { + if (!mountedMethods.has(method)) { + console.warn( + `${path}: validation is set for ${method} but no ${method} handler is exported` + ) + } + }) + return [mounted, numberWithoutValidation] } const isExpressMethod = (method: string): method is ExpressMethod => { @@ -245,7 +343,13 @@ const getFiles = async (src: string) => { return [src] } - const files = await glob(src + '/**/!(*.test).[tj]s', { nodir: true }) + // glob relative to src, so that special characters in the path to the routes are not treated as a pattern + const files = await glob('**/*.{ts,js}', { + cwd: src, + absolute: true, + nodir: true, + ignore: ['**/*.test.{ts,js}', '**/*.d.ts'], + }) return files } diff --git a/src/middleware/errors.test.ts b/src/middleware/errors.test.ts index e61b14c..404e2cd 100644 --- a/src/middleware/errors.test.ts +++ b/src/middleware/errors.test.ts @@ -2,6 +2,8 @@ process.env.NODE_ENV = 'production' import { + asyncErrorHandler, + defaultErrorHandler, isUserFacingError, UserFacingError, userFacingErrorHandler, @@ -96,3 +98,46 @@ describe('user facing errors', () => { }) }) }) + +describe('default error handler', () => { + it('does not return error details in production', () => { + const json = jest.fn() + const res = { status: jest.fn().mockImplementation(() => ({ json })) } + const req = { originalUrl: '/test' } + jest.spyOn(console, 'error').mockImplementationOnce(() => {}) + + // @ts-expect-error types dont match + defaultErrorHandler(new Error('secret detail'), req, res, jest.fn()) + expect(res.status).toHaveBeenCalledWith(500) + const body = json.mock.calls[0][0] + expect(body.message).toBe('Unknown error') + expect(body.stack).toBeUndefined() + }) +}) + +describe('async error handler', () => { + it.each([undefined, null, '', 'route', 'router'])( + 'passes an error to next when a handler throws %p', + async thrown => { + const next = jest.fn() + const handler = asyncErrorHandler(async () => { + throw thrown + }) + // @ts-expect-error the request and response are not used + await handler({}, {}, next) + expect(next).toHaveBeenCalledTimes(1) + expect(next.mock.calls[0][0]).toBeInstanceOf(Error) + } + ) + + it('passes thrown errors to next unchanged', async () => { + const next = jest.fn() + const error = new UserFacingError('nope', 400) + const handler = asyncErrorHandler(async () => { + throw error + }) + // @ts-expect-error the request and response are not used + await handler({}, {}, next) + expect(next).toHaveBeenCalledWith(error) + }) +}) diff --git a/src/middleware/errors.ts b/src/middleware/errors.ts index 5be6f7f..b17b56f 100644 --- a/src/middleware/errors.ts +++ b/src/middleware/errors.ts @@ -1,4 +1,4 @@ -import e, { +import { ErrorRequestHandler, NextFunction, Request, @@ -28,7 +28,8 @@ interface IError { time: Date stack?: string } -const development = process.env.NODE_ENV !== 'production' +// read on every request, so that it does not depend on when this module was imported +const isDevelopment = () => process.env.NODE_ENV !== 'production' export const userFacingErrorHandler: ErrorRequestHandler = ( err, @@ -36,6 +37,10 @@ export const userFacingErrorHandler: ErrorRequestHandler = ( res, next ) => { + // the response has already started, so only express can finish handling this + if (res.headersSent) { + return next(err) + } if (isUserFacingError(err)) { console.error(err.toString()) // TODO: Get status code from error if it exists @@ -47,12 +52,12 @@ export const userFacingErrorHandler: ErrorRequestHandler = ( timestamp: err.timestamp, path: req.originalUrl, }) - } else if (development) { + } else if (isDevelopment()) { // go to the default error handler, which shows more details return next(err) } // We're not in development, and this is not a user facing error, return a default "Unknown error" - console.error('Unknown error returned on path:', req.originalUrl, err.toString()) + console.error('Unknown error returned on path:', req.originalUrl, String(err)) return res.status(500).json({ error: true, message: 'Unknown Error', @@ -67,17 +72,21 @@ export const defaultErrorHandler: ErrorRequestHandler = ( res, next ) => { - const message = err.message || 'Unknown error' console.error( 'Default error handler shown in development. Following error triggered it: ', err ) + if (res.headersSent) { + return next(err) + } + // internal error details are only returned outside of production + const development = isDevelopment() const body: IError = { error: true, time: new Date(), - message, + message: (development && err?.message) || 'Unknown error', path: req.originalUrl, - stack: err.stack, + stack: development ? err?.stack : undefined, } return res.status(500).json(body) } @@ -91,6 +100,11 @@ export const asyncErrorHandler = function wrap(fn: RequestHandler) { await fn(req, res, next) } catch (e) { + // express treats a falsy value as success and 'route'/'router' as a skip, either of + // which would let the request past this handler, so always pass along a real error + if (!e || e === 'route' || e === 'router') { + return next(new Error(`Handler threw a non-error value: ${String(e)}`)) + } next(e) } } diff --git a/src/middleware/roles.ts b/src/middleware/roles.ts index 1ef3c37..3f035db 100644 --- a/src/middleware/roles.ts +++ b/src/middleware/roles.ts @@ -7,6 +7,10 @@ export const createRolesMiddleware = ( roles: string[], rolesResolver: TRolesResolver ) => { + // a string would be matched by substring, so only accept a list of role names + if (!Array.isArray(roles) || !roles.every(role => typeof role === 'string')) { + throw new TypeError('roles must be an array of role names') + } // call the roles resolver, to get the roles for the current user // if the user has any of the roles required for this route, then call next() // otherwise, call next with a UserFacingError @@ -18,7 +22,8 @@ export const createRolesMiddleware = ( const userRoles = await rolesResolver(req) // @ts-ignore req.roles = userRoles - const hasRole = userRoles.some(role => roles?.includes(role)) + const hasRole = + Array.isArray(userRoles) && userRoles.some(role => roles.includes(role)) if (hasRole) { next() } else { diff --git a/src/testfixtures/invalid/guestAdmin.ts b/src/testfixtures/invalid/guestAdmin.ts new file mode 100644 index 0000000..b6ae322 --- /dev/null +++ b/src/testfixtures/invalid/guestAdmin.ts @@ -0,0 +1,8 @@ +import { Endpoint } from '../../types' + +export const guestAccess = true +export const ensureAdmin = true + +export const GET: Endpoint = async (req, res) => { + return res.json({}) +} diff --git a/src/testfixtures/invalid/notAHandler.ts b/src/testfixtures/invalid/notAHandler.ts new file mode 100644 index 0000000..e9b25d4 --- /dev/null +++ b/src/testfixtures/invalid/notAHandler.ts @@ -0,0 +1 @@ +export const GET = 'not a handler' diff --git a/src/testfixtures/invalid/rolesNotArray.ts b/src/testfixtures/invalid/rolesNotArray.ts new file mode 100644 index 0000000..f4667ca --- /dev/null +++ b/src/testfixtures/invalid/rolesNotArray.ts @@ -0,0 +1,9 @@ +import { Endpoint } from '../../types' + +export const roles = { + GET: 'org:admin', +} + +export const GET: Endpoint = async (req, res) => { + return res.json({}) +} diff --git a/src/testfixtures/invalid/rolesWithoutHandler.ts b/src/testfixtures/invalid/rolesWithoutHandler.ts new file mode 100644 index 0000000..5ad5662 --- /dev/null +++ b/src/testfixtures/invalid/rolesWithoutHandler.ts @@ -0,0 +1,9 @@ +import { Endpoint } from '../../types' + +export const roles = { + POST: ['org:admin'], +} + +export const GET: Endpoint = async (req, res) => { + return res.json({}) +} diff --git a/src/testfixtures/lowercase.ts b/src/testfixtures/lowercase.ts new file mode 100644 index 0000000..9f266c1 --- /dev/null +++ b/src/testfixtures/lowercase.ts @@ -0,0 +1,22 @@ +import { Endpoint } from '../types' + +export const guestAccess = true + +export const roles = { + POST: ['org:admin'], +} + +export const validation = { + post: { + query: { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'], + }, + }, +} + +// the export is lowercase, but the roles and validation above still apply to it +export const post: Endpoint = async (req, res) => { + return res.json({ message: 'Hello Authorized World!' }) +} diff --git a/src/testfixtures/ordering/_hidden.ts b/src/testfixtures/ordering/_hidden.ts new file mode 100644 index 0000000..4bc2703 --- /dev/null +++ b/src/testfixtures/ordering/_hidden.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: '_hidden' }) +} diff --git a/src/testfixtures/ordering/index.ts b/src/testfixtures/ordering/index.ts new file mode 100644 index 0000000..8e5b489 --- /dev/null +++ b/src/testfixtures/ordering/index.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: 'index' }) +} diff --git a/src/testfixtures/ordering/indexes.d.ts b/src/testfixtures/ordering/indexes.d.ts new file mode 100644 index 0000000..e5eefe2 --- /dev/null +++ b/src/testfixtures/ordering/indexes.d.ts @@ -0,0 +1,2 @@ +// declaration files sit next to compiled routes, and must not be mounted +export declare const GET: unknown diff --git a/src/testfixtures/ordering/indexes.ts b/src/testfixtures/ordering/indexes.ts new file mode 100644 index 0000000..4202448 --- /dev/null +++ b/src/testfixtures/ordering/indexes.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: 'indexes' }) +} diff --git a/src/testfixtures/ordering/users/2fa.ts b/src/testfixtures/ordering/users/2fa.ts new file mode 100644 index 0000000..b045c33 --- /dev/null +++ b/src/testfixtures/ordering/users/2fa.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: 'users/2fa' }) +} diff --git a/src/testfixtures/ordering/users/:id.ts b/src/testfixtures/ordering/users/:id.ts new file mode 100644 index 0000000..ffa3643 --- /dev/null +++ b/src/testfixtures/ordering/users/:id.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: 'users/:id' }) +} diff --git a/src/testfixtures/ordering/users/me.ts b/src/testfixtures/ordering/users/me.ts new file mode 100644 index 0000000..d08006c --- /dev/null +++ b/src/testfixtures/ordering/users/me.ts @@ -0,0 +1,5 @@ +import { Endpoint } from '../../../types' + +export const GET: Endpoint = async (req, res) => { + return res.json({ route: 'users/me' }) +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..ace0a11 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + // what gets published: the library without its tests and test routes + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": false, + // src is not published, so embed the sources in the source maps + "inlineSources": true + }, + "exclude": ["src/**/*.test.ts", "src/testroutes", "src/testfixtures"] +}