From 4df89d3d25cefbb80916f8974205b705a169f7c3 Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 13 Apr 2026 14:12:43 -0500 Subject: [PATCH 01/25] Modernize testing: migrate from Jest to node:test Add a testing modernization plan and migrate test layout and CI to the new approach. Remove the legacy Jest config and delete legacy __tests__ artifacts, add a new top-level test/ suite with helpers (including test/helpers/env.js) and route test files, and update docs and CI to run the consolidated npm script (allTests) and expose targeted scripts (coreTests, existsTests, functionalTests). Dev dependencies and package metadata were adjusted to support the new runner/coverage tools. --- .github/workflows/cd_dev.yaml | 2 +- README.md | 9 +- TESTING_MODERNIZATION_PLAN.md | 180 + jest.config.js | 211 - package-lock.json | 4903 +++--------------- package.json | 19 +- routes/__tests__/create-route.testcase.md | 29 - routes/__tests__/create.test.js | 165 - routes/__tests__/delete-route.testcase.md | 29 - routes/__tests__/delete.test.js | 113 - routes/__tests__/error-messenger.test.js | 30 - routes/__tests__/error-messenger.testcase.md | 29 - routes/__tests__/index.test.js | 28 - routes/__tests__/mount.test.js | 141 - routes/__tests__/overwrite-route.testcase.md | 29 - routes/__tests__/overwrite.test.js | 132 - routes/__tests__/query.test.js | 159 - routes/__tests__/update-route.testcase.md | 29 - routes/__tests__/update.test.js | 138 - test/helpers/env.js | 7 + test/routes/create.test.js | 109 + test/routes/delete.test.js | 82 + test/routes/error-messenger.test.js | 70 + test/routes/index.test.js | 13 + test/routes/mount.test.js | 69 + test/routes/overwrite.test.js | 101 + test/routes/query.test.js | 105 + test/routes/update.test.js | 84 + 28 files changed, 1641 insertions(+), 5374 deletions(-) create mode 100644 TESTING_MODERNIZATION_PLAN.md delete mode 100644 jest.config.js delete mode 100644 routes/__tests__/create-route.testcase.md delete mode 100644 routes/__tests__/create.test.js delete mode 100644 routes/__tests__/delete-route.testcase.md delete mode 100644 routes/__tests__/delete.test.js delete mode 100644 routes/__tests__/error-messenger.test.js delete mode 100644 routes/__tests__/error-messenger.testcase.md delete mode 100644 routes/__tests__/index.test.js delete mode 100644 routes/__tests__/mount.test.js delete mode 100644 routes/__tests__/overwrite-route.testcase.md delete mode 100644 routes/__tests__/overwrite.test.js delete mode 100644 routes/__tests__/query.test.js delete mode 100644 routes/__tests__/update-route.testcase.md delete mode 100644 routes/__tests__/update.test.js create mode 100644 test/helpers/env.js create mode 100644 test/routes/create.test.js create mode 100644 test/routes/delete.test.js create mode 100644 test/routes/error-messenger.test.js create mode 100644 test/routes/index.test.js create mode 100644 test/routes/mount.test.js create mode 100644 test/routes/overwrite.test.js create mode 100644 test/routes/query.test.js create mode 100644 test/routes/update.test.js diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index f0d30e4..0630e36 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -52,7 +52,7 @@ jobs: - name: Install dependencies and run the test run: | npm install - npm run functionalTests + npm run allTests deploy: if: github.event.pull_request.draft == false needs: diff --git a/README.md b/README.md index cc5252c..d53f757 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,14 @@ OPEN_API_CORS = false Now, you can run tests ```shell -npm run runtest +npm run allTests +``` + +For fast local checks, run targeted suites: +```shell +npm run coreTests +npm run existsTests +npm run functionalTests ``` And start the app diff --git a/TESTING_MODERNIZATION_PLAN.md b/TESTING_MODERNIZATION_PLAN.md new file mode 100644 index 0000000..311cc1e --- /dev/null +++ b/TESTING_MODERNIZATION_PLAN.md @@ -0,0 +1,180 @@ +# TinyNode Testing Modernization Plan + +## Goals + +1. Move from Jest to Node's built-in test runner (`node:test`) without experimental VM flags. +2. Keep ESM native across app and tests. +3. Preserve existing test intent (`__core`, `__e2e`, `__exists`, `__mock_functions`) with a cleaner selection model. +4. Separate API availability checks from API functionality checks. +5. Convert `.testcase.md` descriptions into executable tests, then remove the markdown artifacts. +6. Provide a reusable model for other repositories. + +## Recommended Stack + +- Test runner: `node:test` +- Assertions/mocks: `node:assert/strict`, `node:test` mocks, plus explicit fakes +- HTTP endpoint testing (in-memory app): `supertest` +- Upstream HTTP mocking for `fetch`: `undici` `MockAgent` (preferred) or manual fetch stubs +- Browser/UI tests: `playwright` (preferred over Puppeteer) +- Coverage: `c8` (stable and CI-friendly) +- Optional snapshots/matchers (only if needed): keep minimal, avoid framework lock-in + +## Why Playwright Over Puppeteer + +1. Better multi-browser support (Chromium, Firefox, WebKit). +2. Strong test reliability features (auto-waiting, robust locators). +3. Excellent CI support and diagnostics (trace viewer, screenshots, videos). +4. Works cleanly with custom runners (including `node:test`) if desired. + +## Target Test Architecture + +Create a top-level `test/` folder and phase out `routes/__tests__/` over time: + +- `test/unit/`: + - Pure logic tests (`rest.js`, `tokens.js`, helper functions) +- `test/integration/`: + - Route behavior tests using `supertest` with app/router instances in memory + - Strong mocking for upstream RERUM/network behavior +- `test/contract/`: + - API response shape and header contracts + - Includes route registration/availability checks +- `test/e2e/`: + - Browser and user-flow checks (Playwright) + - Minimal critical-path scenarios +- `test/smoke/`: + - Availability tests (is app up? are endpoints mounted?) +- `test/fixtures/`: + - Reusable payloads and canned upstream responses +- `test/helpers/`: + - App factories, env setup, temporary server lifecycle, mock helpers + +## Important Architecture Adjustments + +### 1) Dependency injection at router/app boundary + +To avoid brittle module-level mocking, export route builders that accept dependencies. + +Current pattern (hard import): +- route imports `fetchRerum`, `checkAccessToken` directly. + +Preferred pattern: +- `buildCreateRouter({ fetchRerum, checkAccessToken, verifyJsonContentType })` +- default export still uses production dependencies. +- tests inject fake dependencies without test-runner-specific magic. + +This is the highest-value structural change for reliable, portable tests. + +### 2) Distinguish availability vs functionality + +- Availability tests: + - verify endpoint exists and returns expected method guards (e.g., 405 on wrong method) + - should not depend on upstream services +- Functionality tests: + - verify request transformation, upstream call behavior, response body/headers/status + - use mocked upstream behavior exhaustively + +### 3) Keep environment control explicit + +- Add dedicated test env setup (`test/helpers/env.js`) to set deterministic env vars. +- Never rely on local `.env` for test behavior. +- Ensure tests do not modify user `.env` files. + +### 4) Reduce side effects in token handling tests + +- Keep token-refresh behavior injectable or guarded so route tests do not fail due to malformed token state. + +## Mapping Existing Tags to New Commands + +Use path-based scripts and optional name filtering. + +Suggested categories: + +- `test:all` -> all suites +- `test:core` -> `test/unit` + core integration contract tests +- `test:exists` -> route registration + smoke availability tests +- `test:functional` -> mocked integration tests +- `test:e2e` -> browser tests and true end-to-end flows + +For quick local runs, rely on folder-level script filters, not test framework internals. + +## Migration Strategy (Incremental) + +### Phase 1: Foundation + +1. Add `test/` directory with helpers and one migrated sample suite. +2. Add `node:test` scripts alongside existing Jest scripts. +3. Add `c8` coverage command for new suites. + +### Phase 2: Route Suite Migration + +1. Migrate existing route tests from Jest to `node:test` one file at a time. +2. Keep test names preserving current semantic tags during migration. +3. Validate parity by running old/new suites together temporarily. + +### Phase 3: Convert `.testcase.md` to Executable Specs + +1. For each testcase markdown file, create corresponding test suite in `test/contract` or `test/integration`. +2. Keep markdown as source-of-truth only during conversion. +3. Remove `.testcase.md` once executable equivalent is merged. + +### Phase 4: UI Coverage + +1. Add Playwright and minimal smoke UI checks (page load, critical controls visible). +2. Add one interaction test per major user action. +3. Expand only for high-value user journeys. + +### Phase 5: Decommission Jest + +1. Remove Jest scripts/config/deps after parity is complete. +2. Update docs and contributor workflow. + +## CI/GitHub Actions Model + +### Pull requests + +Run fast and deterministic checks: + +1. `test:core` +2. `test:exists` +3. `test:functional` (mocked only) + +### Main branch / nightly + +Run full quality gates: + +1. `test:all` +2. `test:e2e` +3. coverage publish/report + +### Suggested safeguards + +- Fail fast on lint/type issues if enabled. +- Upload Playwright traces on failure. +- Keep browser tests isolated from unit/integration timing budgets. + +## Proposed NPM Script Direction + +(illustrative, final command syntax can be adjusted during implementation) + +- `test:all` -> `node --test test/**/*.test.js` +- `test:core` -> `node --test test/unit/**/*.test.js test/integration/**/*core*.test.js` +- `test:exists` -> `node --test test/smoke/**/*.test.js test/contract/**/*exists*.test.js` +- `test:functional` -> `node --test test/integration/**/*.test.js` +- `test:e2e` -> `node --test test/e2e/**/*.test.js` +- `coverage` -> `c8 node --test test/**/*.test.js` + +## Risks and Mitigations + +1. Risk: Migration churn while preserving behavior. + - Mitigation: side-by-side execution and parity checks per suite. +2. Risk: Mocking complexity around upstream fetch and token middleware. + - Mitigation: dependency injection and helper factories. +3. Risk: Browser test flakiness. + - Mitigation: Playwright locators, fixed test data, no arbitrary sleeps. + +## Immediate Next Steps + +1. Implement Phase 1 scaffolding and scripts. +2. Migrate one representative route suite (`create`) to validate architecture. +3. Add CI job that runs both old and new tests until parity is complete. +4. Begin converting `.testcase.md` files into executable suites. diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 37e67a9..0000000 --- a/jest.config.js +++ /dev/null @@ -1,211 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ - -const config = { - // All imported modules in your tests should be mocked automatically - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "C:\\Users\\cubap\\AppData\\Local\\Temp\\jest", - - // Automatically clear mock calls, instances and results before every test - // clearMocks: false, - - //This will tell you why jest couldn't close. Right now, it will flag the client.connect() b/c there is no client.close() - //That is OK in the testing scenario. In production, only one connection is made and it is closed when the app exits. - detectOpenHandles : false, - - displayName: { - name: 'TinyNode', - color: 'cyan' - }, - // extensionsToTreatAsEsm: [".js"], - testEnvironment: "node", - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: [ - "**/*.js" - ], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: [ - "json", - "text", - "html" - ], - - // Indicates whether each individual test should be reported during the run - verbose: true, - - //Don't show console.log and console.debug from the app code - silent: true, - - // The root directory that Jest should scan for tests and modules within - rootDir: "./", - - // The directory where Jest should output its coverage files. Default is /coverage/. See /coverage/index.html. - // coverageDirectory: undefined, - - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, - - // A path to a custom dependency extractor - // dependencyExtractor: undefined, - - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, - - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], - - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, - - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, - - // A set of global variables that need to be available in all test environments - // globals: {}, - - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", - - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], - - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], - - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, - - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], - - // Activates notifications for test results - // notify: false, - - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", - - // A preset that is used as a base for Jest's configuration - // preset: `@shelf/jest-mongodb`, - - // Run tests from one or more projects - // projects: undefined, - - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, - - // Automatically reset mock state before every test - // resetMocks: false, - - // Reset the module registry before running each individual test - // resetModules: false, - - // A path to a custom resolver - // resolver: undefined, - - // Automatically restore mock state and implementation before every test - // restoreMocks: false, - - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "./__tests__" - // ], - - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", - - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], - - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], - - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, - - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], - - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", - - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - - // Adds a location field to test results - // testLocationInResults: false, - - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], - - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], - - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, - - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", - - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", - - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", - - // A map from regular expressions to paths to transformers - transform: {}, - - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "\\\\node_modules\\\\", - // "\\.pnp\\.[^\\\\]+$" - // ], - - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, - - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - watchPathIgnorePatterns: ['globalConfig'], - - // Whether to use watchman for file crawling - // watchman: true, -} - -export default config diff --git a/package-lock.json b/package-lock.json index 20a1750..760cbbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "morgan": "~1.10.1" }, "devDependencies": { - "@jest/globals": "^30.2.0", - "jest": "^30.2.0", + "c8": "^10.1.3", "supertest": "^7.1.4" }, "engines": { @@ -26,3849 +25,1216 @@ "npm": ">=11.7.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" } }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, "engines": { - "node": ">=6.9.0" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@noble/hashes": "^1.1.5" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, + "optional": true, "engines": { - "node": ">=6.9.0" + "node": ">=14" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">= 0.6" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6.9.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "safe-buffer": "5.1.2" }, "engines": { - "node": ">=6.9.0" + "node": ">= 0.8" } }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "balanced-match": "^1.0.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 0.8" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "node_modules/c8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "node_modules/c8/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "node_modules/c8/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "node_modules/c8/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/c8/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "p-locate": "^5.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "node_modules/c8/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "brace-expansion": "^5.0.5" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "node_modules/c8/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "p-limit": "^3.0.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "node": ">=8" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=8" + "node": ">=7.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/@jest/console": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "slash": "^3.0.0" + "delayed-stream": "~1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.2.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.2.0", - "jest-config": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-resolve-dependencies": "30.2.0", - "jest-runner": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "jest-watcher": "30.2.0", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">= 0.8" } }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" + "safe-buffer": "5.2.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/@jest/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "dependencies": { - "expect": "30.2.0", - "jest-snapshot": "30.2.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } + "license": "MIT" }, - "node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6.6.0" } }, - "node_modules/@jest/globals": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/types": "30.2.0", - "jest-mock": "30.2.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.10" } }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 8" } }, - "node_modules/@jest/reporters": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" + "ms": "^2.1.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "node": ">=6.0" }, "peerDependenciesMeta": { - "node-notifier": { + "supports-color": { "optional": true } } }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.4.0" } }, - "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.8" } }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "asap": "^2.0.0", + "wrappy": "1" } }, - "node_modules/@jest/test-result": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.2.0", - "@jest/types": "30.2.0", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.2.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.41", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", - "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.14.0" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.2.0", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.2.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", - "integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001748", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", - "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.231", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.231.tgz", - "integrity": "sha512-cyl6vqZGkEBnz/PmvFHn/u9G/hbo+FF2CNAOXriG87QOeLsUdifCZ9UbHNscE9wGdrC8XstNMli0CbQnZQ+fkA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/envfile": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/envfile/-/envfile-7.1.0.tgz", - "integrity": "sha512-dyH4QnnZsArCLhPASr29eqBWDvKpq0GggQFTmysTT/S9TTmt1JrEKNvTBc09Cd7ujVZQful2HBGRMe2agu7Krg==", - "license": "Artistic-2.0", - "bin": { - "envfile": "bin.cjs" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.2.0", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } + "license": "MIT" }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/isexe": { + "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", + "node_modules/envfile": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/envfile/-/envfile-7.1.0.tgz", + "integrity": "sha512-dyH4QnnZsArCLhPASr29eqBWDvKpq0GggQFTmysTT/S9TTmt1JrEKNvTBc09Cd7ujVZQful2HBGRMe2agu7Krg==", + "license": "Artistic-2.0", "bin": { - "semver": "bin/semver.js" + "envfile": "bin.cjs" }, "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "node": ">=8" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://bevry.me/fund" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node": ">= 0.4" } }, - "node_modules/jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", - "dev": true, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/types": "30.2.0", - "import-local": "^3.2.0", - "jest-cli": "30.2.0" - }, - "bin": { - "jest": "bin/jest.js" + "es-errors": "^1.3.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">= 0.4" } }, - "node_modules/jest-changed-files": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.2.0", - "p-limit": "^3.1.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-circus": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "p-limit": "^3.1.0", - "pretty-format": "30.2.0", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/core": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">=6" } }, - "node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", - "dev": true, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } + "engines": { + "node": ">= 0.6" } }, - "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "dev": true, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-docblock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "detect-newline": "^3.1.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.8" } }, - "node_modules/jest-each": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "jest-util": "30.2.0", - "pretty-format": "30.2.0" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 6" } }, - "node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" + "node": ">= 0.6" } }, - "node_modules/jest-leak-detector": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.2.0" + "mime-db": "1.52.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.6" } }, - "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.8" } }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/jest-resolve": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-resolve-dependencies": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.2.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-runner": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.2.0", - "@jest/environment": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-leak-detector": "30.2.0", - "jest-message-util": "30.2.0", - "jest-resolve": "30.2.0", - "jest-runtime": "30.2.0", - "jest-util": "30.2.0", - "jest-watcher": "30.2.0", - "jest-worker": "30.2.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/globals": "30.2.0", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.2.0", - "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, "bin": { - "semver": "bin/semver.js" + "glob": "dist/esm/bin.mjs" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=8" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { - "node": ">=10" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-watcher": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.2.0", - "string-length": "^4.0.2" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.10" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, - "node_modules/js-tokens": { + "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } + "license": "ISC" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "p-locate": "^4.1.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "yallist": "^3.0.2" + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/make-dir": { @@ -3900,16 +1266,6 @@ "node": ">=10" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3940,13 +1296,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3957,20 +1306,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -4005,24 +1340,13 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4090,29 +1414,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -4122,43 +1423,6 @@ "node": ">= 0.6" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4210,22 +1474,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4242,45 +1490,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4288,25 +1497,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4326,16 +1516,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4351,104 +1531,32 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr": { @@ -4464,28 +1572,10 @@ "node": ">= 0.10" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dependencies": { "side-channel": "^1.1.0" }, @@ -4520,13 +1610,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4537,29 +1620,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4602,16 +1662,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -4763,57 +1813,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4823,43 +1822,6 @@ "node": ">= 0.8" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4964,39 +1926,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/superagent": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", @@ -5045,103 +1974,6 @@ "node": ">=8" } }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5151,37 +1983,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -5196,13 +1997,6 @@ "node": ">= 0.6" } }, - "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5212,72 +2006,6 @@ "node": ">= 0.8" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -5302,16 +2030,6 @@ "node": ">= 0.8" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5429,20 +2147,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5453,13 +2157,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 8347edc..1272588 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,16 @@ }, "scripts": { "start": "node ./bin/tinyNode.js", - "allTests": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "E2Etests": "node --experimental-vm-modules node_modules/jest/bin/jest.js -t __e2e ", - "existsTests": "node --experimental-vm-modules node_modules/jest/bin/jest.js -t __exists ", - "coreTests": "node --experimental-vm-modules node_modules/jest/bin/jest.js -t __core ", - "functionalTests": "node --experimental-vm-modules node_modules/jest/bin/jest.js -t __mock_functions " + "test": "node --test test/routes/**/*.test.js", + "allTests": "npm run test && npm run E2Etests", + "E2Etests": "node --test test/e2e/**/*.test.js", + "existsTests": "node --test --test-name-pattern=__exists test/routes/**/*.test.js", + "coreTests": "node --test --test-name-pattern=__core test/routes/**/*.test.js", + "functionalTests": "node --test --test-name-pattern=__mock_functions test/routes/**/*.test.js", + "coverage": "c8 --reporter=text --reporter=json --reporter=html node --test test/routes/**/*.test.js", + "e2e:install": "playwright install --with-deps chromium", + "ci:fast": "npm run coreTests && npm run existsTests && npm run functionalTests", + "ci:full": "npm run e2e:install && npm run allTests && npm run coverage" }, "dependencies": { "cors": "^2.8.5", @@ -35,8 +40,8 @@ "morgan": "~1.10.1" }, "devDependencies": { - "@jest/globals": "^30.2.0", - "jest": "^30.2.0", + "c8": "^10.1.3", + "playwright": "^1.54.2", "supertest": "^7.1.4" } } diff --git a/routes/__tests__/create-route.testcase.md b/routes/__tests__/create-route.testcase.md deleted file mode 100644 index 0cb5c43..0000000 --- a/routes/__tests__/create-route.testcase.md +++ /dev/null @@ -1,29 +0,0 @@ -# Create Route Test Cases - -These are planned coverage cases only. They are intentionally harness-agnostic. - -## Scope - -Target: `POST /create` and `POST /app/create` - -## Missing Coverage - -1. Upstream JSON error passthrough -- Setup: upstream returns non-2xx with `Content-Type: application/json` body -- Expected: TinyNode returns same status; body forwarded as plain text through shared messenger - -2. Missing `Content-Type` header on request -- Setup: client sends valid JSON body but omits `Content-Type` -- Expected: `415 Unsupported Media Type` - -3. Multiple `Content-Type` values -- Setup: request header includes multiple comma-separated MIME values -- Expected: `415 Unsupported Media Type` - -4. Upstream timeout/network failure classification -- Setup: fetch rejects with timeout/socket error -- Expected: `502 Bad Gateway` with deterministic plain-text error message - -5. Legacy route parity -- Setup: run the same failing scenarios against `/app/create` -- Expected: status and body parity with `/create` diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js deleted file mode 100644 index 3952880..0000000 --- a/routes/__tests__/create.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import express from "express" -import request from "supertest" -import { jest } from "@jest/globals" - -import createRoute from "../create.js" -import { messenger } from "../../error-messenger.js" -//import app from "../../app.js" - -const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) -routeTester.use("/create", createRoute) -routeTester.use("/app/create", createRoute) -routeTester.use(messenger) - -const rerum_uri = `${process.env.RERUM_ID_PATTERN}_not_` - -beforeEach(() => { - /** - * Request/Response Mock Using manual fetch replacement - * This is overruling the fetch(store.rerum.io/v1/api/create) call in create.js - */ - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }), - ok: true, - text: () => Promise.resolve("Descriptive Error Here") - }) - ) -}) - -afterEach(() => { - /** - * Food for thought: delete data generated by tests? - * Make a test.store available that uses the same annotationStoreTesting as RERUM tests? - */ -}) - -/** - * This test suite runs the logic of the route file 'create.js' but does not actually communicate with RERUM. - * It will confirm the following: - * - Is the express req/resp sent into the route - * - Can the route read the JSON body - * - Does the route add @id and __rerum - * - Does the route respond 201 - * - Does the route respond with the object that was in the request body - * - Does the route respond with the proper 'Location' header - * - * Note: /app/create uses the same logic and would be a redundant test. - */ -describe("Check that the request/response behavior of the TinyNode create route functions. Mock the connection to RERUM. __mock_functions", () => { - it("'/create' route request and response behavior is functioning.", async () => { - - const response = await request(routeTester) - .post("/create") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(rerum_uri) - expect(response.statusCode).toBe(201) - expect(response.body.test).toBe("item") - }) -}) - -/** - * This test suite checks the RESTful responses when using the TinyNode create endpoint incorrectly. - * - * - Incorrect HTTP method - * - Invalid JSON body - * - * Note: /app/create uses the same logic and would be a redundant test - */ -describe("Check that incorrect TinyNode create route usage results in expected RESTful responses from RERUM. __rest __core", () => { - it("Incorrect '/create' route usage has expected RESTful responses.", async () => { - let response = null - - response = await request(routeTester) - .get("/create") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .put("/create") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .patch("/create") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .delete("/create") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send("not json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - }) -}) - -describe("Check that TinyNode create route propagates upstream and network errors predictably. __rest __core", () => { - it("Preserves upstream text errors and maps network failures to 502.", async () => { - global.fetch = jest.fn(() => - Promise.resolve({ - ok: false, - status: 503, - headers: { - get: () => "text/plain; charset=utf-8" - }, - text: () => Promise.resolve("Upstream create failure") - }) - ) - - let response = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ "test": "item" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(502) - expect(response.text).toContain("Upstream create failure") - - global.fetch = jest.fn(() => Promise.reject(new Error("socket hang up"))) - - response = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ "test": "item" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(502) - expect(response.text).toContain("A RERUM error occurred") - }) -}) - -/** - * Full integration test. Checks the TinyNode app create endpoint functionality and RERUM connection. - * - * Note: /app/create uses the same logic and would be a redundant test - */ -describe("Check that the properly used create endpoints function and interact with RERUM. __e2e", () => { - it("'/create' route can save an object to RERUM.", async () => { - const response = await request(routeTester) - .post("/create") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header).toHaveProperty('location') - expect(response.statusCode).toBe(201) - expect(response.body.test).toBe("item") - }) -}) diff --git a/routes/__tests__/delete-route.testcase.md b/routes/__tests__/delete-route.testcase.md deleted file mode 100644 index bad644e..0000000 --- a/routes/__tests__/delete-route.testcase.md +++ /dev/null @@ -1,29 +0,0 @@ -# Delete Route Test Cases - -These are planned coverage cases only. They are intentionally harness-agnostic. - -## Scope - -Target: `DELETE /delete`, `DELETE /delete/:id`, and legacy `/app` equivalents - -## Missing Coverage - -1. Body-delete upstream failure passthrough -- Setup: `/delete` upstream returns non-2xx -- Expected: error goes through shared messenger with preserved status - -2. Path-delete upstream failure passthrough -- Setup: `/delete/:id` upstream returns non-2xx -- Expected: error goes through shared messenger with preserved status - -3. Network failure mapping for both delete forms -- Setup: fetch rejects for `/delete` and `/delete/:id` -- Expected: `502 Bad Gateway` - -4. Missing id/body validation behavior -- Setup: `/delete` without id in body -- Expected: `400` with clear message - -5. Legacy route parity -- Setup: mirror failure scenarios for `/app/delete` and `/app/delete/:id` -- Expected: same behavior as non-legacy routes diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js deleted file mode 100644 index f92d6b1..0000000 --- a/routes/__tests__/delete.test.js +++ /dev/null @@ -1,113 +0,0 @@ -import express from "express" -import request from "supertest" -import { jest } from "@jest/globals" -import deleteRoute from "../delete.js" -//import app from "../../app.js" - -const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) -routeTester.use("/delete", deleteRoute) -routeTester.use("/app/delete", deleteRoute) - -const rerum_uri = `${process.env.RERUM_ID_PATTERN}_not_` - -beforeEach(() => { - /** - * Request/Response Mock Using manual fetch replacement - * This is overruling the fetch(store.rerum.io/v1/api/delete) call in delete.js - */ - global.fetch = jest.fn(() => - Promise.resolve({ - text: () => Promise.resolve(""), - ok: true - }) - ) -}) - -/** - * This test suite runs the logic of the route file 'delete.js' but does not actually communicate with RERUM. - * It will confirm the following: - * - Is the express req/resp sent into the route - * - Can the route read the JSON body - * - Does the route respond 204 - * - * Note: /app/delete uses the same logic and would be a redundant test. - */ -describe("Check that the request/response behavior of the TinyNode delete route functions. Mock the connection to RERUM. __mock_functions", () => { - it("'/delete' route request and response behavior is functioning.", async () => { - let response = null - - response = await request(routeTester) - .delete("/delete") - .send({ "@id": rerum_uri, "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(204) - - response = await request(routeTester) - .delete("/delete/00000") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(204) - }) -}) - -describe("Check that incorrect TinyNode delete route usage results in expected RESTful responses from RERUM. __rest __core", () => { - it("Incorrect '/delete' route usage has expected RESTful responses.", async () => { - let response = null - - response = await request(routeTester) - .get("/delete") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .put("/delete") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .patch("/delete") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .post("/delete") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - //Bad request body - //FIXME to uncomment: https://github.com/CenterForDigitalHumanities/TinyNode/issues/89 - // response = await request(routeTester) - // .delete("/delete") - // .set("Content-Type", "application/json") - // .then(resp => resp) - // .catch(err => err) - // expect(response.statusCode).toBe(400) - - }) -}) - -/** - * TODO - skipped for now. - * Full integration test. Checks the TinyNode app delete endpoint functionality and RERUM connection. - * - * Note: /app/delete uses the same logic and would be a redundant test. - */ -describe.skip("Check that the properly used delete endpoints function and interact with RERUM. __e2e", () => { - it("'/delete' route can delete an object in RERUM. __e2e", async () => { - const response = await request(routeTester) - .delete("/app/delete/00000") - .then(response => { - expect(response.statusCode).toBe(204) - }) - .catch(err => err) - expect(response.statusCode).toBe(204) - }) -}) \ No newline at end of file diff --git a/routes/__tests__/error-messenger.test.js b/routes/__tests__/error-messenger.test.js deleted file mode 100644 index 5e70992..0000000 --- a/routes/__tests__/error-messenger.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import express from "express" -import request from "supertest" -import { messenger } from "../../error-messenger.js" - -const app = express() - -app.get("/json-error", (req, res, next) => { - next({ - status: 422, - headers: { - get: () => "application/json" - }, - json: async () => ({ message: "Invalid payload", field: "name" }) - }) -}) - -app.use(messenger) - -describe("Check shared error messenger behavior. __rest __core", () => { - it("Returns structured JSON error bodies when upstream responds with JSON.", async () => { - const response = await request(app) - .get("/json-error") - .then(resp => resp) - .catch(err => err) - - expect(response.statusCode).toBe(422) - expect(response.body.message).toBe("Invalid payload") - expect(response.body.field).toBe("name") - }) -}) diff --git a/routes/__tests__/error-messenger.testcase.md b/routes/__tests__/error-messenger.testcase.md deleted file mode 100644 index 3704f53..0000000 --- a/routes/__tests__/error-messenger.testcase.md +++ /dev/null @@ -1,29 +0,0 @@ -# Shared Error Messenger Test Cases - -These are planned coverage cases only. They are intentionally harness-agnostic. - -## Scope - -Target: shared middleware in `error-messenger.js` - -## Missing Coverage - -1. Generic Error fallback -- Setup: plain `Error` object without response-like fields -- Expected: `500` fallback with safe plain-text message - -2. Headers already sent guard -- Setup: middleware invoked after headers were sent -- Expected: middleware exits without secondary write attempts - -3. `.text()` failure fallback -- Setup: upstream response `.text()` rejects (e.g., body stream already consumed) -- Expected: middleware uses generic fallback message with status preserved - -4. Empty upstream body behavior -- Setup: upstream error response has status but empty body -- Expected: status preserved and default message used - -5. Status source precedence -- Setup: error object has multiple status fields (`statusCode`, `status`) -- Expected: precedence is deterministic and documented diff --git a/routes/__tests__/index.test.js b/routes/__tests__/index.test.js deleted file mode 100644 index eb05995..0000000 --- a/routes/__tests__/index.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import request from "supertest" -import { jest } from "@jest/globals" -import app from "../../app.js" - -beforeEach(() => { - // This comes from tokens.js in the app.js import. This apps tries to read env.ACCESS_TOKEN to refresh expired tokens. - // We don't care whether or not the token is expired here, so let's just state we don't care about tokens in tests. - updateExpiredToken = jest.fn(() => true) - isTokenExpired = jest.fn(() => false) -}) - -afterEach(() => { - /** - * Food for thought: delete data generated by tests? - * Make a test.store available that uses the same annotationStoreTesting as RERUM tests? - */ -}) - -describe("Make sure TinyNode demo interface is present. __core", () => { - it("/index.html", async () => { - const response = await request(app) - .get("/index.html") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.header["content-type"]).toMatch(/html/) - }) -}) diff --git a/routes/__tests__/mount.test.js b/routes/__tests__/mount.test.js deleted file mode 100644 index 002a0dc..0000000 --- a/routes/__tests__/mount.test.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Express Route Detection - * - * This approach checks routes without making HTTP requests by - * directly inspecting the Express app's routing table. - */ - -import request from "supertest" -import { jest } from "@jest/globals" -import fs from "fs" -import app from "../../app.js" - -beforeEach(() => { - // This comes from tokens.js in the app.js import. This apps tries to read env.ACCESS_TOKEN to refresh expired tokens. - // We don't care whether or not the token is expired here, so let's just state we don't care about tokens in tests. - updateExpiredToken = jest.fn(() => true) - isTokenExpired = jest.fn(() => false) -}) - -afterEach(() => { - /** - * Food for thought: delete data generated by tests? - * Make a test.store available that uses the same annotationStoreTesting as RERUM tests? - */ -}) - -/** - * Check if a route exists in the Express app - * Routes are checked by testing the matcher functions on each layer. - * - * @param {Array} routes - Array of route paths to check - * @returns {boolean} - True if all routes are found - */ -function routeExists(routes) { - const foundRoutes = new Set() - const scanStack = (stack = []) => { - for (const layer of stack) { - if (layer.matchers && layer.matchers[0]) { - const matcher = layer.matchers[0] - for (const route of routes) { - if (matcher(route)) foundRoutes.add(route) - } - } - if (layer.regexp) { - for (const route of routes) { - if (layer.regexp.test(route)) foundRoutes.add(route) - } - } - if (layer.route && layer.route.path) { - for (const route of routes) { - if (layer.route.path === route) foundRoutes.add(route) - } - } - if (layer.handle?.stack) { - scanStack(layer.handle.stack) - } - } - } - - scanStack(app._router?.stack) - // Check if all expected routes were found - return routes.every(route => foundRoutes.has(route)) -} - -/** - * This test suite uses the built app.js app and checks that the expected create endpoints are registered. - * - /create - * - /app/create - */ -describe("Check that the expected TinyNode create route patterns are registered.", () => { - it("'/app/create' and '/create' are registered routes in the app. __exists __core", () => { - const routes = ["/create", "/app/create"] - const exists = routeExists(routes) - expect(exists).toBe(true) - }) -}) - -/** - * This test suite uses the built app.js app and checks that the expected query endpoints are registered. - * - /query - * - /app/query - */ -describe("Check that the expected TinyNode query route patterns are registered.", () => { - it("'/app/query' and '/query' are registered routes in the app. __exists __core", () => { - const routes = ["/query", "/app/query"] - const exists = routeExists(routes) - expect(exists).toBe(true) - }) -}) - -/** - * This test suite uses the built app.js app and checks that the expected update endpoints are registered. - * - /update - * - /app/update - */ -describe("Check that the expected TinyNode update route patterns are registered.", () => { - it("'/app/update' and '/update' are registered routes in the app. __exists __core", () => { - const routes = ["/update", "/app/update"] - const exists = routeExists(routes) - expect(exists).toBe(true) - }) -}) - - -/** - * This test suite uses the built app.js app and checks that the expected overwrite endpoints are registered. - * - /overwrite - * - /app/overwrite - */ -describe("Check that the expected TinyNode overwrite route patterns are registered.", () => { - it("'/app/overwrite' and '/overwrite' are registered routes in the app. __exists __core", () => { - const routes = ["/overwrite", "/app/overwrite"] - const exists = routeExists(routes) - expect(exists).toBe(true) - }) -}) - -/** - * This test suite uses the built app.js app and checks that the expected delete endpoints are registered. - * - /delete - * - /app/delete - */ -describe("Combined unit tests for the '/delete' route.", () => { - it("'/app/delete' and '/delete' are registered routes in the app. __exists __core", () => { - const routes = ["/delete", "/app/delete"] - const exists = routeExists(routes) - expect(exists).toBe(true) - }) -}) - -describe('Check to see that critical repo files are present', () => { - it('root folder files', () => { - const filePath = './' // Replace with the actual file path - expect(fs.existsSync(filePath+"CODEOWNERS")).toBeTruthy() - expect(fs.existsSync(filePath+"CONTRIBUTING.md")).toBeTruthy() - expect(fs.existsSync(filePath+"README.md")).toBeTruthy() - expect(fs.existsSync(filePath+".gitignore")).toBeTruthy() - expect(fs.existsSync(filePath+"jest.config.js")).toBeTruthy() - expect(fs.existsSync(filePath+"package.json")).toBeTruthy() - }) -}) diff --git a/routes/__tests__/overwrite-route.testcase.md b/routes/__tests__/overwrite-route.testcase.md deleted file mode 100644 index d5fe536..0000000 --- a/routes/__tests__/overwrite-route.testcase.md +++ /dev/null @@ -1,29 +0,0 @@ -# Overwrite Route Test Cases - -These are planned coverage cases only. They are intentionally harness-agnostic. - -## Scope - -Target: `PUT /overwrite` and `PUT /app/overwrite` - -## Missing Coverage - -1. Conflict (`409`) passthrough -- Setup: upstream returns `409` with JSON current version payload -- Expected: TinyNode returns `409` and same JSON payload - -2. Header precedence contract -- Setup: both `If-Overwritten-Version` header and `__rerum.isOverwritten` body value are supplied -- Expected: documented precedence is consistently enforced - -3. Non-JSON upstream error response -- Setup: upstream returns non-2xx with missing or non-JSON content type -- Expected: shared messenger handles without local exception - -4. Network failure mapping -- Setup: fetch rejects before upstream response -- Expected: `502 Bad Gateway` - -5. Legacy route parity -- Setup: same conflict/error scenarios against `/app/overwrite` -- Expected: parity with `/overwrite` diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js deleted file mode 100644 index 6ee5ae4..0000000 --- a/routes/__tests__/overwrite.test.js +++ /dev/null @@ -1,132 +0,0 @@ - -import { jest } from "@jest/globals" -import express from "express" -import request from "supertest" -import overwriteRoute from "../overwrite.js" -//import app from "../../app.js" - -const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) -routeTester.use("/overwrite", overwriteRoute) -routeTester.use("/app/overwrite", overwriteRoute) - -const rerum_tiny_test_obj_id = `${process.env.RERUM_ID_PATTERN}tiny_tester` - -beforeEach(() => { - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({ "@id": rerum_tiny_test_obj_id, "testing": "item", "__rerum": { "stuff": "here" } }), - ok: true, - text: () => Promise.resolve("Descriptive Error Here") - }) - ) -}) - -afterEach(() => { - -}) - -/** - * This test suite runs the logic of the route file 'overwrite.js' but does not actually communicate with RERUM. - * It will confirm the following: - * - Is the express req/resp sent into the route - * - Can the route read the JSON body - * - Does the route add @id and __rerum - * - Does the route respond 200 - * - Does the route respond with the object that was in the request body - * - Does the route respond with the proper 'Location' header - * - * Note: /app/overwrite uses the same logic and would be a redundant test. - */ -describe("Check that the request/response behavior of the TinyNode overwrite route functions. Mock the connection to RERUM. __mock_functions", () => { - it("'/overwrite' route request and response behavior is functioning.", async () => { - const response = await request(routeTester) - .put("/overwrite") - .send({ "@id": rerum_tiny_test_obj_id, "testing": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - if (response.header.location !== rerum_tiny_test_obj_id) { - throw new Error(`Expected Location header to be '${rerum_tiny_test_obj_id}', but got '${response.header.location}'.\nAll headers: ${JSON.stringify(response.header)}\nResponse body: ${JSON.stringify(response.body)}`) - } - expect(response.statusCode).toBe(200) - expect(response.body.testing).toBe("item") - }) -}) - -/** - * This test suite checks the RESTful responses when using the TinyNode overwrite endpoint incorrectly. - * - * - Incorrect HTTP method - * - Invalid JSON body - * - * Note: /app/overwrite uses the same logic and would be a redundant test. - */ -describe("Check that incorrect TinyNode overwrite route usage results in expected RESTful responses from RERUM. __rest __core", () => { - it("Incorrect '/overwrite' route usage has expected RESTful responses.", async () => { - let response = null - - response = await request(routeTester) - .get("/overwrite") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .post("/overwrite") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .patch("/overwrite") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .delete("/overwrite") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - //Bad request body - response = await request(routeTester) - .put("/overwrite") - .set("Content-Type", "application/json") - .send("not json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .put("/overwrite") - .set("Content-Type", "application/json") - .send({ "no": "@id" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - }) -}) - -/** - * Full integration test. Checks the TinyNode app overwrite endpoint functionality and RERUM connection. - * - * Note: /app/update uses the same logic. - */ -describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => { - it("'/overwrite' route can overwrite an object in RERUM.", async () => { - const response = await request(routeTester) - .put("/overwrite") - .send({ "@id": rerum_tiny_test_obj_id, "testing": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header).toHaveProperty("location") - expect(response.header.location).toBe(rerum_tiny_test_obj_id) - expect(response.statusCode).toBe(200) - expect(response.body.testing).toBe("item") - }) -}) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js deleted file mode 100644 index f116bb4..0000000 --- a/routes/__tests__/query.test.js +++ /dev/null @@ -1,159 +0,0 @@ -import express from "express" -import request from "supertest" -import { jest } from "@jest/globals" -import queryRoute from "../query.js" -import { messenger } from "../../error-messenger.js" -//import app from "../../app.js" - -const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) -routeTester.use("/query", queryRoute) -routeTester.use("/app/query", queryRoute) -routeTester.use(messenger) - -const rerum_uri = `${process.env.RERUM_ID_PATTERN}_not_` - -beforeEach(() => { - /** - * Request/Response Mock Using manual fetch replacement - * This is overruling the fetch(store.rerum.io/v1/api/query) call in query.js - */ - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve([{ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }]), - ok: true, - text: () => Promise.resolve("Descriptive Error Here") - }) - ) -}) - -/** - * This test suite runs the logic of the route file 'query.js' but does not actually communicate with RERUM. - * It will confirm the following: - * - Is the express req/resp sent into the route - * - Can the route read the JSON body - * - Does the route add @id and __rerum - * - Does the route respond 201 - * - Does the route respond with the object that was in the request body - * - Does the route respond with the proper 'Location' header - * - * Note: /app/query uses the same logic and would be a redundant test. - */ -describe("Check that the request/response behavior of the TinyNode query route functions. Mock the connection to RERUM. __mock_functions", () => { - it("'/query' route request and response behavior is functioning.", async () => { - const response = await request(routeTester) - .post("/query") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.body[0].test).toBe("item") - }) -}) - -/** - * This test suite checks the RESTful responses when using the TinyNode query endpoint incorrectly. - * - * - Incorrect HTTP method - * - Invalid JSON body - * - * Note: /app/query uses the same logic and would be a redundant test. - */ -describe("Check that incorrect TinyNode query route usage results in expected RESTful responses from RERUM. __rest __core", () => { - it("Incorrect '/query' route usage has expected RESTful responses.", async () => { - let response = null - - response = await request(routeTester) - .get("/query") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .put("/query") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .patch("/query") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .delete("/query") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send("not json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send({}) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .post("/query") - .set("Content-Type", "application/json") - .send([]) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .post("/query?limit=-1") - .set("Content-Type", "application/json") - .send({ "test": "item" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .post("/query?skip=abc") - .set("Content-Type", "application/json") - .send({ "test": "item" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .post("/query") - .set("Content-Type", "text/plain") - .send("plain text") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(415) - - }) -}) - -/** - * Full integration test. Checks the TinyNode app query endpoint functionality and RERUM connection. - * - * Note: /app/query uses the same logic and would be a redundant test. - */ -describe("Check that the properly used query endpoints function and interact with RERUM. __e2e", () => { - it("'/query' route can save an object to RERUM.", async () => { - const response = await request(routeTester) - .post("/query") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.body[0].test).toBe("item") - }) -}) diff --git a/routes/__tests__/update-route.testcase.md b/routes/__tests__/update-route.testcase.md deleted file mode 100644 index 4b280bc..0000000 --- a/routes/__tests__/update-route.testcase.md +++ /dev/null @@ -1,29 +0,0 @@ -# Update Route Test Cases - -These are planned coverage cases only. They are intentionally harness-agnostic. - -## Scope - -Target: `PUT /update` and `PUT /app/update` - -## Missing Coverage - -1. Upstream non-2xx text error passthrough -- Setup: upstream update returns text/plain error and non-2xx status -- Expected: TinyNode returns same status and text - -2. Upstream JSON error passthrough -- Setup: upstream update returns JSON error payload and non-2xx status -- Expected: TinyNode returns same status and JSON body - -3. Network failure mapping -- Setup: fetch rejects before upstream response -- Expected: `502 Bad Gateway` - -4. Identifier edge cases -- Setup: body contains malformed `@id` value (type mismatch, blank string) -- Expected: explicit `400` validation response - -5. Response shape consistency -- Setup: successful update returns object -- Expected: `200`, `Location` header populated, JSON response body diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js deleted file mode 100644 index 1ddc3fc..0000000 --- a/routes/__tests__/update.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import express from "express" -import request from "supertest" -import { jest } from "@jest/globals" -import updateRoute from "../update.js" -//import app from "../../app.js" - -const routeTester = new express() -routeTester.use(express.json()) -routeTester.use(express.urlencoded({ extended: false })) -routeTester.use("/update", updateRoute) -routeTester.use("/app/update", updateRoute) - -const rerum_uri_orig = `${process.env.RERUM_ID_PATTERN}_not_` -const rerum_uri_updated = `${process.env.RERUM_ID_PATTERN}_updated_` -const rerum_tiny_test_obj_id = `${process.env.RERUM_ID_PATTERN}tiny_tester` - -beforeEach(() => { - /** - * Request/Response Mock Using manual fetch replacement - * This is overruling the fetch(store.rerum.io/v1/api/create) call in update.js - */ - global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({ "@id": rerum_uri_updated, "testing": "item", "__rerum": { "stuff": "here" } }), - ok: true, - text: () => Promise.resolve("Descriptive Error Here") - }) - ) -}) - -afterEach(() => { - /** - * Food for thought: delete data generated by tests? - * Make a test.store available that uses the same annotationStoreTesting as RERUM tests? - */ -}) - -/** - * This test suite runs the logic of the route file 'update.js' but does not actually communicate with RERUM. - * It will confirm the following: - * - Is the express req/resp sent into the route - * - Can the route read the JSON body - * - Does the route add @id and __rerum - * - Does the route respond 200 - * - Does the route respond with the object that was in the request body - * - Does the route respond with the proper 'Location' header - * - * Note: /app/update uses the same logic and would be a redundant test. - */ -describe("Check that the request/response behavior of the TinyNode update route functions. Mock the connection to RERUM. __mock_functions", () => { - it("'/update' route request and response behavior is functioning.", async () => { - const response = await request(routeTester) - .put("/update") - .send({ "@id": rerum_uri_orig, "testing": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(rerum_uri_updated) - expect(response.statusCode).toBe(200) - expect(response.body.testing).toBe("item") - }) -}) - -/** - * This test suite checks the RESTful responses when using the TinyNode update endpoint incorrectly. - * - * - Incorrect HTTP method - * - Invalid JSON body - * - * Note: /app/update uses the same logic and would be a redundant test. - */ -describe("Check that incorrect TinyNode update route usage results in expected RESTful responses from RERUM. __rest __core", () => { - it("Incorrect '/update' route usage has expected RESTful responses.", async () => { - let response = null - - response = await request(routeTester) - .get("/update") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .post("/update") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .patch("/update") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - response = await request(routeTester) - .delete("/update") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(405) - - //Bad request body - response = await request(routeTester) - .put("/update") - .set("Content-Type", "application/json") - .send("not json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - response = await request(routeTester) - .put("/update") - .set("Content-Type", "application/json") - .send({ "no": "@id" }) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(400) - - }) -}) - -/** - * Full integration test. Checks the TinyNode app update endpoint functionality and RERUM connection. - * - * Note: /app/update uses the same logic and would be a redundant test. - */ -describe("Check that the properly used update endpoints function and interact with RERUM. __e2e", () => { - it("'/update' route can update an object in RERUM.", async () => { - const response = await request(routeTester) - .put("/update") - .send({ "@id": rerum_tiny_test_obj_id, "testing": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header).toHaveProperty("location") - expect(response.header.location).not.toBe(rerum_tiny_test_obj_id) - expect(response.statusCode).toBe(200) - expect(response.body.testing).toBe("item") - }) -}) \ No newline at end of file diff --git a/test/helpers/env.js b/test/helpers/env.js new file mode 100644 index 0000000..09f7f0d --- /dev/null +++ b/test/helpers/env.js @@ -0,0 +1,7 @@ +process.env.NODE_ENV ??= "test" +process.env.DOTENV_CONFIG_QUIET ??= "true" +process.env.RERUM_API_ADDR ??= "https://devstore.rerum.io/v1/api/" +process.env.RERUM_ID_PATTERN ??= "https://devstore.rerum.io/v1/id/" +process.env.ORIGIN ??= "http://localhost" +process.env.ACCESS_TOKEN ??= "token" +process.env.OPEN_API_CORS ??= "false" diff --git a/test/routes/create.test.js b/test/routes/create.test.js new file mode 100644 index 0000000..d7655db --- /dev/null +++ b/test/routes/create.test.js @@ -0,0 +1,109 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import express from "express" +import request from "supertest" +import createRoute from "../../routes/create.js" +import { messenger } from "../../error-messenger.js" + +const routeTester = express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) +routeTester.use("/create", createRoute) +routeTester.use("/app/create", createRoute) +routeTester.use(messenger) + +const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + json: async () => ({ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +describe("Check that the request/response behavior of the TinyNode create route functions. __mock_functions", () => { + it("'/create' route request and response behavior is functioning.", async () => { + const response = await request(routeTester) + .post("/create") + .send({ test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 201) + assert.equal(response.header.location, rerumUri) + assert.equal(response.body.test, "item") + }) +}) + +describe("Check that incorrect TinyNode create route usage results in expected RESTful responses from RERUM. __rest __core", () => { + it("Incorrect '/create' route usage has expected RESTful responses.", async () => { + let response = await request(routeTester).get("/create") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).put("/create") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).patch("/create") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).delete("/create") + assert.equal(response.statusCode, 405) + + response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send("not json") + assert.equal(response.statusCode, 400) + + }) +}) + +describe("Check that TinyNode create route propagates upstream and network errors predictably. __rest __core", () => { + it("Preserves upstream text errors and maps network failures to 502.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + headers: { + get: () => "text/plain; charset=utf-8" + }, + text: async () => "Upstream create failure" + }) + + let response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream create failure/) + + global.fetch = async () => { + throw new Error("socket hang up") + } + + response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) +}) + +describe("Check that the properly used create endpoints function and interact with RERUM. __e2e", () => { + it("'/create' route can save an object to RERUM.", async () => { + const response = await request(routeTester) + .post("/create") + .send({ test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 201) + assert.ok(response.header.location) + assert.equal(response.body.test, "item") + }) +}) diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js new file mode 100644 index 0000000..4a3753d --- /dev/null +++ b/test/routes/delete.test.js @@ -0,0 +1,82 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import express from "express" +import request from "supertest" +import deleteRoute from "../../routes/delete.js" + +const routeTester = express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) +routeTester.use("/delete", deleteRoute) +routeTester.use("/app/delete", deleteRoute) + +const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + text: async () => "", + ok: true + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +describe("Check that the request/response behavior of the TinyNode delete route functions. Mock the connection to RERUM. __mock_functions", () => { + it("'/delete' route request and response behavior is functioning.", async () => { + let response = await request(routeTester) + .delete("/delete") + .send({ "@id": rerumUri, test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 204) + + response = await request(routeTester) + .delete("/delete/00000") + + assert.equal(response.statusCode, 204) + }) +}) + +describe("Check that incorrect TinyNode delete route usage results in expected RESTful responses from RERUM. __rest __core", () => { + it("Incorrect '/delete' route usage has expected RESTful responses.", async () => { + let response = await request(routeTester).get("/delete") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).put("/delete") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).patch("/delete") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).post("/delete") + assert.equal(response.statusCode, 405) + + response = await request(routeTester) + .delete("/delete") + .set("Content-Type", "application/json") + .send({}) + assert.equal(response.statusCode, 400) + }) +}) + +describe("Delete network failure and passthrough behavior. __rest __core", () => { + it("Maps rejected fetch to 502 for body and path delete forms.", async () => { + global.fetch = async () => { + throw new Error("socket hang up") + } + + let response = await request(routeTester) + .delete("/delete") + .set("Content-Type", "application/json") + .send({ "@id": rerumUri }) + assert.equal(response.statusCode, 502) + + response = await request(routeTester) + .delete("/delete/00000") + assert.equal(response.statusCode, 502) + }) +}) diff --git a/test/routes/error-messenger.test.js b/test/routes/error-messenger.test.js new file mode 100644 index 0000000..3963b14 --- /dev/null +++ b/test/routes/error-messenger.test.js @@ -0,0 +1,70 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import express from "express" +import request from "supertest" +import { messenger } from "../../error-messenger.js" + +function appWith(routeHandler) { + const app = express() + app.get("/test", routeHandler) + app.use(messenger) + return app +} + +describe("Check shared error messenger behavior. __rest __core", () => { + it("Returns structured JSON error bodies when upstream responds with JSON.", async () => { + const app = appWith((req, res, next) => { + next({ + status: 422, + headers: { get: () => "application/json" }, + json: async () => ({ message: "Invalid payload", field: "name" }) + }) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 422) + assert.equal(response.body.message, "Invalid payload") + assert.equal(response.body.field, "name") + }) + + it("Falls back to 500 for plain Error objects.", async () => { + const app = appWith((req, res, next) => { + next(new Error("boom")) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 500) + assert.match(response.text, /boom/) + }) + + it("Uses fallback message if .text() throws.", async () => { + const app = appWith((req, res, next) => { + next({ + status: 502, + message: "Upstream unavailable", + headers: { get: () => "text/plain" }, + text: async () => { + throw new Error("consumed") + } + }) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream unavailable/) + }) + + it("Returns structured payload when error carries payload.", async () => { + const app = appWith((req, res, next) => { + next({ + status: 400, + payload: { code: "BAD_INPUT", detail: "Missing @id" } + }) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 400) + assert.equal(response.body.code, "BAD_INPUT") + }) +}) diff --git a/test/routes/index.test.js b/test/routes/index.test.js new file mode 100644 index 0000000..e00f637 --- /dev/null +++ b/test/routes/index.test.js @@ -0,0 +1,13 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import request from "supertest" +import app from "../../app.js" + +describe("Make sure TinyNode demo interface is present. __core", () => { + it("/index.html", async () => { + const response = await request(app).get("/index.html") + assert.equal(response.statusCode, 200) + assert.match(response.header["content-type"], /html/) + }) +}) diff --git a/test/routes/mount.test.js b/test/routes/mount.test.js new file mode 100644 index 0000000..7900977 --- /dev/null +++ b/test/routes/mount.test.js @@ -0,0 +1,69 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import fs from "node:fs" +import request from "supertest" +import app from "../../app.js" + +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + ok: false, + status: 401, + text: async () => "Unauthorized", + headers: { get: () => "text/plain" } + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +async function routeExists(routes, method = "post") { + const responses = await Promise.all( + routes.map(route => request(app)[method](route).set("Content-Type", "application/json").send({})) + ) + return responses.every(response => response.statusCode !== 404) +} + +describe("Check that the expected TinyNode create route patterns are registered.", () => { + it("'/app/create' and '/create' are registered routes in the app. __exists __core", async () => { + assert.equal(await routeExists(["/create", "/app/create"]), true) + }) +}) + +describe("Check that the expected TinyNode query route patterns are registered.", () => { + it("'/app/query' and '/query' are registered routes in the app. __exists __core", async () => { + assert.equal(await routeExists(["/query", "/app/query"]), true) + }) +}) + +describe("Check that the expected TinyNode update route patterns are registered.", () => { + it("'/app/update' and '/update' are registered routes in the app. __exists __core", async () => { + assert.equal(await routeExists(["/update", "/app/update"], "put"), true) + }) +}) + +describe("Check that the expected TinyNode overwrite route patterns are registered.", () => { + it("'/app/overwrite' and '/overwrite' are registered routes in the app. __exists __core", async () => { + assert.equal(await routeExists(["/overwrite", "/app/overwrite"], "put"), true) + }) +}) + +describe("Combined unit tests for the '/delete' route.", () => { + it("'/app/delete' and '/delete' are registered routes in the app. __exists __core", async () => { + assert.equal(await routeExists(["/delete", "/app/delete"], "delete"), true) + }) +}) + +describe("Check to see that critical repo files are present", () => { + it("root folder files", () => { + const filePath = "./" + assert.equal(fs.existsSync(`${filePath}CODEOWNERS`), true) + assert.equal(fs.existsSync(`${filePath}CONTRIBUTING.md`), true) + assert.equal(fs.existsSync(`${filePath}README.md`), true) + assert.equal(fs.existsSync(`${filePath}.gitignore`), true) + assert.equal(fs.existsSync(`${filePath}package.json`), true) + }) +}) diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js new file mode 100644 index 0000000..c1b7336 --- /dev/null +++ b/test/routes/overwrite.test.js @@ -0,0 +1,101 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import express from "express" +import request from "supertest" +import overwriteRoute from "../../routes/overwrite.js" + +const routeTester = express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) +routeTester.use("/overwrite", overwriteRoute) +routeTester.use("/app/overwrite", overwriteRoute) + +const rerumTinyTestObjId = `${process.env.RERUM_ID_PATTERN}tiny_tester` +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + json: async () => ({ "@id": rerumTinyTestObjId, testing: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +describe("Check that the request/response behavior of the TinyNode overwrite route functions. Mock the connection to RERUM. __mock_functions", () => { + it("'/overwrite' route request and response behavior is functioning.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.header.location, rerumTinyTestObjId) + assert.equal(response.body.testing, "item") + }) +}) + +describe("Check that incorrect TinyNode overwrite route usage results in expected RESTful responses from RERUM. __rest __core", () => { + it("Incorrect '/overwrite' route usage has expected RESTful responses.", async () => { + let response = await request(routeTester).get("/overwrite") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).post("/overwrite") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).patch("/overwrite") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).delete("/overwrite") + assert.equal(response.statusCode, 405) + + response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send("not json") + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send({ no: "@id" }) + assert.equal(response.statusCode, 400) + }) +}) + +describe("Overwrite conflict and header contract behavior. __rest __core", () => { + it("Returns upstream 409 payload as JSON.", async () => { + global.fetch = async () => ({ + ok: false, + status: 409, + json: async () => ({ message: "Version conflict", current: "v2" }), + text: async () => "Version conflict" + }) + + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 409) + assert.equal(response.body.message, "Version conflict") + }) +}) + +describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => { + it("'/overwrite' route can overwrite an object in RERUM.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.ok(response.header.location) + assert.equal(response.header.location, rerumTinyTestObjId) + assert.equal(response.body.testing, "item") + }) +}) diff --git a/test/routes/query.test.js b/test/routes/query.test.js new file mode 100644 index 0000000..9e20c49 --- /dev/null +++ b/test/routes/query.test.js @@ -0,0 +1,105 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import express from "express" +import request from "supertest" +import queryRoute from "../../routes/query.js" +import { messenger } from "../../error-messenger.js" + +const routeTester = express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) +routeTester.use("/query", queryRoute) +routeTester.use("/app/query", queryRoute) +routeTester.use(messenger) + +const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + json: async () => ([{ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }]), + ok: true, + text: async () => "Descriptive Error Here" + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +describe("Check that the request/response behavior of the TinyNode query route functions. Mock the connection to RERUM. __mock_functions", () => { + it("'/query' route request and response behavior is functioning.", async () => { + const response = await request(routeTester) + .post("/query") + .send({ test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.body[0].test, "item") + }) +}) + +describe("Check that incorrect TinyNode query route usage results in expected RESTful responses from RERUM. __rest __core", () => { + it("Incorrect '/query' route usage has expected RESTful responses.", async () => { + let response = await request(routeTester).get("/query") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).put("/query") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).patch("/query") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).delete("/query") + assert.equal(response.statusCode, 405) + + response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send("not json") + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({}) + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send([]) + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .post("/query?limit=-1") + .set("Content-Type", "application/json") + .send({ test: "item" }) + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .post("/query?skip=abc") + .set("Content-Type", "application/json") + .send({ test: "item" }) + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .post("/query") + .set("Content-Type", "text/plain") + .send("plain text") + assert.equal(response.statusCode, 415) + }) +}) + +describe("Check that the properly used query endpoints function and interact with RERUM. __e2e", () => { + it("'/query' route can save an object to RERUM.", async () => { + const response = await request(routeTester) + .post("/query") + .send({ test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.body[0].test, "item") + }) +}) diff --git a/test/routes/update.test.js b/test/routes/update.test.js new file mode 100644 index 0000000..aa412ea --- /dev/null +++ b/test/routes/update.test.js @@ -0,0 +1,84 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import express from "express" +import request from "supertest" +import updateRoute from "../../routes/update.js" + +const routeTester = express() +routeTester.use(express.json()) +routeTester.use(express.urlencoded({ extended: false })) +routeTester.use("/update", updateRoute) +routeTester.use("/app/update", updateRoute) + +const rerumUriOrig = `${process.env.RERUM_ID_PATTERN}_not_` +const rerumUriUpdated = `${process.env.RERUM_ID_PATTERN}_updated_` +const rerumTinyTestObjId = `${process.env.RERUM_ID_PATTERN}tiny_tester` +const originalFetch = global.fetch + +beforeEach(() => { + global.fetch = async () => ({ + json: async () => ({ "@id": rerumUriUpdated, testing: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + }) +}) + +afterEach(() => { + global.fetch = originalFetch +}) + +describe("Check that the request/response behavior of the TinyNode update route functions. Mock the connection to RERUM. __mock_functions", () => { + it("'/update' route request and response behavior is functioning.", async () => { + const response = await request(routeTester) + .put("/update") + .send({ "@id": rerumUriOrig, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.header.location, rerumUriUpdated) + assert.equal(response.body.testing, "item") + }) +}) + +describe("Check that incorrect TinyNode update route usage results in expected RESTful responses from RERUM. __rest __core", () => { + it("Incorrect '/update' route usage has expected RESTful responses.", async () => { + let response = await request(routeTester).get("/update") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).post("/update") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).patch("/update") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).delete("/update") + assert.equal(response.statusCode, 405) + + response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send("not json") + assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ no: "@id" }) + assert.equal(response.statusCode, 400) + }) +}) + +describe("Check that the properly used update endpoints function and interact with RERUM. __e2e", () => { + it("'/update' route can update an object in RERUM.", async () => { + const response = await request(routeTester) + .put("/update") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.ok(response.header.location) + assert.notEqual(response.header.location, rerumTinyTestObjId) + assert.equal(response.body.testing, "item") + }) +}) From 7ac283ade6c2cae8cc8e5dd89feed165e3f8a6ae Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 13 Apr 2026 14:21:40 -0500 Subject: [PATCH 02/25] Add Playwright e2e smoke tests and CI workflow Introduce a GitHub Actions test matrix (fast and full gates) to run CI test groups. Add a Playwright-based browser smoke test (test/e2e/ui-smoke.test.js) that launches the app and verifies basic UI flows. Update README with local instructions for running e2e (install browsers) and the ci:fast/ci:full command groups. --- .github/workflows/tests.yaml | 53 +++++++++++++++++++++++++++++++ README.md | 12 ++++++++ package-lock.json | 45 +++++++++++++++++++++++++++ test/e2e/ui-smoke.test.js | 60 ++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 .github/workflows/tests.yaml create mode 100644 test/e2e/ui-smoke.test.js diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..5194478 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,53 @@ +name: TinyNode Test Matrix + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + fast: + name: Fast Test Gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run fast suites + run: npm run ci:fast + + full: + name: Full Test Gate + needs: fast + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browser + run: npm run e2e:install + + - name: Run full suite and coverage + run: npm run ci:full diff --git a/README.md b/README.md index d53f757..a1065d3 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,18 @@ npm run existsTests npm run functionalTests ``` +Run browser smoke tests (optional for local development): +```shell +npm run e2e:install +npm run E2Etests +``` + +Run the CI-equivalent command groups locally: +```shell +npm run ci:fast +npm run ci:full +``` + And start the app ```shell npm start diff --git a/package-lock.json b/package-lock.json index 760cbbd..eafed6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ }, "devDependencies": { "c8": "^10.1.3", + "playwright": "^1.54.2", "supertest": "^7.1.4" }, "engines": { @@ -962,6 +963,20 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1559,6 +1574,36 @@ "url": "https://opencollective.com/express" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/test/e2e/ui-smoke.test.js b/test/e2e/ui-smoke.test.js new file mode 100644 index 0000000..7705f54 --- /dev/null +++ b/test/e2e/ui-smoke.test.js @@ -0,0 +1,60 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { after, before, describe, it } from "node:test" +import http from "node:http" +import { chromium } from "playwright" +import app from "../../app.js" + +let server +let baseUrl + +before(async () => { + server = http.createServer(app) + await new Promise(resolve => { + server.listen(0, "127.0.0.1", resolve) + }) + const address = server.address() + baseUrl = `http://127.0.0.1:${address.port}` +}) + +after(async () => { + if (!server) return + await new Promise(resolve => { + server.close(() => resolve()) + }) +}) + +describe("TinyNode browser smoke checks. __e2e", () => { + it("Loads index page and toggles forms from the button panel. __e2e", async t => { + let browser + try { + browser = await chromium.launch({ headless: true }) + } + catch (error) { + t.skip(`Chromium is not installed. Run 'npm run e2e:install'. ${error.message}`) + return + } + + try { + const page = await browser.newPage() + await page.goto(`${baseUrl}/index.html`, { waitUntil: "domcontentloaded" }) + await page.waitForSelector(".button-panel") + + const title = await page.title() + assert.match(title, /Tiny Things by Rerum/i) + + const createIsVisibleByDefault = await page.locator("form#create:not([data-hidden])").count() + assert.equal(createIsVisibleByDefault, 1) + + await page.click('.button-panel > button[data-lang-key="updateBtn"]') + const updateVisible = await page.locator("form#rerumUpdate:not([data-hidden])").count() + const createHidden = await page.locator('form#create[data-hidden="true"]').count() + + assert.equal(updateVisible, 1) + assert.equal(createHidden, 1) + } + finally { + await browser.close() + } + }) +}) From 091234bc97ebb69362354ef685dd5f3f133a6abb Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 13 Apr 2026 14:30:31 -0500 Subject: [PATCH 03/25] coverage thresholds --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1272588..7660ccc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "existsTests": "node --test --test-name-pattern=__exists test/routes/**/*.test.js", "coreTests": "node --test --test-name-pattern=__core test/routes/**/*.test.js", "functionalTests": "node --test --test-name-pattern=__mock_functions test/routes/**/*.test.js", - "coverage": "c8 --reporter=text --reporter=json --reporter=html node --test test/routes/**/*.test.js", + "coverage": "c8 --check-coverage --lines 75 --functions 80 --branches 60 --statements 75 --reporter=text --reporter=json --reporter=html node --test test/routes/**/*.test.js", "e2e:install": "playwright install --with-deps chromium", "ci:fast": "npm run coreTests && npm run existsTests && npm run functionalTests", "ci:full": "npm run e2e:install && npm run allTests && npm run coverage" From 8424ea22c25fa7ecd7b6be9d774fb87b1fce78d1 Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 13 Apr 2026 14:34:59 -0500 Subject: [PATCH 04/25] e2e(ui): add browser helper and new smoke tests Introduce launchBrowserOrSkip helper to centralize Chromium launch and skip logic when not installed, and refactor existing smoke test to use it. Add several end-to-end UI smoke tests: successful create submission (stubbing /create), client-side JSON validation that prevents network calls, query form results (stubbing /query), and overwrite conflict handling (stubbing /overwrite returning 409). Tests use Playwright routing to mock server responses and assert flash messages and object viewer output. --- test/e2e/ui-smoke.test.js | 146 +++++++++++++++++++++++++++++++++++--- 1 file changed, 138 insertions(+), 8 deletions(-) diff --git a/test/e2e/ui-smoke.test.js b/test/e2e/ui-smoke.test.js index 7705f54..f2ece05 100644 --- a/test/e2e/ui-smoke.test.js +++ b/test/e2e/ui-smoke.test.js @@ -8,6 +8,16 @@ import app from "../../app.js" let server let baseUrl +async function launchBrowserOrSkip(t) { + try { + return await chromium.launch({ headless: true }) + } + catch (error) { + t.skip(`Chromium is not installed. Run 'npm run e2e:install'. ${error.message}`) + return null + } +} + before(async () => { server = http.createServer(app) await new Promise(resolve => { @@ -26,14 +36,8 @@ after(async () => { describe("TinyNode browser smoke checks. __e2e", () => { it("Loads index page and toggles forms from the button panel. __e2e", async t => { - let browser - try { - browser = await chromium.launch({ headless: true }) - } - catch (error) { - t.skip(`Chromium is not installed. Run 'npm run e2e:install'. ${error.message}`) - return - } + const browser = await launchBrowserOrSkip(t) + if (!browser) return try { const page = await browser.newPage() @@ -57,4 +61,130 @@ describe("TinyNode browser smoke checks. __e2e", () => { await browser.close() } }) + + it("Submits create form and renders success message and object payload. __e2e", async t => { + const browser = await launchBrowserOrSkip(t) + if (!browser) return + + try { + const page = await browser.newPage() + await page.route("**/create", async route => { + await route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({ + "@id": "https://devstore.rerum.io/v1/id/e2e-created", + test: "created" + }) + }) + }) + + await page.goto(`${baseUrl}/index.html`, { waitUntil: "domcontentloaded" }) + await page.fill("#createJSON", JSON.stringify({ test: "created" })) + await page.click('form#create button[type="submit"]') + + await page.waitForSelector('#flash-message:not([style*="display: none"])') + const message = await page.locator("#flash-message").textContent() + const objectView = await page.locator("#obj-viewer").textContent() + + assert.match(message ?? "", /Created new object at/i) + assert.match(objectView ?? "", /e2e-created/) + assert.match(objectView ?? "", /"test":\s*"created"/) + } + finally { + await browser.close() + } + }) + + it("Blocks invalid create JSON in the client before issuing network calls. __e2e", async t => { + const browser = await launchBrowserOrSkip(t) + if (!browser) return + + try { + const page = await browser.newPage() + let createRequests = 0 + await page.route("**/create", async route => { + createRequests += 1 + await route.fulfill({ status: 500, body: "should not be called" }) + }) + + await page.goto(`${baseUrl}/index.html`, { waitUntil: "domcontentloaded" }) + await page.fill("#createJSON", "{not valid json}") + await page.click('form#create button[type="submit"]') + + await page.waitForSelector('#flash-message:not([style*="display: none"])') + const message = await page.locator("#flash-message").textContent() + assert.match(message ?? "", /You did not provide valid JSON/i) + assert.equal(createRequests, 0) + } + finally { + await browser.close() + } + }) + + it("Submits query form and renders returned results. __e2e", async t => { + const browser = await launchBrowserOrSkip(t) + if (!browser) return + + try { + const page = await browser.newPage() + await page.route("**/query", async route => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([ + { "@id": "https://devstore.rerum.io/v1/id/query-hit", label: "found" } + ]) + }) + }) + + await page.goto(`${baseUrl}/index.html`, { waitUntil: "domcontentloaded" }) + await page.click('.button-panel > button[data-lang-key="queryBtn"]') + await page.fill("#queryKey", "label") + await page.fill("#queryValue", "found") + await page.click('form#query button[type="submit"]') + + await page.waitForSelector('#flash-message:not([style*="display: none"])') + const message = await page.locator("#flash-message").textContent() + const objectView = await page.locator("#obj-viewer").textContent() + + assert.match(message ?? "", /matching results/i) + assert.match(objectView ?? "", /query-hit/) + assert.match(objectView ?? "", /"label":\s*"found"/) + } + finally { + await browser.close() + } + }) + + it("Shows a conflict error when overwrite returns 409. __e2e", async t => { + const browser = await launchBrowserOrSkip(t) + if (!browser) return + + try { + const page = await browser.newPage() + await page.route("**/overwrite", async route => { + await route.fulfill({ + status: 409, + statusText: "Conflict", + contentType: "application/json", + body: JSON.stringify({ message: "Version conflict", currentVersion: { "@id": "v2" } }) + }) + }) + + await page.goto(`${baseUrl}/index.html`, { waitUntil: "domcontentloaded" }) + await page.click('.button-panel > button[data-lang-key="overwriteBtn"]') + await page.fill("#overwriteId", "https://devstore.rerum.io/v1/id/overwrite-target") + await page.fill("#overwriteJSON", JSON.stringify({ testing: "overwrite" })) + await page.click('form#overwrite button[type="submit"]') + + await page.waitForSelector('#flash-message:not([style*="display: none"])') + const message = await page.locator("#flash-message").textContent() + assert.match(message ?? "", /Conflict detected while trying to overwrite object/i) + assert.match(message ?? "", /409/i) + } + finally { + await browser.close() + } + }) }) From e1f155d7d9ef78848fd40929fcd1bcc81f2296ab Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 13 Apr 2026 14:41:28 -0500 Subject: [PATCH 05/25] Update tokens.js --- tokens.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tokens.js b/tokens.js index 3f50a61..26cf467 100644 --- a/tokens.js +++ b/tokens.js @@ -5,7 +5,6 @@ import { parse, stringify } from "envfile" const sourcePath = '.env' -// https://stackoverflow.com/a/69058154/1413302 const isTokenExpired = (token) => { try { const payload = token?.split('.')?.[1] From 7ad9e49c163891e3db66b5b004d20c19ac0dd62e Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Mon, 13 Apr 2026 14:47:37 -0500 Subject: [PATCH 06/25] Update .github/workflows/tests.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/tests.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5194478..bd3fb47 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -46,8 +46,5 @@ jobs: - name: Install dependencies run: npm ci - - name: Install Playwright browser - run: npm run e2e:install - - name: Run full suite and coverage run: npm run ci:full From adb1916ed708628bf68b60c17d30de4bc981208c Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Mon, 13 Apr 2026 14:48:24 -0500 Subject: [PATCH 07/25] Update package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7660ccc..48ed4f8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "coverage": "c8 --check-coverage --lines 75 --functions 80 --branches 60 --statements 75 --reporter=text --reporter=json --reporter=html node --test test/routes/**/*.test.js", "e2e:install": "playwright install --with-deps chromium", "ci:fast": "npm run coreTests && npm run existsTests && npm run functionalTests", - "ci:full": "npm run e2e:install && npm run allTests && npm run coverage" + "ci:full": "npm run e2e:install && npm run E2Etests && npm run coverage" }, "dependencies": { "cors": "^2.8.5", From b749933bf7961679dd4830f7cf48689da3102060 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 14 Apr 2026 17:01:51 -0500 Subject: [PATCH 08/25] better test description --- TESTING_MODERNIZATION_PLAN.md | 180 ---------------------------------- test/TESTING.md | 47 +++++++++ 2 files changed, 47 insertions(+), 180 deletions(-) delete mode 100644 TESTING_MODERNIZATION_PLAN.md create mode 100644 test/TESTING.md diff --git a/TESTING_MODERNIZATION_PLAN.md b/TESTING_MODERNIZATION_PLAN.md deleted file mode 100644 index 311cc1e..0000000 --- a/TESTING_MODERNIZATION_PLAN.md +++ /dev/null @@ -1,180 +0,0 @@ -# TinyNode Testing Modernization Plan - -## Goals - -1. Move from Jest to Node's built-in test runner (`node:test`) without experimental VM flags. -2. Keep ESM native across app and tests. -3. Preserve existing test intent (`__core`, `__e2e`, `__exists`, `__mock_functions`) with a cleaner selection model. -4. Separate API availability checks from API functionality checks. -5. Convert `.testcase.md` descriptions into executable tests, then remove the markdown artifacts. -6. Provide a reusable model for other repositories. - -## Recommended Stack - -- Test runner: `node:test` -- Assertions/mocks: `node:assert/strict`, `node:test` mocks, plus explicit fakes -- HTTP endpoint testing (in-memory app): `supertest` -- Upstream HTTP mocking for `fetch`: `undici` `MockAgent` (preferred) or manual fetch stubs -- Browser/UI tests: `playwright` (preferred over Puppeteer) -- Coverage: `c8` (stable and CI-friendly) -- Optional snapshots/matchers (only if needed): keep minimal, avoid framework lock-in - -## Why Playwright Over Puppeteer - -1. Better multi-browser support (Chromium, Firefox, WebKit). -2. Strong test reliability features (auto-waiting, robust locators). -3. Excellent CI support and diagnostics (trace viewer, screenshots, videos). -4. Works cleanly with custom runners (including `node:test`) if desired. - -## Target Test Architecture - -Create a top-level `test/` folder and phase out `routes/__tests__/` over time: - -- `test/unit/`: - - Pure logic tests (`rest.js`, `tokens.js`, helper functions) -- `test/integration/`: - - Route behavior tests using `supertest` with app/router instances in memory - - Strong mocking for upstream RERUM/network behavior -- `test/contract/`: - - API response shape and header contracts - - Includes route registration/availability checks -- `test/e2e/`: - - Browser and user-flow checks (Playwright) - - Minimal critical-path scenarios -- `test/smoke/`: - - Availability tests (is app up? are endpoints mounted?) -- `test/fixtures/`: - - Reusable payloads and canned upstream responses -- `test/helpers/`: - - App factories, env setup, temporary server lifecycle, mock helpers - -## Important Architecture Adjustments - -### 1) Dependency injection at router/app boundary - -To avoid brittle module-level mocking, export route builders that accept dependencies. - -Current pattern (hard import): -- route imports `fetchRerum`, `checkAccessToken` directly. - -Preferred pattern: -- `buildCreateRouter({ fetchRerum, checkAccessToken, verifyJsonContentType })` -- default export still uses production dependencies. -- tests inject fake dependencies without test-runner-specific magic. - -This is the highest-value structural change for reliable, portable tests. - -### 2) Distinguish availability vs functionality - -- Availability tests: - - verify endpoint exists and returns expected method guards (e.g., 405 on wrong method) - - should not depend on upstream services -- Functionality tests: - - verify request transformation, upstream call behavior, response body/headers/status - - use mocked upstream behavior exhaustively - -### 3) Keep environment control explicit - -- Add dedicated test env setup (`test/helpers/env.js`) to set deterministic env vars. -- Never rely on local `.env` for test behavior. -- Ensure tests do not modify user `.env` files. - -### 4) Reduce side effects in token handling tests - -- Keep token-refresh behavior injectable or guarded so route tests do not fail due to malformed token state. - -## Mapping Existing Tags to New Commands - -Use path-based scripts and optional name filtering. - -Suggested categories: - -- `test:all` -> all suites -- `test:core` -> `test/unit` + core integration contract tests -- `test:exists` -> route registration + smoke availability tests -- `test:functional` -> mocked integration tests -- `test:e2e` -> browser tests and true end-to-end flows - -For quick local runs, rely on folder-level script filters, not test framework internals. - -## Migration Strategy (Incremental) - -### Phase 1: Foundation - -1. Add `test/` directory with helpers and one migrated sample suite. -2. Add `node:test` scripts alongside existing Jest scripts. -3. Add `c8` coverage command for new suites. - -### Phase 2: Route Suite Migration - -1. Migrate existing route tests from Jest to `node:test` one file at a time. -2. Keep test names preserving current semantic tags during migration. -3. Validate parity by running old/new suites together temporarily. - -### Phase 3: Convert `.testcase.md` to Executable Specs - -1. For each testcase markdown file, create corresponding test suite in `test/contract` or `test/integration`. -2. Keep markdown as source-of-truth only during conversion. -3. Remove `.testcase.md` once executable equivalent is merged. - -### Phase 4: UI Coverage - -1. Add Playwright and minimal smoke UI checks (page load, critical controls visible). -2. Add one interaction test per major user action. -3. Expand only for high-value user journeys. - -### Phase 5: Decommission Jest - -1. Remove Jest scripts/config/deps after parity is complete. -2. Update docs and contributor workflow. - -## CI/GitHub Actions Model - -### Pull requests - -Run fast and deterministic checks: - -1. `test:core` -2. `test:exists` -3. `test:functional` (mocked only) - -### Main branch / nightly - -Run full quality gates: - -1. `test:all` -2. `test:e2e` -3. coverage publish/report - -### Suggested safeguards - -- Fail fast on lint/type issues if enabled. -- Upload Playwright traces on failure. -- Keep browser tests isolated from unit/integration timing budgets. - -## Proposed NPM Script Direction - -(illustrative, final command syntax can be adjusted during implementation) - -- `test:all` -> `node --test test/**/*.test.js` -- `test:core` -> `node --test test/unit/**/*.test.js test/integration/**/*core*.test.js` -- `test:exists` -> `node --test test/smoke/**/*.test.js test/contract/**/*exists*.test.js` -- `test:functional` -> `node --test test/integration/**/*.test.js` -- `test:e2e` -> `node --test test/e2e/**/*.test.js` -- `coverage` -> `c8 node --test test/**/*.test.js` - -## Risks and Mitigations - -1. Risk: Migration churn while preserving behavior. - - Mitigation: side-by-side execution and parity checks per suite. -2. Risk: Mocking complexity around upstream fetch and token middleware. - - Mitigation: dependency injection and helper factories. -3. Risk: Browser test flakiness. - - Mitigation: Playwright locators, fixed test data, no arbitrary sleeps. - -## Immediate Next Steps - -1. Implement Phase 1 scaffolding and scripts. -2. Migrate one representative route suite (`create`) to validate architecture. -3. Add CI job that runs both old and new tests until parity is complete. -4. Begin converting `.testcase.md` files into executable suites. diff --git a/test/TESTING.md b/test/TESTING.md new file mode 100644 index 0000000..c09aa42 --- /dev/null +++ b/test/TESTING.md @@ -0,0 +1,47 @@ +# TinyNode Testing Guide + +## Test Structure + +TinyNode includes comprehensive unit and integration tests for local route behavior and request/response handling. Tests are organized by route and concern: + +- **routes/**: Tests for each CRUD operation (create, read, update, delete, query) +- **e2e/**: End-to-end tests including UI smoke tests +- **helpers/**: Test utilities and environment setup + +## RERUM Connection Testing + +**Important:** Actual Tiny > RERUM connections are **not currently tested** with live API calls. + +### Why RERUM Connections Aren't Tested + +Currently, RERUM does not provide a reliable testing harness that TinyNode can depend on. Therefore: + +1. All fetch calls to RERUM endpoints are **mocked** in tests +2. Tests validate TinyNode's local request/response behavior, route handling, and error messaging +3. Tests do **NOT** verify that actual RERUM API calls succeed or fail correctly + +### What This Means + +Tests will pass even if: +- RERUM API endpoints become unavailable +- RERUM API paths are broken (e.g., `/create` → `/createx`) +- RERUM response schemas change +- Network connectivity issues occur + +### Future Improvements + +Once RERUM provides a reliable test harness, TinyNode will: +1. Replace generic mocks with schema-based mocks that validate against RERUM's actual API schema +2. Add integration tests that verify real requests to a test RERUM instance +3. Improve test coverage for error handling scenarios when RERUM is unreachable or responds unexpectedly + +### Testing Mocked Behavior + +Current tests focus on: +- Request validation and marshaling +- Route response formatting +- Error message generation +- REST compliance (correct status codes, headers) +- TinyNode-specific business logic + +These are all validated using mocked fetch responses. See individual test files for mock setup details. From f6ab6385a764a3d356f0bb0377b3b8d8b17540cd Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 14 Apr 2026 17:06:06 -0500 Subject: [PATCH 09/25] Validate upstream RERUM contract in tests Capture fetch URL/options in test mocks and add assertions to verify the upstream RERUM contract (URL, HTTP method, Authorization header, Content-Type). Updates test/routes/{create,delete,overwrite,query,update}.test.js to record lastFetchUrl/lastFetchOptions and assert correct endpoint/method/headers, and documents the new validation in test/TESTING.md with an example and rationale to catch breaking changes. --- test/TESTING.md | 25 +++++++++++++++++++++++++ test/routes/create.test.js | 23 ++++++++++++++++++----- test/routes/delete.test.js | 25 +++++++++++++++++++++---- test/routes/overwrite.test.js | 23 ++++++++++++++++++----- test/routes/query.test.js | 23 ++++++++++++++++++----- test/routes/update.test.js | 23 ++++++++++++++++++----- 6 files changed, 118 insertions(+), 24 deletions(-) diff --git a/test/TESTING.md b/test/TESTING.md index c09aa42..1f9dc5b 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -28,6 +28,30 @@ Tests will pass even if: - RERUM response schemas change - Network connectivity issues occur +### Upstream Contract Validation + +To improve coverage, each route's `__mock_functions` test now **validates the upstream contract** by asserting on: + +- **URL**: Verifies the correct RERUM endpoint is called (e.g., `/create`, `/query?limit=10&skip=0`) +- **HTTP Method**: Confirms POST for create/query, PUT for update/overwrite, DELETE for delete +- **Authorization Header**: Ensures Bearer token is present and properly formatted +- **Content-Type Header**: Validates correct JSON encoding + +Example from create.test.js: +```javascript +// Verify upstream contract +assert.match(lastFetchUrl, /\/create$/, "URL should end with /create") +assert.equal(lastFetchOptions.method, "POST", "Method should be POST") +assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") +assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") +``` + +This catches breaking changes like: +- ✅ Typos in endpoint URLs (`/createx` instead of `/create`) +- ✅ Missing or incorrect Authorization headers +- ✅ Wrong HTTP methods +- ✅ Incorrect Content-Type headers + ### Future Improvements Once RERUM provides a reliable test harness, TinyNode will: @@ -43,5 +67,6 @@ Current tests focus on: - Error message generation - REST compliance (correct status codes, headers) - TinyNode-specific business logic +- Upstream contract validation (URL, method, headers) These are all validated using mocked fetch responses. See individual test files for mock setup details. diff --git a/test/routes/create.test.js b/test/routes/create.test.js index d7655db..61d5eca 100644 --- a/test/routes/create.test.js +++ b/test/routes/create.test.js @@ -15,13 +15,20 @@ routeTester.use(messenger) const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` const originalFetch = global.fetch +let lastFetchUrl, lastFetchOptions beforeEach(() => { - global.fetch = async () => ({ - json: async () => ({ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }), - ok: true, - text: async () => "Descriptive Error Here" - }) + lastFetchUrl = null + lastFetchOptions = null + global.fetch = async (url, opts) => { + lastFetchUrl = url + lastFetchOptions = opts + return { + json: async () => ({ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + } + } }) afterEach(() => { @@ -38,6 +45,12 @@ describe("Check that the request/response behavior of the TinyNode create route assert.equal(response.statusCode, 201) assert.equal(response.header.location, rerumUri) assert.equal(response.body.test, "item") + + // Verify upstream contract + assert.match(lastFetchUrl, /\/create$/, "URL should end with /create") + assert.equal(lastFetchOptions.method, "POST", "Method should be POST") + assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") + assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) }) diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index 4a3753d..ed801cd 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -13,12 +13,19 @@ routeTester.use("/app/delete", deleteRoute) const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` const originalFetch = global.fetch +let lastFetchUrl, lastFetchOptions beforeEach(() => { - global.fetch = async () => ({ - text: async () => "", - ok: true - }) + lastFetchUrl = null + lastFetchOptions = null + global.fetch = async (url, opts) => { + lastFetchUrl = url + lastFetchOptions = opts + return { + text: async () => "", + ok: true + } + } }) afterEach(() => { @@ -33,11 +40,21 @@ describe("Check that the request/response behavior of the TinyNode delete route .set("Content-Type", "application/json") assert.equal(response.statusCode, 204) + + // Verify upstream contract for body-based delete + assert.match(lastFetchUrl, /\/delete$/, "URL should end with /delete") + assert.equal(lastFetchOptions.method, "DELETE", "Method should be DELETE") + assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") + assert.equal(lastFetchOptions.headers["Content-Type"], "application/json; charset=utf-8", "Content-Type header mismatch") response = await request(routeTester) .delete("/delete/00000") assert.equal(response.statusCode, 204) + + // Verify upstream contract for path-based delete + assert.match(lastFetchUrl, /\/delete\/00000$/, "URL should end with /delete/00000") + assert.equal(lastFetchOptions.method, "DELETE", "Method should be DELETE") }) }) diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js index c1b7336..fe8d66b 100644 --- a/test/routes/overwrite.test.js +++ b/test/routes/overwrite.test.js @@ -13,13 +13,20 @@ routeTester.use("/app/overwrite", overwriteRoute) const rerumTinyTestObjId = `${process.env.RERUM_ID_PATTERN}tiny_tester` const originalFetch = global.fetch +let lastFetchUrl, lastFetchOptions beforeEach(() => { - global.fetch = async () => ({ - json: async () => ({ "@id": rerumTinyTestObjId, testing: "item", __rerum: { stuff: "here" } }), - ok: true, - text: async () => "Descriptive Error Here" - }) + lastFetchUrl = null + lastFetchOptions = null + global.fetch = async (url, opts) => { + lastFetchUrl = url + lastFetchOptions = opts + return { + json: async () => ({ "@id": rerumTinyTestObjId, testing: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + } + } }) afterEach(() => { @@ -36,6 +43,12 @@ describe("Check that the request/response behavior of the TinyNode overwrite rou assert.equal(response.statusCode, 200) assert.equal(response.header.location, rerumTinyTestObjId) assert.equal(response.body.testing, "item") + + // Verify upstream contract + assert.match(lastFetchUrl, /\/overwrite$/, "URL should end with /overwrite") + assert.equal(lastFetchOptions.method, "PUT", "Method should be PUT") + assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") + assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) }) diff --git a/test/routes/query.test.js b/test/routes/query.test.js index 9e20c49..8f08934 100644 --- a/test/routes/query.test.js +++ b/test/routes/query.test.js @@ -15,13 +15,20 @@ routeTester.use(messenger) const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` const originalFetch = global.fetch +let lastFetchUrl, lastFetchOptions beforeEach(() => { - global.fetch = async () => ({ - json: async () => ([{ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }]), - ok: true, - text: async () => "Descriptive Error Here" - }) + lastFetchUrl = null + lastFetchOptions = null + global.fetch = async (url, opts) => { + lastFetchUrl = url + lastFetchOptions = opts + return { + json: async () => ([{ "@id": rerumUri, test: "item", __rerum: { stuff: "here" } }]), + ok: true, + text: async () => "Descriptive Error Here" + } + } }) afterEach(() => { @@ -37,6 +44,12 @@ describe("Check that the request/response behavior of the TinyNode query route f assert.equal(response.statusCode, 200) assert.equal(response.body[0].test, "item") + + // Verify upstream contract + assert.match(lastFetchUrl, /\/query\?limit=10&skip=0$/, "URL should be /query with default limit and skip") + assert.equal(lastFetchOptions.method, "POST", "Method should be POST") + assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") + assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) }) diff --git a/test/routes/update.test.js b/test/routes/update.test.js index aa412ea..dbf0dc1 100644 --- a/test/routes/update.test.js +++ b/test/routes/update.test.js @@ -15,13 +15,20 @@ const rerumUriOrig = `${process.env.RERUM_ID_PATTERN}_not_` const rerumUriUpdated = `${process.env.RERUM_ID_PATTERN}_updated_` const rerumTinyTestObjId = `${process.env.RERUM_ID_PATTERN}tiny_tester` const originalFetch = global.fetch +let lastFetchUrl, lastFetchOptions beforeEach(() => { - global.fetch = async () => ({ - json: async () => ({ "@id": rerumUriUpdated, testing: "item", __rerum: { stuff: "here" } }), - ok: true, - text: async () => "Descriptive Error Here" - }) + lastFetchUrl = null + lastFetchOptions = null + global.fetch = async (url, opts) => { + lastFetchUrl = url + lastFetchOptions = opts + return { + json: async () => ({ "@id": rerumUriUpdated, testing: "item", __rerum: { stuff: "here" } }), + ok: true, + text: async () => "Descriptive Error Here" + } + } }) afterEach(() => { @@ -38,6 +45,12 @@ describe("Check that the request/response behavior of the TinyNode update route assert.equal(response.statusCode, 200) assert.equal(response.header.location, rerumUriUpdated) assert.equal(response.body.testing, "item") + + // Verify upstream contract + assert.match(lastFetchUrl, /\/update$/, "URL should end with /update") + assert.equal(lastFetchOptions.method, "PUT", "Method should be PUT") + assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") + assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) }) From 75d10c2f4c763486e4466020e1d43976e3d79480 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 14 Apr 2026 17:08:40 -0500 Subject: [PATCH 10/25] overwrite header inclusion --- test/TESTING.md | 32 +++++++++++++++++++++++ test/routes/overwrite.test.js | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/test/TESTING.md b/test/TESTING.md index 1f9dc5b..82feb0c 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -52,6 +52,38 @@ This catches breaking changes like: - ✅ Wrong HTTP methods - ✅ Incorrect Content-Type headers +### If-Overwritten-Version Header Behavior + +The overwrite route includes special handling for version conflict resolution via the `If-Overwritten-Version` header. Tests validate: + +1. **Request header passthrough** — `If-Overwritten-Version` from the request is forwarded upstream +2. **Body extraction** — `__rerum.isOverwritten` in the request body becomes the upstream header +3. **Precedence** — Body value takes priority over request header value +4. **Absence handling** — Headers are only added when values are present + +Example from overwrite.test.js: +```javascript +// Passes request header through +await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("If-Overwritten-Version", "abc123") + +assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "abc123") + +// Or extracts from body __rerum.isOverwritten +await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, __rerum: { isOverwritten: "xyz789" } }) + +assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "xyz789") +``` + +This prevents accidental changes to: +- ✅ Header passthrough logic +- ✅ Body field name (`isOverwritten` vs other names) +- ✅ Precedence between header and body sources + ### Future Improvements Once RERUM provides a reliable test harness, TinyNode will: diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js index fe8d66b..24ab33e 100644 --- a/test/routes/overwrite.test.js +++ b/test/routes/overwrite.test.js @@ -99,6 +99,54 @@ describe("Overwrite conflict and header contract behavior. __rest __core", () = }) }) +describe("Overwrite If-Overwritten-Version header behavior. __mock_functions", () => { + it("Passes through If-Overwritten-Version request header to upstream.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + .set("If-Overwritten-Version", "abc123") + + assert.equal(response.statusCode, 200) + // Verify the header was passed through to upstream + assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "abc123") + }) + + it("Extracts __rerum.isOverwritten from body as If-Overwritten-Version.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item", __rerum: { isOverwritten: "xyz789" } }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + // Verify the body value was extracted and sent as header + assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "xyz789") + }) + + it("Body __rerum.isOverwritten takes precedence over request header.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item", __rerum: { isOverwritten: "body-version" } }) + .set("Content-Type", "application/json") + .set("If-Overwritten-Version", "header-version") + + assert.equal(response.statusCode, 200) + // Body value should override header value + assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "body-version") + }) + + it("Header is sent when neither __rerum.isOverwritten nor request header is present.", async () => { + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + // Header should not be present or should be undefined + assert.strictEqual(lastFetchOptions.headers["If-Overwritten-Version"], undefined) + }) +}) + describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => { it("'/overwrite' route can overwrite an object in RERUM.", async () => { const response = await request(routeTester) From 37f15ab48f98e54ed0c33594592e5820df19f109 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 14 Apr 2026 17:18:53 -0500 Subject: [PATCH 11/25] Accept application/ld+json in tests; add 415/502 cases Update test suites to accept application/ld+json by configuring express.json({ type: ['application/json', 'application/ld+json'] }) across create, query, delete, overwrite, and update tests. Add new tests: create and query now verify requests with Content-Type application/ld+json are accepted. Add tests that sending text/plain yields 415 in create, delete, overwrite, and update. Import and mount the error messenger in delete, overwrite, and update tests. Add network-failure cases that map rejected fetch to a 502 response for overwrite and update. --- test/routes/create.test.js | 19 ++++++++++++++++++- test/routes/delete.test.js | 10 +++++++++- test/routes/overwrite.test.js | 22 +++++++++++++++++++++- test/routes/query.test.js | 12 +++++++++++- test/routes/update.test.js | 22 +++++++++++++++++++++- 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/test/routes/create.test.js b/test/routes/create.test.js index 61d5eca..f31761c 100644 --- a/test/routes/create.test.js +++ b/test/routes/create.test.js @@ -7,7 +7,7 @@ import createRoute from "../../routes/create.js" import { messenger } from "../../error-messenger.js" const routeTester = express() -routeTester.use(express.json()) +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.use(express.urlencoded({ extended: false })) routeTester.use("/create", createRoute) routeTester.use("/app/create", createRoute) @@ -52,6 +52,17 @@ describe("Check that the request/response behavior of the TinyNode create route assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) + + it("Accepts application/ld+json content type.", async () => { + const response = await request(routeTester) + .post("/create") + .send({ test: "item" }) + .set("Content-Type", "application/ld+json") + + assert.equal(response.statusCode, 201) + assert.ok(response.header.location) + assert.equal(response.body.test, "item") + }) }) describe("Check that incorrect TinyNode create route usage results in expected RESTful responses from RERUM. __rest __core", () => { @@ -74,6 +85,12 @@ describe("Check that incorrect TinyNode create route usage results in expected R .send("not json") assert.equal(response.statusCode, 400) + response = await request(routeTester) + .post("/create") + .set("Content-Type", "text/plain") + .send("plain text") + assert.equal(response.statusCode, 415) + }) }) diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index ed801cd..d2d32bb 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -4,12 +4,14 @@ import { afterEach, beforeEach, describe, it } from "node:test" import express from "express" import request from "supertest" import deleteRoute from "../../routes/delete.js" +import { messenger } from "../../error-messenger.js" const routeTester = express() -routeTester.use(express.json()) +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.use(express.urlencoded({ extended: false })) routeTester.use("/delete", deleteRoute) routeTester.use("/app/delete", deleteRoute) +routeTester.use(messenger) const rerumUri = `${process.env.RERUM_ID_PATTERN}_not_` const originalFetch = global.fetch @@ -77,6 +79,12 @@ describe("Check that incorrect TinyNode delete route usage results in expected R .set("Content-Type", "application/json") .send({}) assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .delete("/delete") + .set("Content-Type", "text/plain") + .send("plain text") + assert.equal(response.statusCode, 415) }) }) diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js index 24ab33e..3a91589 100644 --- a/test/routes/overwrite.test.js +++ b/test/routes/overwrite.test.js @@ -4,12 +4,14 @@ import { afterEach, beforeEach, describe, it } from "node:test" import express from "express" import request from "supertest" import overwriteRoute from "../../routes/overwrite.js" +import { messenger } from "../../error-messenger.js" const routeTester = express() -routeTester.use(express.json()) +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.use(express.urlencoded({ extended: false })) routeTester.use("/overwrite", overwriteRoute) routeTester.use("/app/overwrite", overwriteRoute) +routeTester.use(messenger) const rerumTinyTestObjId = `${process.env.RERUM_ID_PATTERN}tiny_tester` const originalFetch = global.fetch @@ -77,6 +79,12 @@ describe("Check that incorrect TinyNode overwrite route usage results in expecte .set("Content-Type", "application/json") .send({ no: "@id" }) assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "text/plain") + .send("plain text") + assert.equal(response.statusCode, 415) }) }) @@ -147,6 +155,18 @@ describe("Overwrite If-Overwritten-Version header behavior. __mock_functions", }) }) +describe("Overwrite network failure behavior. __rest __core", () => { + it("Maps rejected fetch to 502.", async () => { + global.fetch = async () => { throw new Error("socket hang up") } + + const response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + assert.equal(response.statusCode, 502) + }) +}) + describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => { it("'/overwrite' route can overwrite an object in RERUM.", async () => { const response = await request(routeTester) diff --git a/test/routes/query.test.js b/test/routes/query.test.js index 8f08934..f732fb5 100644 --- a/test/routes/query.test.js +++ b/test/routes/query.test.js @@ -7,7 +7,7 @@ import queryRoute from "../../routes/query.js" import { messenger } from "../../error-messenger.js" const routeTester = express() -routeTester.use(express.json()) +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.use(express.urlencoded({ extended: false })) routeTester.use("/query", queryRoute) routeTester.use("/app/query", queryRoute) @@ -51,6 +51,16 @@ describe("Check that the request/response behavior of the TinyNode query route f assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) + + it("Accepts application/ld+json content type.", async () => { + const response = await request(routeTester) + .post("/query") + .send({ test: "item" }) + .set("Content-Type", "application/ld+json") + + assert.equal(response.statusCode, 200) + assert.equal(response.body[0].test, "item") + }) }) describe("Check that incorrect TinyNode query route usage results in expected RESTful responses from RERUM. __rest __core", () => { diff --git a/test/routes/update.test.js b/test/routes/update.test.js index dbf0dc1..c082385 100644 --- a/test/routes/update.test.js +++ b/test/routes/update.test.js @@ -4,12 +4,14 @@ import { afterEach, beforeEach, describe, it } from "node:test" import express from "express" import request from "supertest" import updateRoute from "../../routes/update.js" +import { messenger } from "../../error-messenger.js" const routeTester = express() -routeTester.use(express.json()) +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.use(express.urlencoded({ extended: false })) routeTester.use("/update", updateRoute) routeTester.use("/app/update", updateRoute) +routeTester.use(messenger) const rerumUriOrig = `${process.env.RERUM_ID_PATTERN}_not_` const rerumUriUpdated = `${process.env.RERUM_ID_PATTERN}_updated_` @@ -79,6 +81,24 @@ describe("Check that incorrect TinyNode update route usage results in expected R .set("Content-Type", "application/json") .send({ no: "@id" }) assert.equal(response.statusCode, 400) + + response = await request(routeTester) + .put("/update") + .set("Content-Type", "text/plain") + .send("plain text") + assert.equal(response.statusCode, 415) + }) +}) + +describe("Update network failure behavior. __rest __core", () => { + it("Maps rejected fetch to 502.", async () => { + global.fetch = async () => { throw new Error("socket hang up") } + + const response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ "@id": rerumUriOrig, testing: "item" }) + assert.equal(response.statusCode, 502) }) }) From e9d090431c389ff6f08f828f8e8dfc65c597e173 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 09:01:27 -0500 Subject: [PATCH 12/25] Update TESTING.md --- test/TESTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/TESTING.md b/test/TESTING.md index 82feb0c..fc7b188 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -30,7 +30,7 @@ Tests will pass even if: ### Upstream Contract Validation -To improve coverage, each route's `__mock_functions` test now **validates the upstream contract** by asserting on: +To improve coverage, each route's `__mock_functions` test **validates the upstream contract** by asserting on: - **URL**: Verifies the correct RERUM endpoint is called (e.g., `/create`, `/query?limit=10&skip=0`) - **HTTP Method**: Confirms POST for create/query, PUT for update/overwrite, DELETE for delete @@ -52,6 +52,8 @@ This catches breaking changes like: - ✅ Wrong HTTP methods - ✅ Incorrect Content-Type headers +> This is not yet connected to the RERUM API or tests directly! It may get out of sync! + ### If-Overwritten-Version Header Behavior The overwrite route includes special handling for version conflict resolution via the `If-Overwritten-Version` header. Tests validate: From 92cef9d2a8a1a78498cc94f12423061b1b570bc0 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 09:10:29 -0500 Subject: [PATCH 13/25] test description cleanup --- test/TESTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/TESTING.md b/test/TESTING.md index fc7b188..6763f9f 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -23,6 +23,7 @@ Currently, RERUM does not provide a reliable testing harness that TinyNode can d ### What This Means Tests will pass even if: + - RERUM API endpoints become unavailable - RERUM API paths are broken (e.g., `/create` → `/createx`) - RERUM response schemas change @@ -52,8 +53,6 @@ This catches breaking changes like: - ✅ Wrong HTTP methods - ✅ Incorrect Content-Type headers -> This is not yet connected to the RERUM API or tests directly! It may get out of sync! - ### If-Overwritten-Version Header Behavior The overwrite route includes special handling for version conflict resolution via the `If-Overwritten-Version` header. Tests validate: @@ -82,6 +81,7 @@ assert.equal(lastFetchOptions.headers["If-Overwritten-Version"], "xyz789") ``` This prevents accidental changes to: + - ✅ Header passthrough logic - ✅ Body field name (`isOverwritten` vs other names) - ✅ Precedence between header and body sources @@ -89,6 +89,7 @@ This prevents accidental changes to: ### Future Improvements Once RERUM provides a reliable test harness, TinyNode will: + 1. Replace generic mocks with schema-based mocks that validate against RERUM's actual API schema 2. Add integration tests that verify real requests to a test RERUM instance 3. Improve test coverage for error handling scenarios when RERUM is unreachable or responds unexpectedly @@ -96,6 +97,7 @@ Once RERUM provides a reliable test harness, TinyNode will: ### Testing Mocked Behavior Current tests focus on: + - Request validation and marshaling - Route response formatting - Error message generation From 2204f9bbc89c22f1f1d4e7f90b8ab691448c301c Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 16 Apr 2026 13:35:03 -0500 Subject: [PATCH 14/25] Easy gap to cover. Cleanup dead index.js code. --- routes/index.js | 5 ----- test/routes/create.test.js | 16 ++++++++++++++++ test/routes/overwrite.test.js | 16 ++++++++++++++++ test/routes/update.test.js | 16 ++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/routes/index.js b/routes/index.js index 4cbe4da..466a60a 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,11 +1,6 @@ import express from "express" const router = express.Router() -/* GET home page. */ -router.get('/', function(req, res, next) { - res.status(200).sendFile('index.html') -}) - router.all('/', (req, res, next) => { res.status(405).send("Method Not Allowed") }) diff --git a/test/routes/create.test.js b/test/routes/create.test.js index f31761c..a8572d0 100644 --- a/test/routes/create.test.js +++ b/test/routes/create.test.js @@ -63,6 +63,22 @@ describe("Check that the request/response behavior of the TinyNode create route assert.ok(response.header.location) assert.equal(response.body.test, "item") }) + + it("Falls back to rerumResponse.id when @id is absent.", async () => { + const fallbackUri = `${process.env.RERUM_ID_PATTERN}_fallback_` + global.fetch = async () => ({ + json: async () => ({ id: fallbackUri, test: "item" }), + ok: true + }) + + const response = await request(routeTester) + .post("/create") + .send({ test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 201) + assert.equal(response.header.location, fallbackUri) + }) }) describe("Check that incorrect TinyNode create route usage results in expected RESTful responses from RERUM. __rest __core", () => { diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js index 3a91589..ac9011e 100644 --- a/test/routes/overwrite.test.js +++ b/test/routes/overwrite.test.js @@ -52,6 +52,22 @@ describe("Check that the request/response behavior of the TinyNode overwrite rou assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) + + it("Falls back to rerumResponse.id when @id is absent.", async () => { + const fallbackUri = `${process.env.RERUM_ID_PATTERN}_fallback_` + global.fetch = async () => ({ + json: async () => ({ id: fallbackUri, testing: "item" }), + ok: true + }) + + const response = await request(routeTester) + .put("/overwrite") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.header.location, fallbackUri) + }) }) describe("Check that incorrect TinyNode overwrite route usage results in expected RESTful responses from RERUM. __rest __core", () => { diff --git a/test/routes/update.test.js b/test/routes/update.test.js index c082385..9b531e0 100644 --- a/test/routes/update.test.js +++ b/test/routes/update.test.js @@ -54,6 +54,22 @@ describe("Check that the request/response behavior of the TinyNode update route assert.match(lastFetchOptions.headers["Authorization"], /^Bearer /, "Authorization header missing or invalid") assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) + + it("Falls back to rerumResponse.id when @id is absent.", async () => { + const fallbackUri = `${process.env.RERUM_ID_PATTERN}_fallback_` + global.fetch = async () => ({ + json: async () => ({ id: fallbackUri, testing: "item" }), + ok: true + }) + + const response = await request(routeTester) + .put("/update") + .send({ "@id": rerumUriOrig, testing: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 200) + assert.equal(response.header.location, fallbackUri) + }) }) describe("Check that incorrect TinyNode update route usage results in expected RESTful responses from RERUM. __rest __core", () => { From fc689d5f53c29f8f225ca45c053b7aa7dd11aa20 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 14:56:18 -0500 Subject: [PATCH 15/25] Create app-cors.test.js --- test/routes/app-cors.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/routes/app-cors.test.js diff --git a/test/routes/app-cors.test.js b/test/routes/app-cors.test.js new file mode 100644 index 0000000..0b6b6a4 --- /dev/null +++ b/test/routes/app-cors.test.js @@ -0,0 +1,27 @@ +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import request from "supertest" + +const originalOpenApiCors = process.env.OPEN_API_CORS + +beforeEach(() => { + process.env.OPEN_API_CORS = "true" +}) + +afterEach(() => { + process.env.OPEN_API_CORS = originalOpenApiCors +}) + +describe("App CORS middleware behavior. __core", () => { + it("Adds CORS headers when OPEN_API_CORS is enabled.", async () => { + const { default: app } = await import("../../app.js?test-cors-enabled") + + const response = await request(app) + .get("/index.html") + .set("Origin", "http://example.test") + + assert.equal(response.statusCode, 200) + assert.equal(response.header["access-control-allow-origin"], "*") + assert.equal(response.header["access-control-expose-headers"], "*") + }) +}) From fd2fa6ff865dd4430dc0597d4910e9f73d897c14 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:06:33 -0500 Subject: [PATCH 16/25] Add create id-mapping tests and content-type tests Add tests to exercise create route behavior: ensure request body id is converted to _id before upstream, and verify a successful upstream response missing id fields is treated as an error (502). Add new tests for verifyJsonContentType middleware to ensure it returns 415 when the Content-Type header is missing or when multiple Content-Type values are provided. These tests guard against regressions in id handling and content-type validation. --- test/routes/create.test.js | 27 +++++++++++++++++++++++++++ test/routes/rest.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 test/routes/rest.test.js diff --git a/test/routes/create.test.js b/test/routes/create.test.js index a8572d0..48a7378 100644 --- a/test/routes/create.test.js +++ b/test/routes/create.test.js @@ -53,6 +53,18 @@ describe("Check that the request/response behavior of the TinyNode create route assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch") }) + it("Converts body id to _id before sending upstream.", async () => { + const response = await request(routeTester) + .post("/create") + .send({ id: "https://example.org/id/abc123", test: "item" }) + .set("Content-Type", "application/json") + + assert.equal(response.statusCode, 201) + const upstreamBody = JSON.parse(lastFetchOptions.body) + assert.equal(upstreamBody._id, "abc123") + assert.equal(upstreamBody.id, "https://example.org/id/abc123") + }) + it("Accepts application/ld+json content type.", async () => { const response = await request(routeTester) .post("/create") @@ -139,6 +151,21 @@ describe("Check that TinyNode create route propagates upstream and network error assert.equal(response.statusCode, 502) assert.match(response.text, /A RERUM error occurred/) }) + + it("Maps successful upstream payload without id fields to 502.", async () => { + global.fetch = async () => ({ + ok: true, + json: async () => ({ test: "item" }) + }) + + const response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) }) describe("Check that the properly used create endpoints function and interact with RERUM. __e2e", () => { diff --git a/test/routes/rest.test.js b/test/routes/rest.test.js new file mode 100644 index 0000000..9e9d539 --- /dev/null +++ b/test/routes/rest.test.js @@ -0,0 +1,31 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import express from "express" +import request from "supertest" +import { verifyJsonContentType } from "../../rest.js" + +const routeTester = express() +routeTester.post("/verify", verifyJsonContentType, (req, res) => { + res.status(204).end() +}) + +describe("verifyJsonContentType edge behavior. __core", () => { + it("Returns 415 when Content-Type header is missing.", async () => { + const response = await request(routeTester) + .post("/verify") + + assert.equal(response.statusCode, 415) + assert.match(response.text, /Use application\/json or application\/ld\+json/) + }) + + it("Returns 415 when multiple Content-Type values are provided.", async () => { + const response = await request(routeTester) + .post("/verify") + .set("Content-Type", "application/json,text/plain") + .send("{}") + + assert.equal(response.statusCode, 415) + assert.match(response.text, /Multiple Content-Type values are not allowed/) + }) +}) From 8ab19182fea80fd7ac9547b74757befb6c6cf9f9 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:11:02 -0500 Subject: [PATCH 17/25] Add tests for messenger headers and text body Add two tests to test/routes/error-messenger.test.js: one verifies the error messenger returns early when headers have already been sent (preserving partial response and leaving status 200), and another verifies that when an upstream error provides a text/plain Content-Type and a text() method, the messenger forwards the plain-text body and upstream status (example uses 418). These cover edge cases for header-sent behavior and handling of plain-text upstream responses. --- test/routes/error-messenger.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/routes/error-messenger.test.js b/test/routes/error-messenger.test.js index 3963b14..903f4fb 100644 --- a/test/routes/error-messenger.test.js +++ b/test/routes/error-messenger.test.js @@ -13,6 +13,19 @@ function appWith(routeHandler) { } describe("Check shared error messenger behavior. __rest __core", () => { + it("Returns early when headers are already sent.", async () => { + const app = express() + app.get("/test", (req, res, next) => { + res.write("partial") + next(new Error("late error")) + }) + app.use(messenger) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 200) + assert.match(response.text, /partial/) + }) + it("Returns structured JSON error bodies when upstream responds with JSON.", async () => { const app = appWith((req, res, next) => { next({ @@ -55,6 +68,20 @@ describe("Check shared error messenger behavior. __rest __core", () => { assert.match(response.text, /Upstream unavailable/) }) + it("Sends plain text body from upstream text() when provided.", async () => { + const app = appWith((req, res, next) => { + next({ + status: 418, + headers: { get: () => "text/plain" }, + text: async () => "Teapot exploded" + }) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 418) + assert.match(response.text, /Teapot exploded/) + }) + it("Returns structured payload when error carries payload.", async () => { const app = appWith((req, res, next) => { next({ From 5c11a7161c8307612d9f84cdf3b4cce647e6e450 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:16:50 -0500 Subject: [PATCH 18/25] Add test for fetchRerum timeout mapping Adds test/routes/rerum.test.js which verifies that fetchRerum maps fetch timeouts (AbortError) to a 504 upstream timeout error. The test mocks global.fetch to reject with an AbortError, sets RERUM_FETCH_TIMEOUT_MS to a low value, and asserts the returned error has status 504 and an appropriate message. Test setup/teardown restore the original global.fetch and environment variable. --- test/routes/rerum.test.js | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/routes/rerum.test.js diff --git a/test/routes/rerum.test.js b/test/routes/rerum.test.js new file mode 100644 index 0000000..be083e3 --- /dev/null +++ b/test/routes/rerum.test.js @@ -0,0 +1,40 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, beforeEach, describe, it } from "node:test" +import { fetchRerum } from "../../rerum.js" + +const originalFetch = global.fetch +const originalTimeout = process.env.RERUM_FETCH_TIMEOUT_MS + +beforeEach(() => { + process.env.RERUM_FETCH_TIMEOUT_MS = "1" +}) + +afterEach(() => { + global.fetch = originalFetch + process.env.RERUM_FETCH_TIMEOUT_MS = originalTimeout +}) + +describe("fetchRerum timeout behavior. __core", () => { + it("Maps timeout aborts to a 504 upstream timeout error.", async () => { + global.fetch = async (url, options = {}) => { + const { signal } = options + return await new Promise((resolve, reject) => { + signal?.addEventListener("abort", () => { + const err = new Error("request aborted") + err.name = "AbortError" + reject(err) + }) + }) + } + + await assert.rejects( + () => fetchRerum("https://example.org/rerum"), + err => { + assert.equal(err.status, 504) + assert.match(err.message, /did not respond within/i) + return true + } + ) + }) +}) From 532a9ff70bcc58ba80331ee8cbd83f95d4823db4 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:24:22 -0500 Subject: [PATCH 19/25] Add token tests; extend error & fetchRerum tests Add a new test file for checkAccessToken (test/routes/tokens.test.js) to cover missing ACCESS_TOKEN, malformed JWTs, and refresh error propagation. Update error-messenger.test.js to assert statusCode/statusMessage fallback handling. Extend rerum.test.js to save/restore global setTimeout/clearTimeout and add tests that map non-timeout fetch failures to a 502 upstream error, ensure provided AbortSignal is forwarded, and verify fallback to the default timeout when the configured timeout is invalid. --- test/routes/error-messenger.test.js | 15 ++++++++ test/routes/rerum.test.js | 49 ++++++++++++++++++++++++ test/routes/tokens.test.js | 58 +++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 test/routes/tokens.test.js diff --git a/test/routes/error-messenger.test.js b/test/routes/error-messenger.test.js index 903f4fb..53abe0e 100644 --- a/test/routes/error-messenger.test.js +++ b/test/routes/error-messenger.test.js @@ -51,6 +51,21 @@ describe("Check shared error messenger behavior. __rest __core", () => { assert.match(response.text, /boom/) }) + it("Uses statusCode and statusMessage fallback fields when present.", async () => { + const app = appWith((req, res, next) => { + next({ + statusCode: 499, + statusMessage: "Client closed request", + headers: { get: () => "text/plain" }, + text: async () => "" + }) + }) + + const response = await request(app).get("/test") + assert.equal(response.statusCode, 499) + assert.match(response.text, /Client closed request/) + }) + it("Uses fallback message if .text() throws.", async () => { const app = appWith((req, res, next) => { next({ diff --git a/test/routes/rerum.test.js b/test/routes/rerum.test.js index be083e3..455fd52 100644 --- a/test/routes/rerum.test.js +++ b/test/routes/rerum.test.js @@ -5,6 +5,8 @@ import { fetchRerum } from "../../rerum.js" const originalFetch = global.fetch const originalTimeout = process.env.RERUM_FETCH_TIMEOUT_MS +const originalSetTimeout = global.setTimeout +const originalClearTimeout = global.clearTimeout beforeEach(() => { process.env.RERUM_FETCH_TIMEOUT_MS = "1" @@ -13,6 +15,8 @@ beforeEach(() => { afterEach(() => { global.fetch = originalFetch process.env.RERUM_FETCH_TIMEOUT_MS = originalTimeout + global.setTimeout = originalSetTimeout + global.clearTimeout = originalClearTimeout }) describe("fetchRerum timeout behavior. __core", () => { @@ -37,4 +41,49 @@ describe("fetchRerum timeout behavior. __core", () => { } ) }) + + it("Maps non-timeout fetch failures to a 502 upstream network error.", async () => { + global.fetch = async () => { + throw new Error("socket hang up") + } + + await assert.rejects( + () => fetchRerum("https://example.org/rerum"), + err => { + assert.equal(err.status, 502) + assert.match(err.message, /A RERUM error occurred/) + return true + } + ) + }) + + it("Uses the provided signal path and still resolves successful responses.", async () => { + const externalController = new AbortController() + global.fetch = async (url, options = {}) => { + assert.ok(options.signal, "A signal should be forwarded to fetch") + return { ok: true, source: url } + } + + const response = await fetchRerum("https://example.org/rerum", { signal: externalController.signal }) + assert.equal(response.ok, true) + assert.equal(response.source, "https://example.org/rerum") + }) + + it("Falls back to default timeout when configured timeout is invalid.", async () => { + process.env.RERUM_FETCH_TIMEOUT_MS = "-10" + let capturedTimeoutMs = null + let timeoutFn + + global.setTimeout = (fn, ms) => { + timeoutFn = fn + capturedTimeoutMs = ms + return 1 + } + global.clearTimeout = () => {} + global.fetch = async () => ({ ok: true }) + + await fetchRerum("https://example.org/rerum") + assert.equal(capturedTimeoutMs, 30000) + assert.equal(typeof timeoutFn, "function") + }) }) diff --git a/test/routes/tokens.test.js b/test/routes/tokens.test.js new file mode 100644 index 0000000..985fa20 --- /dev/null +++ b/test/routes/tokens.test.js @@ -0,0 +1,58 @@ +import "../helpers/env.js" +import assert from "node:assert/strict" +import { afterEach, describe, it } from "node:test" +import checkAccessToken from "../../tokens.js" + +const originalAccessToken = process.env.ACCESS_TOKEN +const originalFetch = global.fetch + +afterEach(() => { + process.env.ACCESS_TOKEN = originalAccessToken + global.fetch = originalFetch +}) + +function jwtWithExp(expSeconds) { + const payload = Buffer.from(JSON.stringify({ exp: expSeconds })).toString("base64") + return `header.${payload}.signature` +} + +describe("checkAccessToken middleware behavior. __core", () => { + it("Calls next when ACCESS_TOKEN is missing.", async () => { + delete process.env.ACCESS_TOKEN + let called = 0 + + await checkAccessToken({}, {}, err => { + assert.equal(err, undefined) + called += 1 + }) + + assert.equal(called, 1) + }) + + it("Treats malformed token as non-expired and calls next.", async () => { + process.env.ACCESS_TOKEN = "not-a-jwt" + let called = 0 + + await checkAccessToken({}, {}, err => { + assert.equal(err, undefined) + called += 1 + }) + + assert.equal(called, 1) + }) + + it("Propagates refresh errors to next(err) when token is expired.", async () => { + process.env.ACCESS_TOKEN = jwtWithExp(Math.floor(Date.now() / 1000) - 60) + global.fetch = async () => { + throw new Error("refresh failed") + } + + let receivedError + await checkAccessToken({}, {}, err => { + receivedError = err + }) + + assert.ok(receivedError) + assert.match(receivedError.message, /refresh failed/) + }) +}) From 4fa75218202880c8bdebf701962b58d9225be853 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:27:01 -0500 Subject: [PATCH 20/25] Update update.test.js --- test/routes/update.test.js | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/routes/update.test.js b/test/routes/update.test.js index 9b531e0..a6b5949 100644 --- a/test/routes/update.test.js +++ b/test/routes/update.test.js @@ -116,6 +116,55 @@ describe("Update network failure behavior. __rest __core", () => { .send({ "@id": rerumUriOrig, testing: "item" }) assert.equal(response.statusCode, 502) }) + + it("Preserves upstream text error message when update returns non-ok.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + text: async () => "Upstream update failure" + }) + + const response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ "@id": rerumUriOrig, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream update failure/) + }) + + it("Falls back to generic RERUM error text when upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ "@id": rerumUriOrig, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) + + it("Maps successful upstream payload without id fields to 502.", async () => { + global.fetch = async () => ({ + ok: true, + json: async () => ({ testing: "item" }) + }) + + const response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ "@id": rerumUriOrig, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) }) describe("Check that the properly used update endpoints function and interact with RERUM. __e2e", () => { From c585a14f671d57592085218577e8ae10216208c4 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:28:23 -0500 Subject: [PATCH 21/25] network query failures Add unit tests for /query to cover upstream and network failure handling: (1) preserves upstream error text when fetch returns non-ok (503), (2) falls back to a generic RERUM error message when upstream .text() throws, and (3) maps rejected fetch (e.g., socket hang up) to a 502 response. Tests mock global.fetch and assert statusCode 502 and expected response text. --- test/routes/query.test.js | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/routes/query.test.js b/test/routes/query.test.js index f732fb5..6b45b36 100644 --- a/test/routes/query.test.js +++ b/test/routes/query.test.js @@ -115,6 +115,56 @@ describe("Check that incorrect TinyNode query route usage results in expected RE }) }) +describe("Query upstream and network failure behavior. __rest __core", () => { + it("Preserves upstream text error message when query returns non-ok.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + text: async () => "Upstream query failure" + }) + + const response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream query failure/) + }) + + it("Falls back to generic RERUM error text when upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) + + it("Maps rejected fetch to 502.", async () => { + global.fetch = async () => { + throw new Error("socket hang up") + } + + const response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) +}) + describe("Check that the properly used query endpoints function and interact with RERUM. __e2e", () => { it("'/query' route can save an object to RERUM.", async () => { const response = await request(routeTester) From a494b71b68e1efbde6d6b4bfb6cc1439deac29a7 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:30:18 -0500 Subject: [PATCH 22/25] nextwork errros --- test/routes/delete.test.js | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/routes/delete.test.js b/test/routes/delete.test.js index d2d32bb..5803382 100644 --- a/test/routes/delete.test.js +++ b/test/routes/delete.test.js @@ -104,4 +104,68 @@ describe("Delete network failure and passthrough behavior. __rest __core", () = .delete("/delete/00000") assert.equal(response.statusCode, 502) }) + + it("Preserves upstream text error message for body delete when response is non-ok.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + text: async () => "Upstream body delete failure" + }) + + const response = await request(routeTester) + .delete("/delete") + .set("Content-Type", "application/json") + .send({ "@id": rerumUri }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream body delete failure/) + }) + + it("Falls back to generic RERUM error text for body delete when upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .delete("/delete") + .set("Content-Type", "application/json") + .send({ "@id": rerumUri }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) + + it("Preserves upstream text error message for path delete when response is non-ok.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + text: async () => "Upstream path delete failure" + }) + + const response = await request(routeTester) + .delete("/delete/00000") + + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream path delete failure/) + }) + + it("Falls back to generic RERUM error text for path delete when upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .delete("/delete/00000") + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) }) From 28b95eb4ff42438254ab746dcdd8b4fe771c5c40 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:32:53 -0500 Subject: [PATCH 23/25] more tests for file env token refresh --- test/routes/tokens.test.js | 73 +++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/test/routes/tokens.test.js b/test/routes/tokens.test.js index 985fa20..9661653 100644 --- a/test/routes/tokens.test.js +++ b/test/routes/tokens.test.js @@ -1,14 +1,39 @@ import "../helpers/env.js" import assert from "node:assert/strict" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" import { afterEach, describe, it } from "node:test" import checkAccessToken from "../../tokens.js" const originalAccessToken = process.env.ACCESS_TOKEN +const originalAccessTokenUrl = process.env.RERUM_ACCESS_TOKEN_URL +const originalRefreshToken = process.env.REFRESH_TOKEN const originalFetch = global.fetch +const originalCwd = process.cwd() +const tempDirs = [] -afterEach(() => { +async function inTempCwd(run, envContent) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "tinynode-token-test-")) + tempDirs.push(tempDir) + if (envContent !== undefined) { + await fs.writeFile(path.join(tempDir, ".env"), envContent) + } + process.chdir(tempDir) + await run(tempDir) +} + +afterEach(async () => { process.env.ACCESS_TOKEN = originalAccessToken + process.env.RERUM_ACCESS_TOKEN_URL = originalAccessTokenUrl + process.env.REFRESH_TOKEN = originalRefreshToken global.fetch = originalFetch + process.chdir(originalCwd) + + while (tempDirs.length > 0) { + const tempDir = tempDirs.pop() + await fs.rm(tempDir, { recursive: true, force: true }) + } }) function jwtWithExp(expSeconds) { @@ -55,4 +80,50 @@ describe("checkAccessToken middleware behavior. __core", () => { assert.ok(receivedError) assert.match(receivedError.message, /refresh failed/) }) + + it("Refreshes an expired token and persists it to .env.", async () => { + await inTempCwd(async tempDir => { + process.env.ACCESS_TOKEN = jwtWithExp(Math.floor(Date.now() / 1000) - 60) + process.env.RERUM_ACCESS_TOKEN_URL = "https://auth.example/token" + process.env.REFRESH_TOKEN = "refresh-token" + + global.fetch = async (url, options) => { + assert.equal(url, "https://auth.example/token") + assert.equal(options.method, "POST") + return { + json: async () => ({ access_token: "new-access-token" }) + } + } + + let nextError + await checkAccessToken({}, {}, err => { + nextError = err + }) + + assert.equal(nextError, undefined) + assert.equal(process.env.ACCESS_TOKEN, "new-access-token") + const envText = await fs.readFile(path.join(tempDir, ".env"), "utf8") + assert.match(envText, /ACCESS_TOKEN=new-access-token/) + }, "ACCESS_TOKEN=old-token\n") + }) + + it("Continues when refresh succeeds but .env read fails.", async () => { + await inTempCwd(async () => { + process.env.ACCESS_TOKEN = jwtWithExp(Math.floor(Date.now() / 1000) - 60) + process.env.RERUM_ACCESS_TOKEN_URL = "https://auth.example/token" + process.env.REFRESH_TOKEN = "refresh-token" + + global.fetch = async () => ({ + json: async () => ({ access_token: "new-access-token-2" }) + }) + + let nextError + await checkAccessToken({}, {}, err => { + nextError = err + }) + + assert.equal(nextError, undefined) + assert.equal(process.env.ACCESS_TOKEN, "new-access-token-2") + }) + }) }) From 6f44526c1df26fd09b7b53160f13a7d077f1ee9b Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:36:03 -0500 Subject: [PATCH 24/25] Update index.test.js --- test/routes/index.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/routes/index.test.js b/test/routes/index.test.js index e00f637..9733886 100644 --- a/test/routes/index.test.js +++ b/test/routes/index.test.js @@ -1,8 +1,13 @@ import "../helpers/env.js" import assert from "node:assert/strict" import { describe, it } from "node:test" +import express from "express" import request from "supertest" import app from "../../app.js" +import indexRoute from "../../routes/index.js" + +const routeTester = express() +routeTester.use("/", indexRoute) describe("Make sure TinyNode demo interface is present. __core", () => { it("/index.html", async () => { @@ -10,4 +15,12 @@ describe("Make sure TinyNode demo interface is present. __core", () => { assert.equal(response.statusCode, 200) assert.match(response.header["content-type"], /html/) }) + + it("Index router returns 405 for unsupported root methods.", async () => { + let response = await request(routeTester).get("/") + assert.equal(response.statusCode, 405) + + response = await request(routeTester).post("/") + assert.equal(response.statusCode, 405) + }) }) From af47f0701be43e3c16cada75ba681fd6ee44d613 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 16 Apr 2026 15:56:11 -0500 Subject: [PATCH 25/25] Add error-handling and token unit tests Add several unit tests to improve error handling coverage and token middleware behavior. create.test.js and overwrite.test.js gain tests that preserve upstream text on non-409 failures, fall back to a generic RERUM message when upstream .text() throws, and map missing-id successful payloads to 502. error-messenger.test.js replaces res.write with res.end in an existing test and introduces createMockRes plus focused messenger unit tests for headersSent, payload JSON, JSON content-type, and plain-text upstream errors. tokens.test.js adds tests ensuring valid or non-numeric-exp access tokens skip refresh. These changes increase robustness of error-path handling and token refresh logic in tests. --- test/routes/create.test.js | 18 +++++++ test/routes/error-messenger.test.js | 80 ++++++++++++++++++++++++++++- test/routes/overwrite.test.js | 49 ++++++++++++++++++ test/routes/tokens.test.js | 29 +++++++++++ 4 files changed, 175 insertions(+), 1 deletion(-) diff --git a/test/routes/create.test.js b/test/routes/create.test.js index 48a7378..8adb180 100644 --- a/test/routes/create.test.js +++ b/test/routes/create.test.js @@ -152,6 +152,24 @@ describe("Check that TinyNode create route propagates upstream and network error assert.match(response.text, /A RERUM error occurred/) }) + it("Falls back to generic RERUM error text when upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) + it("Maps successful upstream payload without id fields to 502.", async () => { global.fetch = async () => ({ ok: true, diff --git a/test/routes/error-messenger.test.js b/test/routes/error-messenger.test.js index 53abe0e..50f213a 100644 --- a/test/routes/error-messenger.test.js +++ b/test/routes/error-messenger.test.js @@ -16,7 +16,7 @@ describe("Check shared error messenger behavior. __rest __core", () => { it("Returns early when headers are already sent.", async () => { const app = express() app.get("/test", (req, res, next) => { - res.write("partial") + res.end("partial") next(new Error("late error")) }) app.use(messenger) @@ -110,3 +110,81 @@ describe("Check shared error messenger behavior. __rest __core", () => { assert.equal(response.body.code, "BAD_INPUT") }) }) + +function createMockRes(headersSent = false) { + const res = { + headersSent, + statusCode: null, + sentText: null, + sentJson: null, + setHeaders: {}, + status(code) { + this.statusCode = code + return this + }, + json(payload) { + this.sentJson = payload + return this + }, + send(text) { + this.sentText = text + return this + }, + set(name, value) { + this.setHeaders[name] = value + return this + } + } + return res +} + +describe("Check shared error messenger unit branches. __core", () => { + it("Returns immediately when headersSent is true.", async () => { + const res = createMockRes(true) + await messenger(new Error("ignored"), {}, res, () => {}) + assert.equal(res.statusCode, null) + assert.equal(res.sentText, null) + assert.equal(res.sentJson, null) + }) + + it("Sends payload JSON when payload is present.", async () => { + const res = createMockRes(false) + await messenger({ status: 409, payload: { code: "CONFLICT" } }, {}, res, () => {}) + assert.equal(res.statusCode, 409) + assert.equal(res.sentJson.code, "CONFLICT") + }) + + it("Uses upstream JSON response when content-type is JSON.", async () => { + const res = createMockRes(false) + await messenger( + { + status: 422, + headers: { get: () => "application/json; charset=utf-8" }, + json: async () => ({ detail: "bad request" }) + }, + {}, + res, + () => {} + ) + assert.equal(res.statusCode, 422) + assert.equal(res.sentJson.detail, "bad request") + }) + + it("Sends plain text and sets content-type for non-JSON upstream errors.", async () => { + const res = createMockRes(false) + await messenger( + { + status: 503, + message: "fallback", + headers: { get: () => "text/plain" }, + text: async () => "upstream text" + }, + {}, + res, + () => {} + ) + assert.equal(res.statusCode, 503) + assert.equal(res.sentText, "upstream text") + assert.equal(res.setHeaders["Content-Type"], "text/plain; charset=utf-8") + }) +}) diff --git a/test/routes/overwrite.test.js b/test/routes/overwrite.test.js index ac9011e..e64d92d 100644 --- a/test/routes/overwrite.test.js +++ b/test/routes/overwrite.test.js @@ -181,6 +181,55 @@ describe("Overwrite network failure behavior. __rest __core", () => { .send({ "@id": rerumTinyTestObjId, testing: "item" }) assert.equal(response.statusCode, 502) }) + + it("Preserves upstream text error message for non-409 overwrite failures.", async () => { + global.fetch = async () => ({ + ok: false, + status: 503, + text: async () => "Upstream overwrite failure" + }) + + const response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /Upstream overwrite failure/) + }) + + it("Falls back to generic RERUM error text when overwrite upstream .text() throws.", async () => { + global.fetch = async () => ({ + ok: false, + status: 500, + text: async () => { + throw new Error("text stream consumed") + } + }) + + const response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) + + it("Maps successful overwrite payload without id fields to 502.", async () => { + global.fetch = async () => ({ + ok: true, + json: async () => ({ testing: "item" }) + }) + + const response = await request(routeTester) + .put("/overwrite") + .set("Content-Type", "application/json") + .send({ "@id": rerumTinyTestObjId, testing: "item" }) + + assert.equal(response.statusCode, 502) + assert.match(response.text, /A RERUM error occurred/) + }) }) describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => { diff --git a/test/routes/tokens.test.js b/test/routes/tokens.test.js index 9661653..99d61e6 100644 --- a/test/routes/tokens.test.js +++ b/test/routes/tokens.test.js @@ -66,6 +66,35 @@ describe("checkAccessToken middleware behavior. __core", () => { assert.equal(called, 1) }) + it("Calls next without refresh when token is valid and not expired.", async () => { + process.env.ACCESS_TOKEN = jwtWithExp(Math.floor(Date.now() / 1000) + 3600) + global.fetch = async () => { + throw new Error("fetch should not be called") + } + + let nextError + await checkAccessToken({}, {}, err => { + nextError = err + }) + + assert.equal(nextError, undefined) + }) + + it("Treats non-numeric exp payload as non-expired and skips refresh.", async () => { + const payload = Buffer.from(JSON.stringify({ exp: "not-a-number" })).toString("base64") + process.env.ACCESS_TOKEN = `header.${payload}.signature` + global.fetch = async () => { + throw new Error("fetch should not be called") + } + + let nextError + await checkAccessToken({}, {}, err => { + nextError = err + }) + + assert.equal(nextError, undefined) + }) + it("Propagates refresh errors to next(err) when token is expired.", async () => { process.env.ACCESS_TOKEN = jwtWithExp(Math.floor(Date.now() / 1000) - 60) global.fetch = async () => {