diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index e0fe036c..c11bb8e4 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -3,31 +3,18 @@ on: pull_request: branches: main jobs: - merge-branch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Merge with main - uses: devmasx/merge-branch@master - with: - type: now - from_branch: main - target_branch: ${{ github.head_ref }} - github_token: ${{ secrets.OPENAPI }} - message: Merge main into this branch to deploy to dev for testing. test: - needs: merge-branch runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Create .env from secrets run: echo "${{ secrets.DEV_FULL_ENV }}" > .env - name: Setup Node.js - uses: actions/setup-node@master + uses: actions/setup-node@v4 with: node-version: "24" - name: Cache node modules - uses: actions/cache@master + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -38,14 +25,13 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - - name: Install dependencies and run the test - run: | - npm install - npm run runtest + - name: Install dependencies + run: npm install + - name: Generate coverage report + run: npm run coverage:ci deploy: if: github.event.pull_request.draft == false needs: - - merge-branch - test strategy: matrix: @@ -55,7 +41,7 @@ jobs: - vlcdhp02 runs-on: ${{ matrix.machines }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then @@ -70,4 +56,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 startOrReload ecosystem.config.json --env development diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 70ea945a..09a685c4 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -6,17 +6,17 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Create .env from secrets run: echo "${{ secrets.PROD_FULL_ENV }}" > .env - name: Setup Node.js - uses: actions/setup-node@master + uses: actions/setup-node@v4 with: node-version: "24" # Speed up subsequent runs with caching - name: Cache node modules - uses: actions/cache@master + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -27,10 +27,10 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - - name: Install dependencies and run the test - run: | - npm install - npm run runtest + - name: Install dependencies + run: npm install + - name: Generate coverage report + run: npm run coverage:ci deploy: needs: test strategy: @@ -41,7 +41,7 @@ jobs: - vlcdhprdp02 runs-on: ${{ matrix.machines }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then @@ -55,4 +55,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 startOrReload ecosystem.config.json --env production diff --git a/.github/workflows/sync-core-provider-contract.yml b/.github/workflows/sync-core-provider-contract.yml index 0e412125..88a81cc2 100644 --- a/.github/workflows/sync-core-provider-contract.yml +++ b/.github/workflows/sync-core-provider-contract.yml @@ -1,48 +1,53 @@ -name: Sync Core Provider Contract +name: Sync RERUM core provider contract on: - workflow_dispatch: push: branches: - main paths: - - contracts/core-provider.openapi.yaml + - openapi/contracts/core-provider.openapi.yaml - routes/** + workflow_dispatch: + +permissions: + contents: read jobs: - sync-core-provider-contract: + sync: + name: Sync RERUM core provider contract runs-on: ubuntu-latest - permissions: - contents: read steps: - - name: Checkout source repository + - name: Checkout rerum_server_nodejs uses: actions/checkout@v4 - - name: Checkout rerum_openapi + - name: Checkout receiver repository uses: actions/checkout@v4 with: repository: cubap/rerum_openapi + ref: main token: ${{ secrets.OPENAPI }} - path: rerum_openapi + path: receiver - - name: Copy provider contract baseline - run: | - cp contracts/core-provider.openapi.yaml rerum_openapi/seams/tinynode-to-rerum/openapi/baseline.openapi.yaml + - name: Verify receiver stub exists + run: test -f receiver/seams/tinynode-to-rerum/openapi/baseline.openapi.yaml - - name: Create sync pull request - uses: peter-evans/create-pull-request@v7 + - name: Copy canonical provider contract + run: cp openapi/contracts/core-provider.openapi.yaml receiver/seams/tinynode-to-rerum/openapi/baseline.openapi.yaml + + - name: Create or update sync pull request + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.OPENAPI }} - path: rerum_openapi - branch: automation/sync-rerum-core-provider-contract + path: receiver + add-paths: seams/tinynode-to-rerum/openapi/baseline.openapi.yaml + commit-message: "chore: sync RERUM core provider contract" + branch: sync/rerum-core-provider-contract + base: main delete-branch: true - commit-message: Sync RERUM core provider contract from rerum_server_nodejs - title: Sync RERUM core provider contract from rerum_server_nodejs + title: "chore: sync RERUM core provider contract" body: | - Automated sync from `${{ github.repository }}` at `${{ github.sha }}`. - - Source: - - `contracts/core-provider.openapi.yaml` + Syncs the canonical RERUM core provider contract from CenterForDigitalHumanities/rerum_server_nodejs. - Target: - - `seams/tinynode-to-rerum/openapi/baseline.openapi.yaml` + - Source commit: ${{ github.sha }} + - Source artifact: `openapi/contracts/core-provider.openapi.yaml` + - Target artifact: `seams/tinynode-to-rerum/openapi/baseline.openapi.yaml` diff --git a/.github/workflows/sync-rerum-shared-openapi.yml b/.github/workflows/sync-rerum-shared-openapi.yml index a1e8697d..52a5feab 100644 --- a/.github/workflows/sync-rerum-shared-openapi.yml +++ b/.github/workflows/sync-rerum-shared-openapi.yml @@ -1,4 +1,4 @@ -name: Sync shared RERUM OpenAPI artifact +name: Sync RERUM shared OpenAPI artifact on: push: @@ -12,37 +12,41 @@ permissions: contents: read jobs: - sync-shared-openapi-artifact: + sync: + name: Sync RERUM shared OpenAPI artifact runs-on: ubuntu-latest steps: - - name: Checkout source repository + - name: Checkout rerum_server_nodejs uses: actions/checkout@v4 - - name: Checkout rerum_openapi + - name: Checkout receiver repository uses: actions/checkout@v4 with: repository: cubap/rerum_openapi + ref: main token: ${{ secrets.OPENAPI }} - path: rerum_openapi + path: receiver - - name: Copy shared artifact - run: | - cp openapi/components/rerum-shared-components.openapi.yaml rerum_openapi/schemas/openapi/rerum-shared-components.openapi.yaml + - name: Verify receiver stub exists + run: test -f receiver/schemas/openapi/rerum-shared-components.openapi.yaml - - name: Create sync pull request - uses: peter-evans/create-pull-request@v7 + - name: Copy canonical shared artifact + run: cp openapi/components/rerum-shared-components.openapi.yaml receiver/schemas/openapi/rerum-shared-components.openapi.yaml + + - name: Create or update sync pull request + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.OPENAPI }} - path: rerum_openapi - branch: automation/sync-rerum-shared-openapi-artifact + path: receiver + add-paths: schemas/openapi/rerum-shared-components.openapi.yaml + commit-message: "chore: sync RERUM shared OpenAPI artifact" + branch: sync/rerum-shared-openapi + base: main delete-branch: true - commit-message: Sync shared RERUM OpenAPI artifact from rerum_server_nodejs - title: Sync shared RERUM OpenAPI artifact from rerum_server_nodejs + title: "chore: sync RERUM shared OpenAPI artifact" body: | - Automated sync from `${{ github.repository }}` at `${{ github.sha }}`. - - Source: - - `openapi/components/rerum-shared-components.openapi.yaml` + Syncs the canonical RERUM shared OpenAPI artifact from CenterForDigitalHumanities/rerum_server_nodejs. - Target: - - `schemas/openapi/rerum-shared-components.openapi.yaml` + - Source commit: ${{ github.sha }} + - Source artifact: `openapi/components/rerum-shared-components.openapi.yaml` + - Target artifact: `schemas/openapi/rerum-shared-components.openapi.yaml` diff --git a/__tests__/core_provider_contract.test.js b/__tests__/core_provider_contract.test.js index b358a896..9cea0178 100644 --- a/__tests__/core_provider_contract.test.js +++ b/__tests__/core_provider_contract.test.js @@ -1,26 +1,40 @@ -import fs from "fs" -import path from "path" -import { fileURLToPath } from "url" +import { describe, it } from 'node:test' +import assert from 'node:assert' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' const here = path.dirname(fileURLToPath(import.meta.url)) -const repoRoot = path.resolve(here, "..") -const apiRoutesPath = path.join(repoRoot, "routes", "api-routes.js") -const contractPath = path.join(repoRoot, "contracts", "core-provider.openapi.yaml") +const repoRoot = path.resolve(here, '..') +const apiRoutesPath = path.join(repoRoot, 'routes', 'api-routes.js') +const contractPath = path.join(repoRoot, 'openapi', 'contracts', 'core-provider.openapi.yaml') const skippedMountedRouters = new Set([ - "./static.js", - "./compatability.js" + './static.js', + './compatability.js' ]) +/** + * Normalize route paths from Express format to OpenAPI format + * /id/:id -> /id/{id} + */ function normalizeRoutePath(routePath) { - return routePath.replace(/\/:([A-Za-z0-9_]+)/g, "/{id}") + return routePath + .replace(/\/:([A-Za-z0-9_]+)/g, '/{$1}') + .replace(/\{_?id\}/g, '{id}') } +/** + * Join mounted prefix with route path + */ function joinMountedPath(prefix, routePath) { - const suffix = routePath === "/" ? "" : routePath - return normalizeRoutePath(`${prefix}${suffix}`.replace(/\/+/g, "/")) + const suffix = routePath === '/' ? '' : routePath + return normalizeRoutePath(`${prefix}${suffix}`.replace(/\/+/g, '/')) } +/** + * Parse ES6 import statements from source + */ function parseImports(source) { const imports = new Map() const importPattern = /^import\s+(\w+)\s+from\s+'(\.\/[^']+)';?$/gm @@ -30,6 +44,9 @@ function parseImports(source) { return imports } +/** + * Parse router.use() mounted subrouters + */ function parseMountedRouters(source, imports) { const mounted = [] const usePattern = /router\.use\('([^']+)',\s*(\w+)\)/g @@ -40,14 +57,17 @@ function parseMountedRouters(source, imports) { } mounted.push({ prefix: match[1], - filePath: path.join(repoRoot, "routes", importPath.replace("./", "")) + filePath: path.join(repoRoot, 'routes', importPath.replace('./', '')) }) } return mounted } +/** + * Parse route operations from a route file + */ function parseRouteOperations(filePath, prefix) { - const source = fs.readFileSync(filePath, "utf8") + const source = fs.readFileSync(filePath, 'utf8') const operations = new Set() const routeBlockPattern = /router\.route\('([^']+)'\)([\s\S]*?)(?=\nrouter\.route\(|\nexport default)/g for (const match of source.matchAll(routeBlockPattern)) { @@ -63,19 +83,25 @@ function parseRouteOperations(filePath, prefix) { return operations } +/** + * Parse direct router.METHOD() operations + */ function parseDirectOperations(source) { const operations = new Set() const directPattern = /router\.(get|post|put|patch|delete|head)\('([^']+)'/g for (const match of source.matchAll(directPattern)) { - if (match[2] === "/api") { + if (match[2] === '/api') { operations.add(`${match[1].toUpperCase()} ${match[2]}`) } } return operations } +/** + * Get all mounted core provider operations + */ function getMountedCoreProviderOperations() { - const source = fs.readFileSync(apiRoutesPath, "utf8") + const source = fs.readFileSync(apiRoutesPath, 'utf8') const imports = parseImports(source) const operations = new Set(parseDirectOperations(source)) for (const mountedRouter of parseMountedRouters(source, imports)) { @@ -86,10 +112,13 @@ function getMountedCoreProviderOperations() { return Array.from(operations).sort() } +/** + * Parse operations from OpenAPI contract + */ function getContractOperations() { - const lines = fs.readFileSync(contractPath, "utf8").split("\n") + const lines = fs.readFileSync(contractPath, 'utf8').split('\n') const operations = [] - let currentPath = "" + let currentPath = '' for (const line of lines) { const pathMatch = line.match(/^ (\/[^:]+):\s*$/) if (pathMatch) { @@ -98,14 +127,115 @@ function getContractOperations() { } const methodMatch = line.match(/^ (get|post|put|patch|delete|head):\s*$/) if (methodMatch && currentPath) { - operations.push(`${methodMatch[1].toUpperCase()} ${currentPath}`) + operations.push(`${methodMatch[1].toUpperCase()} ${normalizeRoutePath(currentPath)}`) } } return operations.sort() } -describe("core provider contract", () => { - it("matches the mounted core provider route surface", () => { - expect(getContractOperations()).toEqual(getMountedCoreProviderOperations()) +/** + * Parse declared response status codes for every operation in the contract. + * Returns a Map keyed by "METHOD /path" with a Set of three-digit code strings. + * The line parser keys off indentation: paths at 2 spaces, methods at 4 spaces, + * response codes at 8 spaces inside a `responses:` block. + */ +function getContractResponseCodesByOperation() { + const lines = fs.readFileSync(contractPath, 'utf8').split('\n') + const operations = new Map() + let currentPath = '' + let currentOp = '' + let insideResponses = false + for (const line of lines) { + const pathMatch = line.match(/^ (\/[^:]+):\s*$/) + if (pathMatch) { + currentPath = pathMatch[1] + currentOp = '' + insideResponses = false + continue + } + const methodMatch = line.match(/^ (get|post|put|patch|delete|head):\s*$/) + if (methodMatch && currentPath) { + currentOp = `${methodMatch[1].toUpperCase()} ${normalizeRoutePath(currentPath)}` + operations.set(currentOp, new Set()) + insideResponses = false + continue + } + if (line.match(/^ responses:\s*$/)) { + insideResponses = true + continue + } + // A new 6-space key under the same method ends the responses block. + if (insideResponses && line.match(/^ [A-Za-z]/)) { + insideResponses = false + } + const codeMatch = line.match(/^ '(\d{3})':\s*$/) + if (codeMatch && insideResponses && currentOp) { + operations.get(currentOp).add(codeMatch[1]) + } + } + return operations +} + +/** + * Codes the contract MUST declare for each operation. Each entry is the floor: + * adding new codes is fine; removing or changing one fails the test. Updates to + * this catalogue should be made in lockstep with the matching per-route test + * (e.g. routes/__tests__/create.test.js asserts 201 — so '201' must be here too). + */ +const requiredResponseCodes = { + 'POST /api/create': ['201', '400', '401', '409', '413', '415'], + 'POST /api/bulkCreate': ['201', '400', '401', '413', '415'], + 'DELETE /api/delete/{id}': ['204', '401', '403', '404'], + 'PUT /api/overwrite': ['200', '400', '401', '403', '404', '409', '413', '415'], + 'PUT /api/update': ['200', '400', '401', '403', '404', '413', '415'], + // /bulkUpdate silently skips not-found/deleted items per controllers/bulk.js:157-158, so 403/404 are not promised. + 'PUT /api/bulkUpdate': ['200', '400', '401', '413', '415'], + // /patch, /set, /unset return 501 (not 404) when the object is not in RERUM — controllers/patchUpdate.js:41 and siblings. + 'PATCH /api/patch': ['200', '400', '401', '403', '413', '415', '501'], + 'PATCH /api/set': ['200', '400', '401', '403', '413', '415', '501'], + 'PATCH /api/unset': ['200', '400', '401', '403', '413', '415', '501'], + // 409 is reachable via slug conflict (utils.createExpressError maps code 11000 → 409). + 'PATCH /api/release/{id}': ['200', '400', '401', '403', '404', '409'], + 'GET /id/{id}': ['200', '404'], + 'GET /since/{id}': ['200', '404'], + 'GET /history/{id}': ['200', '404'], + // HEAD parity tests in routes/__tests__/{id,since,history,query}.test.js assert 404 on miss; + // enforce that the contract declares the same so drift on either side is caught. + 'HEAD /id/{id}': ['200', '404'], + 'HEAD /since/{id}': ['200', '404'], + 'HEAD /history/{id}': ['200', '404'], + 'HEAD /api/query': ['200', '404'], + 'POST /api/query': ['200', '400', '413', '415'], + 'POST /api/search': ['200', '400', '413', '415'], + 'POST /api/search/phrase': ['200', '400', '413', '415'] +} + +describe('Core Provider Contract', () => { + it('Mounted routes match the core provider contract', () => { + const contractOps = getContractOperations() + const implementedOps = getMountedCoreProviderOperations() + + assert.deepEqual( + implementedOps, + contractOps, + 'Implemented routes do not match contract specification' + ) }) }) + +describe('Core Provider Contract response codes', () => { + const declared = getContractResponseCodesByOperation() + + for (const [operation, expectedCodes] of Object.entries(requiredResponseCodes)) { + it(`${operation} declares ${expectedCodes.join(', ')}`, () => { + const actual = declared.get(operation) + assert.ok(actual, `Operation ${operation} is missing from the contract`) + const missing = expectedCodes.filter(code => !actual.has(code)) + assert.deepStrictEqual( + missing, + [], + `Contract drift: ${operation} must declare ${expectedCodes.join(', ')} but is missing ${missing.join(', ')}` + ) + }) + } +}) diff --git a/__tests__/openapi_sync_artifacts.test.js b/__tests__/openapi_sync_artifacts.test.js index d63db01a..c06563d4 100644 --- a/__tests__/openapi_sync_artifacts.test.js +++ b/__tests__/openapi_sync_artifacts.test.js @@ -1,3 +1,5 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' import fs from "fs" import path from "path" import { fileURLToPath } from "url" @@ -14,21 +16,46 @@ describe("Shared OpenAPI artifact sync scaffolding", () => { const targetArtifact = fs.readFileSync(targetArtifactPath, "utf8") for (const artifact of [providerArtifact, targetArtifact]) { - expect(artifact).toContain("openapi: 3.0.3") - expect(artifact).toContain("title: RERUM Shared Components") - expect(artifact).toContain("version: 0.1.0") - expect(artifact).toContain("components:") - expect(artifact).toContain("schemas: {}") + assert.match(artifact, /^openapi: 3\.\d+\.\d+/m) + assert.match(artifact, /^\s+title: \S/m) + assert.match(artifact, /^\s+version: \d+\.\d+\.\d+/m) + assert.match(artifact, /^components:/m) + assert.match(artifact, /^\s+schemas:/m) } }) + it("keeps the synced target artifact equivalent to the provider artifact", () => { + const providerArtifactPath = path.join(repoRoot, "openapi/components/rerum-shared-components.openapi.yaml") + const targetArtifactPath = path.join(repoRoot, "schemas/openapi/rerum-shared-components.openapi.yaml") + const stripLeadingComments = (yaml) => yaml.replace(/^(?:#[^\n]*\n)+/, "") + const provider = stripLeadingComments(fs.readFileSync(providerArtifactPath, "utf8")) + const target = stripLeadingComments(fs.readFileSync(targetArtifactPath, "utf8")) + + assert.strictEqual( + target, + provider, + "schemas/openapi/rerum-shared-components.openapi.yaml has drifted from the provider source — re-run .github/workflows/sync-rerum-shared-openapi.yml or copy openapi/components/rerum-shared-components.openapi.yaml over." + ) + }) + it("verifies the shared artifact sync workflow configuration", () => { const workflowPath = path.join(repoRoot, ".github/workflows/sync-rerum-shared-openapi.yml") const workflow = fs.readFileSync(workflowPath, "utf8") - expect(workflow).toContain("openapi/components/rerum-shared-components.openapi.yaml") - expect(workflow).toContain("sync-provider-artifact.yml") - expect(workflow).toContain("repo: 'rerum_openapi'") - expect(workflow).toContain("target_artifact_path: 'schemas/openapi/rerum-shared-components.openapi.yaml'") + assert.match(workflow, /openapi\/components\/rerum-shared-components\.openapi\.yaml/) + assert.match(workflow, /repository:\s*cubap\/rerum_openapi/) + assert.match(workflow, /path:\s*receiver/) + assert.match(workflow, /peter-evans\/create-pull-request@v\d+/) + assert.match(workflow, /schemas\/openapi\/rerum-shared-components\.openapi\.yaml/) + assert.match( + workflow, + /cp\s+openapi\/components\/rerum-shared-components\.openapi\.yaml\s+\S*receiver\/schemas\/openapi\/rerum-shared-components\.openapi\.yaml/, + "workflow's cp command must copy from the canonical source to the receiver target — a retargeted copy would silently corrupt the receiver" + ) + assert.match( + workflow, + /secrets\.OPENAPI(?!\w)/, + "workflow must read the org-level secret named OPENAPI — a rename here breaks the sync silently at the receiver checkout step" + ) }) }) diff --git a/__tests__/provider_sync_artifacts.test.js b/__tests__/provider_sync_artifacts.test.js index b3d3e070..d6ec98a3 100644 --- a/__tests__/provider_sync_artifacts.test.js +++ b/__tests__/provider_sync_artifacts.test.js @@ -1,3 +1,5 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' import fs from "fs" import path from "path" import { fileURLToPath } from "url" @@ -5,12 +7,32 @@ import { fileURLToPath } from "url" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const repoRoot = path.resolve(__dirname, "..") +const contractPath = path.join(repoRoot, "openapi", "contracts", "core-provider.openapi.yaml") +const workflowPath = path.join(repoRoot, ".github", "workflows", "sync-core-provider-contract.yml") describe("provider sync artifacts", () => { - it("syncs only the provider core contract baseline to rerum_openapi", () => { - const workflowPath = path.join(repoRoot, ".github", "workflows", "sync-core-provider-contract.yml") - const workflow = fs.readFileSync(workflowPath, "utf8") + it("the provider contract source file has valid OpenAPI structure", () => { + const contract = fs.readFileSync(contractPath, "utf8") + assert.match(contract, /^openapi: 3\.\d+\.\d+/m) + assert.match(contract, /^\s+title: \S/m) + assert.match(contract, /^\s+version: \d+\.\d+\.\d+/m) + assert.match(contract, /^paths:/m) + }) - expect(workflow).toContain("contracts/core-provider.openapi.yaml") + it("the sync workflow copies the contract to the correct downstream baseline path", () => { + const workflow = fs.readFileSync(workflowPath, "utf8") + // Asserting the literal cp command is what catches a retargeted copy. The target + // path appears in the PR body text too, so a substring match alone is too loose. + assert.match( + workflow, + /cp\s+openapi\/contracts\/core-provider\.openapi\.yaml\s+\S*receiver\/seams\/tinynode-to-rerum\/openapi\/baseline\.openapi\.yaml/ + ) + assert.match(workflow, /repository:\s*cubap\/rerum_openapi/) + assert.match(workflow, /peter-evans\/create-pull-request@v\d+/) + assert.match( + workflow, + /secrets\.OPENAPI(?!\w)/, + "workflow must read the org-level secret named OPENAPI — a rename here breaks the sync silently at the receiver checkout step" + ) }) }) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 0123514d..aacb2094 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -1,173 +1,135 @@ -/** - * 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 app from "../app.js" -import fs from "fs" - -describe('Check to see that all expected top level route patterns exist.', () => { - - it('/v1 -- mounted ', async () => { - const response = await request(app).get('/v1') - expect(response.statusCode).not.toBe(404) - }) - - it('/client -- mounted ', async () => { - const response = await request(app).get('/client/register') - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/id/{_id} -- mounted', async () => { - const response = await request(app).get('/v1/id/test-mounted-id') - // Mounted route with unknown id should 404 (not an unmapped endpoint 404) - expect(response.statusCode).toBe(404) - }) - - it('/v1/since/{_id} -- mounted', async () => { - const response = await request(app).get('/v1/since/test-mounted-id') - // Mounted route with unknown id should 404 - expect(response.statusCode).toBe(404) - }) - - it('/v1/history/{_id} -- mounted', async () => { - const response = await request(app).get('/v1/history/test-mounted-id') - // Mounted route with unknown id should 404 - expect(response.statusCode).toBe(404) - }) - +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import fs from 'fs' +import request from 'supertest' + +import app from '../app.js' + +const mountedTopLevelRoutes = [ + { name: '/v1/', method: 'get', path: '/v1/', expectedStatus: 200 }, + { name: '/client/register', method: 'get', path: '/client/register', expectedStatus: 200 }, + { name: '/v1/id/{_id}', method: 'get', path: '/v1/id/test-mounted-id', expectedStatus: 404 }, + { name: '/v1/since/{_id}', method: 'get', path: '/v1/since/test-mounted-id', expectedStatus: 404 }, + { name: '/v1/history/{_id}', method: 'get', path: '/v1/history/test-mounted-id', expectedStatus: 404 } +] + +const mountedApiRoutes = [ + { name: '/v1/api/query', method: 'post', path: '/v1/api/query', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/create', method: 'post', path: '/v1/api/create', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/bulkCreate', method: 'post', path: '/v1/api/bulkCreate', headers: { 'Content-Type': 'application/json' }, body: [{ mounted: true }] }, + { name: '/v1/api/update', method: 'put', path: '/v1/api/update', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/bulkUpdate', method: 'put', path: '/v1/api/bulkUpdate', headers: { 'Content-Type': 'application/json' }, body: [{ mounted: true }] }, + { name: '/v1/api/overwrite', method: 'put', path: '/v1/api/overwrite', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/patch', method: 'patch', path: '/v1/api/patch', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/set', method: 'patch', path: '/v1/api/set', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/unset', method: 'patch', path: '/v1/api/unset', headers: { 'Content-Type': 'application/json' }, body: { mounted: true } }, + { name: '/v1/api/delete/{id}', method: 'delete', path: '/v1/api/delete/test-mounted-id' }, + { name: '/v1/api/release/{id}', method: 'patch', path: '/v1/api/release/test-mounted-id' }, + { name: '/v1/api/search', method: 'post', path: '/v1/api/search', headers: { 'Content-Type': 'text/plain' }, body: 'mounted search' }, + { name: '/v1/api/search/phrase', method: 'post', path: '/v1/api/search/phrase', headers: { 'Content-Type': 'text/plain' }, body: 'mounted phrase search' } +] + +describe('Mounted route surface', () => { + for (const route of mountedTopLevelRoutes) { + it(`${route.name} is mounted`, async () => { + const response = await request(app)[route.method](route.path) + assert.strictEqual(response.statusCode, route.expectedStatus) + }) + } + + for (const route of mountedApiRoutes) { + it(`${route.name} is mounted`, async () => { + let pending = request(app)[route.method](route.path) + for (const [headerName, headerValue] of Object.entries(route.headers ?? {})) { + pending = pending.set(headerName, headerValue) + } + if (route.body !== undefined) { + pending = pending.send(route.body) + } + const response = await pending + assert.notStrictEqual(response.statusCode, 404) + }) + } }) -describe('Check to see that all /v1/api/ route patterns exist.', () => { - - it('/v1/api/query -- mounted ', async () => { - const response = await request(app) - .post('/v1/api/query') - .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/create -- mounted ', async () => { +describe('Auth pipeline', () => { + // Build a structurally valid JWT with alg:HS256 and an agent claim. The downstream + // _extractUser would happily decode this; only the real auth() middleware rejects + // it for the wrong algorithm (default config is RS256-only). A no-op stand-in for + // auth() would let the request reach the controller and succeed. + const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url') + const payload = Buffer.from(JSON.stringify({ + sub: 'auth-pipeline-test', + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/non-bot-agent' + })).toString('base64url') + const fakeJwt = `${header}.${payload}.fakesignature` + + it('rejects an HS256-signed token with an invalid_token / alg-rejection error', async () => { const response = await request(app) .post('/v1/api/create') + .set('Authorization', `Bearer ${fakeJwt}`) .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/bulkCreate -- mounted ', async () => { - const response = await request(app) - .post('/v1/api/bulkCreate') - .set('Content-Type', 'application/json') - .send([{ mounted: true }]) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/update -- mounted ', async () => { - const response = await request(app) - .put('/v1/api/update') - .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/bulkUpdate -- mounted ', async () => { - const response = await request(app) - .put('/v1/api/bulkUpdate') - .set('Content-Type', 'application/json') - .send([{ mounted: true }]) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/overwrite -- mounted ', async () => { - const response = await request(app) - .post('/v1/api/overwrite') - .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/patch -- mounted ', async () => { - const response = await request(app) - .patch('/v1/api/patch') - .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/set -- mounted ', async () => { - const response = await request(app) - .patch('/v1/api/set') - .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) + .send({ test: 'value' }) + // 401 alone could come from any of several auth failures (missing JWKS, issuer mismatch, + // expired token...). Assert the body carries the JWKS library's specific invalid_token + // error and the "alg" rejection message so this test fails for the right reason. + assert.strictEqual(response.statusCode, 401) + assert.match(response.text, /invalid_token/) + assert.match(response.text, /alg/i) }) +}) - it('/v1/api/unset -- mounted ', async () => { +describe('Body parser limits', () => { + it('returns 413 when a JSON body exceeds the 5 MB limit', async () => { + // 6 MB of payload + JSON framing pushes the request body well above the 5 MB limit set in app.js. + const oversizePayload = { content: 'a'.repeat(6 * 1024 * 1024) } const response = await request(app) - .patch('/v1/api/unset') + .post('/v1/api/create') .set('Content-Type', 'application/json') - .send({ mounted: true }) - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/delete/{id} -- mounted ', async () => { - const response = await request(app).delete('/v1/api/delete/test-mounted-id') - expect(response.statusCode).not.toBe(404) + .send(oversizePayload) + assert.strictEqual(response.statusCode, 413) }) - it('/v1/api/release/{id} -- mounted ', async () => { - const response = await request(app).patch('/v1/api/release/test-mounted-id') - expect(response.statusCode).not.toBe(404) - }) - - it('/v1/api/search -- mounted ', async () => { + it('returns 413 when a text body exceeds the 4 KB limit on /api/search', async () => { + const oversizeText = 'a'.repeat(5000) const response = await request(app) .post('/v1/api/search') .set('Content-Type', 'text/plain') - .send('mounted search') - expect(response.statusCode).not.toBe(404) + .send(oversizeText) + assert.strictEqual(response.statusCode, 413) }) - - it('/v1/api/search/phrase -- mounted ', async () => { - const response = await request(app) - .post('/v1/api/search/phrase') - .set('Content-Type', 'text/plain') - .send('mounted phrase search') - expect(response.statusCode).not.toBe(404) - }) - -}) - -describe('Check to see that critical static files are present', () => { - it('/public folder files', () => { - const filePath = './public/' // Replace with the actual file path - expect(fs.existsSync(filePath+"stylesheets/api.css")).toBeTruthy() - expect(fs.existsSync(filePath+"stylesheets/style.css")).toBeTruthy() - expect(fs.existsSync(filePath+"index.html")).toBeTruthy() - expect(fs.existsSync(filePath+"API.html")).toBeTruthy() - expect(fs.existsSync(filePath+"context.json")).toBeTruthy() - expect(fs.existsSync(filePath+"favicon.ico")).toBeTruthy() - expect(fs.existsSync(filePath+"maintenance.html")).toBeTruthy() - expect(fs.existsSync(filePath+"terms.txt")).toBeTruthy() - expect(fs.existsSync(filePath+"talend.jpg")).toBeTruthy() - }); }) -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+"CODE_OF_CONDUCT.md")).toBeTruthy() - expect(fs.existsSync(filePath+"CONTRIBUTING.md")).toBeTruthy() - expect(fs.existsSync(filePath+"README.md")).toBeTruthy() - expect(fs.existsSync(filePath+"LICENSE")).toBeTruthy() - expect(fs.existsSync(filePath+".gitignore")).toBeTruthy() - expect(fs.existsSync(filePath+"jest.config.js")).toBeTruthy() - expect(fs.existsSync(filePath+"package.json")).toBeTruthy() +describe('Critical project assets', () => { + it('keeps required public files in place', () => { + const requiredPublicFiles = [ + 'stylesheets/api.css', + 'stylesheets/style.css', + 'index.html', + 'API.html', + 'context.json', + 'maintenance.html', + 'terms.txt' + ] + + for (const filePath of requiredPublicFiles) { + assert.ok(fs.existsSync(`./public/${filePath}`), `Missing ./public/${filePath}`) + } + }) + + it('keeps required repository files in place', () => { + const requiredRepoFiles = [ + 'CODEOWNERS', + 'CODE_OF_CONDUCT.md', + 'CONTRIBUTING.md', + 'README.md', + 'LICENSE', + '.gitignore', + 'package.json' + ] + + for (const filePath of requiredRepoFiles) { + assert.ok(fs.existsSync(`./${filePath}`), `Missing ./${filePath}`) + } }) }) diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js new file mode 100644 index 00000000..900ca7c3 --- /dev/null +++ b/__tests__/utils.test.js @@ -0,0 +1,290 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' + +import utils from '../utils.js' +import { + generateSlugId, + getAllVersions, + alterHistoryNext, + parseDocumentID, + _contextid, + idNegotiation, + getPagination +} from '../controllers/utils.js' +import { db, resetMocks } from '../database/index.js' + +describe('utils.js auth gates', () => { + it('isDeleted returns true only for objects with __deleted', () => { + assert.strictEqual(utils.isDeleted({}), false) + assert.strictEqual(utils.isDeleted({ data: 'live' }), false) + assert.strictEqual(utils.isDeleted({ __deleted: { time: '2024-01-01' } }), true) + }) + + it('isReleased returns true only when __rerum.isReleased is non-empty', () => { + assert.strictEqual(utils.isReleased({}), false) + assert.strictEqual(utils.isReleased({ __rerum: {} }), false) + assert.strictEqual(utils.isReleased({ __rerum: { isReleased: '' } }), false) + assert.strictEqual(utils.isReleased({ __rerum: { isReleased: '2024-01-01' } }), true) + }) + + it('isGenerator returns true only when the agent matches __rerum.generatedBy', () => { + assert.strictEqual( + utils.isGenerator({ __rerum: { generatedBy: 'alice' } }, 'alice'), + true + ) + assert.strictEqual( + utils.isGenerator({ __rerum: { generatedBy: 'alice' } }, 'bob'), + false + ) + }) +}) + +describe('utils.js configureRerumOptions', () => { + it('overwrites user-supplied __rerum.generatedBy with the passed-in generator (no attribution forgery)', () => { + const result = utils.configureRerumOptions( + 'https://store.rerum.io/v1/id/legitimate-agent', + { __rerum: { generatedBy: 'https://attacker.example/forged' } }, + false, + false + ) + assert.strictEqual(result.__rerum.generatedBy, 'https://store.rerum.io/v1/id/legitimate-agent') + }) +}) + +describe('controllers/utils.js generateSlugId', () => { + it('returns code 11000 when the proposed slug already exists', async () => { + resetMocks() + db.findOne.mockResolvedValueOnce({ _id: 'taken-slug' }) + const result = await generateSlugId('taken-slug', () => {}) + assert.strictEqual(result.code, 11000) + assert.strictEqual(result.slug_id, 'taken-slug') + }) + + it('returns code 0 when the proposed slug is free', async () => { + resetMocks() + db.findOne.mockResolvedValueOnce(null) + const result = await generateSlugId('free-slug', () => {}) + assert.strictEqual(result.code, 0) + assert.strictEqual(result.slug_id, 'free-slug') + }) +}) + +describe('controllers/utils.js getAllVersions', () => { + const ROOT_ID = 'https://store.rerum.io/v1/id/root-id' + const V1_ID = 'https://store.rerum.io/v1/id/v1-id' + + const rootObj = { + _id: 'root-id', + '@id': ROOT_ID, + __rerum: { history: { prime: 'root', previous: '', next: [V1_ID] } } + } + const v1Obj = { + _id: 'v1-id', + '@id': V1_ID, + __rerum: { history: { prime: ROOT_ID, previous: ROOT_ID, next: [] } } + } + + it('returns [root, ...descendants] when given a root object directly', async () => { + resetMocks() + db.find.mockReturnValueOnce({ toArray: () => Promise.resolve([v1Obj]) }) + + const result = await getAllVersions(rootObj) + + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0]['@id'], ROOT_ID) + assert.strictEqual(result[1]['@id'], V1_ID) + }) + + it('fetches the root from the database when given a non-root object', async () => { + resetMocks() + db.findOne.mockResolvedValueOnce(rootObj) + db.find.mockReturnValueOnce({ toArray: () => Promise.resolve([v1Obj]) }) + + const result = await getAllVersions(v1Obj) + + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0]['@id'], ROOT_ID, 'root must be first') + assert.strictEqual(result[1]['@id'], V1_ID) + }) + + it('throws when the root object cannot be found in the database', async () => { + resetMocks() + db.findOne.mockResolvedValueOnce(null) + const orphan = { + _id: 'orphan-id', + __rerum: { history: { prime: 'https://store.rerum.io/v1/id/missing-root', previous: '', next: [] } } + } + await assert.rejects(() => getAllVersions(orphan), /not found/) + }) +}) + +describe('controllers/utils.js alterHistoryNext', () => { + it('appends newNextID to history.next and persists via db.replaceOne', async () => { + resetMocks() + const obj = { + _id: 'parent-id', + '@id': 'https://store.rerum.io/v1/id/parent-id', + __rerum: { history: { prime: 'root', previous: '', next: [] } } + } + const newNextID = 'https://store.rerum.io/v1/id/child-id' + let captured + db.replaceOne.mockImplementationOnce(async (filter, replacement) => { + captured = { filter, replacement } + return { modifiedCount: 1 } + }) + + const result = await alterHistoryNext(obj, newNextID) + + assert.strictEqual(result, true) + assert.deepStrictEqual(obj.__rerum.history.next, [newNextID]) + assert.ok(captured, 'db.replaceOne must be called') + assert.deepStrictEqual(captured.filter, { _id: 'parent-id' }) + assert.ok(captured.replacement.__rerum.history.next.includes(newNextID)) + }) + + it('does NOT call db.replaceOne when newNextID is already in history.next', async () => { + resetMocks() + const existingID = 'https://store.rerum.io/v1/id/already-linked' + const obj = { + _id: 'parent-id', + __rerum: { history: { prime: 'root', previous: '', next: [existingID] } } + } + let replaceOneCalled = false + db.replaceOne.mockImplementationOnce(async () => { + replaceOneCalled = true + return { modifiedCount: 1 } + }) + + const result = await alterHistoryNext(obj, existingID) + + assert.strictEqual(result, true) + assert.strictEqual(replaceOneCalled, false, 'db.replaceOne should NOT be called for an existing link') + assert.deepStrictEqual(obj.__rerum.history.next, [existingID], 'history.next should not be duplicated') + }) + + it('returns false when db.replaceOne reports modifiedCount === 0', async () => { + resetMocks() + const obj = { + _id: 'parent-id', + __rerum: { history: { prime: 'root', previous: '', next: [] } } + } + db.replaceOne.mockResolvedValueOnce({ modifiedCount: 0 }) + + const result = await alterHistoryNext(obj, 'https://store.rerum.io/v1/id/new-child') + + assert.strictEqual(result, false) + }) +}) + +describe('controllers/utils.js parseDocumentID', () => { + it('returns the last URL segment for an http(s) URL', () => { + assert.strictEqual(parseDocumentID('https://store.rerum.io/v1/id/abc123'), 'abc123') + assert.strictEqual(parseDocumentID('http://example.com/id/xyz'), 'xyz') + }) + + it('throws on non-string input', () => { + assert.throws(() => parseDocumentID(123), /Unable to parse/) + assert.throws(() => parseDocumentID(null), /Unable to parse/) + assert.throws(() => parseDocumentID({}), /Unable to parse/) + }) + + it('throws on non-URL strings', () => { + assert.throws(() => parseDocumentID('not-a-url'), /URL strings/) + assert.throws(() => parseDocumentID('ftp://example.com/id'), /URL strings/) + }) +}) + +describe('controllers/utils.js _contextid', () => { + it('returns true for known JSON-LD contexts', () => { + assert.strictEqual(_contextid('https://store.rerum.io/v1/context.json'), true) + assert.strictEqual(_contextid('http://iiif.io/api/presentation/3/context.json'), true) + assert.strictEqual(_contextid('http://www.w3.org/ns/anno.jsonld'), true) + assert.strictEqual(_contextid('http://www.w3.org/ns/oa.jsonld'), true) + }) + + it('returns false for unknown contexts', () => { + assert.strictEqual(_contextid('http://example.com/random/context.json'), false) + assert.strictEqual(_contextid(''), false) + }) + + it('returns true when an array of contexts contains a known one', () => { + assert.strictEqual( + _contextid(['http://example.com/other', 'http://iiif.io/api/presentation/3/context.json']), + true + ) + }) + + it('returns false for non-string, non-array input', () => { + assert.strictEqual(_contextid(null), false) + assert.strictEqual(_contextid(123), false) + assert.strictEqual(_contextid({}), false) + }) +}) + +describe('controllers/utils.js idNegotiation edge cases', () => { + it('returns undefined for falsy input', () => { + assert.strictEqual(idNegotiation(undefined), undefined) + assert.strictEqual(idNegotiation(null), undefined) + }) + + it('strips _id and returns the body unchanged when there is no @context', () => { + const obj = { _id: 'abc', foo: 'bar' } + const result = idNegotiation(obj) + assert.strictEqual(result._id, undefined) + assert.strictEqual(result.foo, 'bar') + }) + + it('strips @id and projects it onto `id` when @context is a known JSON-LD context', () => { + const result = idNegotiation({ + '@context': 'http://iiif.io/api/presentation/3/context.json', + _id: 'example', + '@id': `${process.env.RERUM_ID_PREFIX}example`, + test: 'item' + }) + assert.strictEqual(result._id, undefined) + assert.strictEqual(result['@id'], undefined) + assert.strictEqual(result.id, `${process.env.RERUM_ID_PREFIX}example`) + assert.strictEqual(result.test, 'item') + }) + + it('keeps @id and preserves an existing `id` field when @context is unknown', () => { + const result = idNegotiation({ + '@context': 'http://example.org/context.json', + _id: 'example', + '@id': `${process.env.RERUM_ID_PREFIX}example`, + id: 'test_example', + test: 'item' + }) + assert.strictEqual(result._id, undefined) + assert.strictEqual(result['@id'], `${process.env.RERUM_ID_PREFIX}example`) + assert.strictEqual(result.id, 'test_example') + assert.strictEqual(result.test, 'item') + }) +}) + +describe('controllers/utils.js getPagination', () => { + it('returns the default limit and skip 0 for an empty query', () => { + const result = getPagination({}, 100) + assert.strictEqual(result.limit, 100) + assert.strictEqual(result.skip, 0) + }) + + it('parses numeric string values from the query', () => { + const result = getPagination({ limit: '50', skip: '10' }) + assert.strictEqual(result.limit, 50) + assert.strictEqual(result.skip, 10) + }) + + it('falls back to defaults on non-numeric or non-positive input', () => { + const result = getPagination({ limit: 'bogus', skip: 'nope' }, 100) + assert.strictEqual(result.limit, 100) + assert.strictEqual(result.skip, 0) + }) + + it('clamps an unreasonably large limit below the max', () => { + const huge = Number.MAX_SAFE_INTEGER + const result = getPagination({ limit: String(huge) }) + assert.ok(result.limit > 0) + assert.ok(result.limit < huge, `limit should be clamped below ${huge}`) + }) +}) diff --git a/app.js b/app.js index e8c8a7a1..ebeb80c7 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,6 @@ import express from 'express' import path from 'path' import cookieParser from 'cookie-parser' -import dotenv from 'dotenv' -dotenv.config() import logger from 'morgan' import cors from 'cors' import indexRouter from './routes/index.js' @@ -57,8 +55,8 @@ app.use( }) ) app.use(logger('dev')) -app.use(express.json({ type: ["application/json", "application/ld+json"] })) -app.use(express.text()) +app.use(express.json({ type: ["application/json", "application/ld+json"], limit: "5mb" })) +app.use(express.text({ limit: "4kb" })) app.use(cookieParser()) //Publicly available scripts, CSS, and HTML pages. diff --git a/auth/__tests__/token.test.js b/auth/__tests__/token.test.js new file mode 100644 index 00000000..fa67f2dd --- /dev/null +++ b/auth/__tests__/token.test.js @@ -0,0 +1,234 @@ +import { afterEach, describe, it, mock } from 'node:test' +import assert from 'node:assert/strict' + +import auth from '../../auth/index.js' + +const originalReadonly = process.env.READONLY +const originalBotAgent = process.env.BOT_AGENT +const originalAgentClaim = process.env.RERUM_AGENT_CLAIM + +// `process.env.X = undefined` writes the literal string "undefined" rather than +// deleting the key. When the original value is unset, restore must delete. +function restoreEnv(key, value) { + if (value === undefined) delete process.env[key] + else process.env[key] = value +} + +function createResponse() { + return { + statusCode: 200, + body: undefined, + status(code) { + this.statusCode = code + return this + }, + json(payload) { + this.body = payload + return this + }, + send(payload) { + this.body = payload + return this + } + } +} + +function makeRequest(authorizationHeaderValue) { + return { + header(name) { + return name.toLowerCase() === 'authorization' ? authorizationHeaderValue : undefined + } + } +} + +function makeBearer(payload, header = { alg: 'RS256', typ: 'JWT' }) { + const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url') + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url') + return `Bearer ${encodedHeader}.${encodedPayload}.fake-signature` +} + +afterEach(() => { + restoreEnv('READONLY', originalReadonly) + restoreEnv('BOT_AGENT', originalBotAgent) + restoreEnv('RERUM_AGENT_CLAIM', originalAgentClaim) + mock.restoreAll() +}) + +describe('auth middleware helpers', () => { + it('READONLY blocks writes when the server is in readonly mode', () => { + process.env.READONLY = 'true' + const response = createResponse() + let nextCalled = false + + auth.READONLY({}, response, () => { + nextCalled = true + }) + + assert.strictEqual(nextCalled, false) + assert.strictEqual(response.statusCode, 503) + assert.match(response.body.message, /read only/i) + }) + + it('READONLY passes through when the server is writable', () => { + process.env.READONLY = 'false' + const response = createResponse() + let nextCalled = false + + auth.READONLY({}, response, () => { + nextCalled = true + }) + + assert.strictEqual(nextCalled, true) + assert.strictEqual(response.body, undefined) + }) + + it('isBot matches the configured bot claim', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + process.env.BOT_AGENT = 'https://store.rerum.io/v1/id/bot-agent' + + const result = auth.isBot({ + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/bot-agent' + }) + + assert.strictEqual(result, true) + }) + + it('isBot returns false for a non-bot agent claim', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + process.env.BOT_AGENT = 'https://store.rerum.io/v1/id/bot-agent' + + const result = auth.isBot({ + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/some-other-user' + }) + + assert.strictEqual(result, false) + }) + + // an unset BOT_AGENT made `undefined === undefined` true and bypassed auth + // for any invalid-token request whose payload was missing the agent claim. + it('isBot returns false when BOT_AGENT is unset', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + delete process.env.BOT_AGENT + + assert.strictEqual(auth.isBot({}), false) + assert.strictEqual(auth.isBot({ unknownClaim: 'x' }), false) + assert.strictEqual(auth.isBot({ 'http://store.rerum.io/agent': 'anyone' }), false) + }) + + it('isGenerator matches the generating agent claim', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + + const result = auth.isGenerator( + { __rerum: { generatedBy: 'https://store.rerum.io/v1/id/agent007' } }, + { 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/agent007' } + ) + + assert.strictEqual(result, true) + }) +}) + +describe('_tokenError (checkJwt[2])', () => { + const _tokenError = auth.checkJwt[2] + + it('forwards non-invalid_token errors unchanged', () => { + const err = Object.assign(new Error('something else broke'), { code: 'something_else' }) + let received = 'not-called' + _tokenError(err, makeRequest('Bearer a.b.c'), createResponse(), (e) => { received = e }) + assert.strictEqual(received, err) + }) + + it('forwards invalid_token errors when the agent is not a bot', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + process.env.BOT_AGENT = 'https://store.rerum.io/v1/id/bot-agent' + const err = { code: 'invalid_token', message: 'signature verification failed' } + const req = makeRequest(makeBearer({ + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/regular-user' + })) + let received = 'not-called' + _tokenError(err, req, createResponse(), (e) => { received = e }) + assert.strictEqual(received, err) + }) + + it('bypasses invalid_token errors when the agent matches BOT_AGENT', () => { + process.env.RERUM_AGENT_CLAIM = 'http://store.rerum.io/agent' + process.env.BOT_AGENT = 'https://store.rerum.io/v1/id/bot-agent' + const err = { code: 'invalid_token', message: 'token expired' } + const req = makeRequest(makeBearer({ + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/bot-agent' + })) + let received = 'not-called' + _tokenError(err, req, createResponse(), (e) => { received = e }) + assert.strictEqual(received, undefined, 'bot bypass should call next() with no arg') + }) +}) + +describe('_extractUser (checkJwt[3])', () => { + const _extractUser = auth.checkJwt[3] + + it('decodes the JWT payload into req.user', () => { + const payload = { + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/agent007', + sub: 'user-x' + } + const req = makeRequest(makeBearer(payload)) + let nextCalled = false + let receivedError + _extractUser(req, createResponse(), (e) => { + nextCalled = true + receivedError = e + }) + assert.strictEqual(nextCalled, true) + assert.strictEqual(receivedError, undefined) + assert.deepStrictEqual(req.user, payload) + }) + + it('returns a 401 error when the Authorization header is malformed', () => { + const req = makeRequest('Bearer not-a-jwt') + let received + _extractUser(req, createResponse(), (e) => { received = e }) + assert.ok(received, 'next should be called with an error') + assert.strictEqual(received.status, 401) + assert.strictEqual(received.statusCode, 401) + }) +}) + +describe('auth token refresh helpers', () => { + it('generateNewAccessToken returns the Auth0 payload on success', async () => { + process.env.CLIENT_ID = 'client-id' + process.env.CLIENT_SECRET = 'client-secret' + process.env.RERUM_PREFIX = 'https://store.rerum.io/v1' + + mock.method(globalThis, 'fetch', async () => ({ + async json() { + return { + access_token: 'access-token', + refresh_token: 'refresh-token' + } + } + })) + + const response = createResponse() + await auth.generateNewAccessToken({ body: { refresh_token: 'incoming-refresh-token' } }, response) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.access_token, 'access-token') + }) + + it('generateNewAccessToken returns 500 for Auth0 error payloads', async () => { + const errorDescription = 'bad refresh token' + mock.method(globalThis, 'fetch', async () => ({ + async json() { + return { + error: true, + error_description: errorDescription + } + } + })) + + const response = createResponse() + await auth.generateNewAccessToken({ body: { refresh_token: 'bad-token' } }, response) + + assert.strictEqual(response.statusCode, 500) + assert.strictEqual(response.body, errorDescription) + }) +}) diff --git a/auth/__tests__/token.test.txt b/auth/__tests__/token.test.txt deleted file mode 100644 index bb09f524..00000000 --- a/auth/__tests__/token.test.txt +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Use this to perform end to end interactions with Auth0 TPEN3 Application. - * The app passes NodeJS Express Request and Response objects which have Bearer Tokens in their headers. - * Those Bearer tokens are pulled from the Request 'Authorization' header. - * The app should be able to verify the token is legitimate and gleam a TPEN3 user from it - * - * Note that in this test we are performing real Auth0 communication. - * There are areas of the app that could benefit from having this communication exist as a mock. - * If that is what you need, get out of here and go see /auth/__mocks__ -*/ - -import auth from "../../auth/index.js" -import httpMocks from "node-mocks-http" - -const goodToken = "TODO -- MAKE ME PROGRAMMATICALLY" - -// A mocked HTTP POST 'create' request with an Authorization header. The token should be a valid one. -const mockRequest_with_token = httpMocks.createRequest({ - method: 'POST', - url: '/create', - body: { - hello: 'world' - }, - headers: { - "Authorization" : `Bearer ${goodToken}` - } -}) - -// A mocked HTTP POST 'create' request without an Authorization header (no Bearer token) -const mockRequest_without_token = httpMocks.createRequest({ - method: 'POST', - url: '/create', - body: { - hello: 'world' - } -}) - -// A mocked HTTP response stub -const mockResponse = httpMocks.createResponse() - -// A mocked express next() call -const nextFunction = jest.fn() - -// REDO -describe('Auth0 Interactions',()=>{ - - it('reject empty request without headers (INCOMPLETE)',async ()=>{ - const resp = await auth.checkJwt[0](mockRequest_without_token,mockResponse,nextFunction) - expect(resp).toBe("token error") - }) - - it('with "authorization" header (INCOMPLETE)', async () => { - const resp = await auth.checkJwt[0](mockRequest_with_token,mockResponse,nextFunction) - expect(resp).toBe("valid token") - }) -}) diff --git a/auth/index.js b/auth/index.js index 0780773e..7de3a892 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,6 +1,4 @@ import { auth } from 'express-oauth2-jwt-bearer' -import dotenv from 'dotenv' -dotenv.config() const _tokenError = function (err, req, res, next) { if(!err.code || err.code !== "invalid_token"){ @@ -19,7 +17,7 @@ const _tokenError = function (err, req, res, next) { e.message = e.statusMessage = `This token did not contain a known RERUM agent.` e.status = 401 e.statusCode = 401 - next(e) + return next(e) } next(err) } @@ -30,7 +28,7 @@ const _extractUser = (req, res, next) => { next() } catch(e){ - e.message = e.statusMessage = `This token did not contain a known RERUM agent.}` + e.message = e.statusMessage = `This token did not contain a known RERUM agent.` e.status = 401 e.statusCode = 401 next(e) @@ -168,6 +166,7 @@ const isGenerator = (obj, userObj) => { * @returns Boolean for matching ID. */ const isBot = (userObj) => { + if (!process.env.BOT_AGENT || !process.env.RERUM_AGENT_CLAIM) return false return process.env.BOT_AGENT === userObj[process.env.RERUM_AGENT_CLAIM] } diff --git a/bin/rerum_v1.js b/bin/rerum_v1.js index 8b269269..0585afc9 100644 --- a/bin/rerum_v1.js +++ b/bin/rerum_v1.js @@ -1,5 +1,10 @@ #!/usr/bin/env node +// Must be the first import: populates process.env from .env synchronously +// before any other module reads it. See env-loader.js for why this lives +// here instead of being injected via PM2 `node_args`. +import '../env-loader.js' + /** * Module dependencies. */ @@ -8,8 +13,6 @@ import app from '../app.js' import debug from 'debug' debug('rerum_server_nodejs:server') import http from "http" -import dotenv from "dotenv" -dotenv.config() /** * Get port from environment and store in Express. diff --git a/controllers/bulk.js b/controllers/bulk.js index 83ba7f06..94e138a9 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -3,7 +3,7 @@ /** * Bulk operations controller for RERUM operations * Handles bulk create and bulk update operations - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/crud.js b/controllers/crud.js index 85feba2a..758a048a 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -2,7 +2,7 @@ /** * Basic CRUD operations for RERUM v1 - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' diff --git a/controllers/delete.js b/controllers/delete.js index caa3ced8..bfc86621 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -2,7 +2,7 @@ /** * Delete operations for RERUM v1 - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { db } from '../database/index.js' import utils from '../utils.js' diff --git a/controllers/gog.js b/controllers/gog.js index b9b11857..a381c9fd 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -3,7 +3,7 @@ /** * Gallery of Glosses (GOG) controller for RERUM operations * Handles specialized operations for the Gallery of Glosses application - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/history.js b/controllers/history.js index 5f72083e..eb299708 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -3,7 +3,7 @@ /** * History controller for RERUM operations * Handles history, since, and HEAD request operations - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/overwrite.js b/controllers/overwrite.js index 3fd62e6a..4a7895e5 100644 --- a/controllers/overwrite.js +++ b/controllers/overwrite.js @@ -3,7 +3,7 @@ /** * Overwrite controller for RERUM operations * Handles overwrite operations with optimistic locking - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/patchSet.js b/controllers/patchSet.js index b892b555..858c5122 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -3,7 +3,7 @@ /** * PATCH Set controller for RERUM operations * Handles PATCH operations that add new keys only - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index faeaf292..95c78247 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -3,7 +3,7 @@ /** * PATCH Unset controller for RERUM operations * Handles PATCH operations that remove keys - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index 74c97d11..9d627449 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -3,7 +3,7 @@ /** * PATCH Update controller for RERUM operations * Handles PATCH updates that modify existing keys - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index ab10020a..5447175c 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -3,7 +3,7 @@ /** * PUT Update controller for RERUM operations * Handles PUT updates and import operations - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' diff --git a/controllers/release.js b/controllers/release.js index afd466e3..81b8a6a3 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -3,7 +3,7 @@ /** * Release controller for RERUM operations * Handles release operations and associated tree management - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' @@ -37,17 +37,24 @@ const release = async function (req, res, next) { } } if (id){ - let originalObject + let originalObject try { originalObject = await db.findOne({"$or":[{"_id": id}, {"__rerum.slug": id}]}) - } + } catch (error) { return next(utils.createExpressError(error)) } + if (null === originalObject) { + err = Object.assign(err, { + message: `No object with this id could be found in RERUM. Cannot release. ${err.message}`, + status: 404 + }) + return next(utils.createExpressError(err)) + } let safe_original = structuredClone(originalObject) let previousReleasedID = safe_original.__rerum.releases.previous let nextReleases = safe_original.__rerum.releases.next - + if (utils.isDeleted(safe_original)) { err = Object.assign(err, { message: `The object you are trying to release is deleted. ${err.message}`, @@ -69,51 +76,54 @@ const release = async function (req, res, next) { if (err.status) { return next(utils.createExpressError(err)) } - if (null !== originalObject){ - safe_original["__rerum"].isReleased = new Date(Date.now()).toISOString().replace("Z", "") - safe_original["__rerum"].releases.replaces = previousReleasedID - if(slug){ - safe_original["__rerum"].slug = slug - } - if (previousReleasedID !== "") { - // A releases tree exists and an ancestral object is being released. + safe_original["__rerum"].isReleased = new Date(Date.now()).toISOString().replace("Z", "") + safe_original["__rerum"].releases.replaces = previousReleasedID + if(slug){ + safe_original["__rerum"].slug = slug + } + if (previousReleasedID !== "") { + // A releases tree exists and an ancestral object is being released. + treeHealed = await healReleasesTree(safe_original) + } + else { + // There was no releases previous value. + if (nextReleases.length > 0) { + // The release tree has been established and a descendant object is now being released. treeHealed = await healReleasesTree(safe_original) - } - else { - // There was no releases previous value. - if (nextReleases.length > 0) { - // The release tree has been established and a descendant object is now being released. - treeHealed = await healReleasesTree(safe_original) - } - else { - // The release tree has not been established - treeHealed = await establishReleasesTree(safe_original) - } } - if (treeHealed) { - // If the tree was established/healed - // perform the update to isReleased of the object being released. Its - // releases.next[] and releases.previous are already correct. - let releasedObject = safe_original - let result - try { - result = await db.replaceOne({ "_id": id }, releasedObject) - } - catch (error) { - return next(utils.createExpressError(error)) - } - if (result.modifiedCount == 0) { - //result didn't error out, the action was not performed. Sometimes, this is a neutral thing. Sometimes it is indicative of an error. - } - res.set(utils.configureWebAnnoHeadersFor(releasedObject)) - console.log(releasedObject._id+" has been released") - releasedObject = idNegotiation(releasedObject) - releasedObject.new_obj_state = structuredClone(releasedObject) - res.location(releasedObject[_contextid(releasedObject["@context"]) ? "id":"@id"]) - res.json(releasedObject) - return - } + else { + // The release tree has not been established + treeHealed = await establishReleasesTree(safe_original) + } + } + if (!treeHealed) { + err = Object.assign(err, { + message: `The releases tree could not be established or healed for this object. The release was not performed. ${err.message}`, + status: 500 + }) + return next(utils.createExpressError(err)) + } + // The tree was established/healed. + // Perform the update to isReleased of the object being released. Its + // releases.next[] and releases.previous are already correct. + let releasedObject = safe_original + let result + try { + result = await db.replaceOne({ "_id": id }, releasedObject) + } + catch (error) { + return next(utils.createExpressError(error)) + } + if (result.modifiedCount == 0) { + //result didn't error out, the action was not performed. Sometimes, this is a neutral thing. Sometimes it is indicative of an error. } + res.set(utils.configureWebAnnoHeadersFor(releasedObject)) + console.log(releasedObject._id+" has been released") + releasedObject = idNegotiation(releasedObject) + releasedObject.new_obj_state = structuredClone(releasedObject) + res.location(releasedObject[_contextid(releasedObject["@context"]) ? "id":"@id"]) + res.json(releasedObject) + return } else{ //This was a bad request diff --git a/controllers/update.js b/controllers/update.js index 88dec30d..8da80104 100644 --- a/controllers/update.js +++ b/controllers/update.js @@ -3,7 +3,7 @@ /** * Update controller aggregator for RERUM operations * This file imports and re-exports all update operations - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ // Import individual update operations diff --git a/controllers/utils.js b/controllers/utils.js index f995e040..dd455d05 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -2,7 +2,7 @@ /** * Utility functions for RERUM controllers - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' diff --git a/database/__mocks__/index.js b/database/__mocks__/index.js index 39204ed4..51ec6a2c 100644 --- a/database/__mocks__/index.js +++ b/database/__mocks__/index.js @@ -1,45 +1,93 @@ /** - * Jest mock for the database/index.js module. - * Replaces all MongoDB operations with jest.fn() stubs so tests - * can run without a live database connection. - * - * Defaults (can be overridden per-test with mockResolvedValueOnce / mockReturnValueOnce): - * db.findOne → resolves null - * db.find → returns a chainable cursor whose toArray resolves [] - * db.insertOne → resolves { insertedId: 'testid123' } - * db.replaceOne→ resolves { modifiedCount: 1 } - * db.bulkWrite → resolves { result: { insertedIds: [] }, insertedCount: 0 } - * db.deleteOne → resolves { deletedCount: 1 } - * newID → returns 'testid123' - * isValidID → returns false (forces ObjectID() path in controllers) - * connected → resolves true - * - * @author thehabes + * Native test mock for database/index.js. + * Exposes a small mock-function surface used by the node:test suites. */ -import { jest } from '@jest/globals' +const registeredMocks = new Set() -/** Chainable cursor stub returned by db.find() */ -const mockCursor = { - limit: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - toArray: jest.fn().mockResolvedValue([]) +function createMockFunction(implementation = () => undefined) { + const onceQueue = [] + let currentImplementation = implementation + + function fn(...args) { + const activeImplementation = onceQueue.length > 0 ? onceQueue.shift() : currentImplementation + return activeImplementation.apply(this, args) + } + + fn.mockImplementation = (nextImplementation) => { + currentImplementation = nextImplementation + return fn + } + fn.mockImplementationOnce = (nextImplementation) => { + onceQueue.push(nextImplementation) + return fn + } + fn.mockReturnValue = (value) => fn.mockImplementation(() => value) + fn.mockReturnValueOnce = (value) => fn.mockImplementationOnce(() => value) + fn.mockResolvedValue = (value) => fn.mockImplementation(() => Promise.resolve(value)) + fn.mockResolvedValueOnce = (value) => fn.mockImplementationOnce(() => Promise.resolve(value)) + fn.mockRejectedValue = (value) => fn.mockImplementation(() => Promise.reject(value)) + fn.mockRejectedValueOnce = (value) => fn.mockImplementationOnce(() => Promise.reject(value)) + fn.mockReturnThis = () => fn.mockImplementation(function () { return this }) + fn.mockReset = () => { + onceQueue.length = 0 + currentImplementation = () => undefined + return fn + } + + registeredMocks.add(fn) + return fn +} + +function createCursor() { + return { + limit: createMockFunction(function () { return this }), + skip: createMockFunction(function () { return this }), + toArray: createMockFunction(() => Promise.resolve([])) + } +} + +const defaultBulkWriteResponse = () => ({ + result: { insertedIds: [{ _id: 'bulkid1' }, { _id: 'bulkid2' }] }, + insertedIds: {}, + insertedCount: 0 +}) + +export function resetMocks() { + for (const fn of registeredMocks) { + fn.mockReset() + } + + db.findOne.mockResolvedValue(null) + db.find.mockReturnValue(createCursor()) + db.aggregate.mockReturnValue(createCursor()) + db.insertOne.mockResolvedValue({ insertedId: 'testid123' }) + db.replaceOne.mockResolvedValue({ modifiedCount: 1 }) + db.countDocuments.mockResolvedValue(0) + db.bulkWrite.mockResolvedValue(defaultBulkWriteResponse()) + db.deleteOne.mockResolvedValue({ deletedCount: 1 }) + db.updateOne.mockResolvedValue({ modifiedCount: 1 }) + db.findOneAndUpdate.mockResolvedValue({ value: null }) + newID.mockReturnValue('testid123') + isValidID.mockReturnValue(false) + connected.mockResolvedValue(true) } export const db = { - findOne: jest.fn().mockResolvedValue(null), - find: jest.fn().mockReturnValue(mockCursor), - insertOne: jest.fn().mockResolvedValue({ insertedId: 'testid123' }), - replaceOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }), - countDocuments: jest.fn().mockResolvedValue(0), - bulkWrite: jest.fn().mockResolvedValue({ - result: { insertedIds: [{ _id: 'bulkid1' }, { _id: 'bulkid2' }] }, - insertedIds: {}, - insertedCount: 0 - }), - deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }) + findOne: createMockFunction(() => Promise.resolve(null)), + find: createMockFunction(() => createCursor()), + aggregate: createMockFunction(() => createCursor()), + insertOne: createMockFunction(() => Promise.resolve({ insertedId: 'testid123' })), + replaceOne: createMockFunction(() => Promise.resolve({ modifiedCount: 1 })), + countDocuments: createMockFunction(() => Promise.resolve(0)), + bulkWrite: createMockFunction(() => Promise.resolve(defaultBulkWriteResponse())), + deleteOne: createMockFunction(() => Promise.resolve({ deletedCount: 1 })), + updateOne: createMockFunction(() => Promise.resolve({ modifiedCount: 1 })), + findOneAndUpdate: createMockFunction(() => Promise.resolve({ value: null })) } -export const newID = jest.fn().mockReturnValue('testid123') -export const isValidID = jest.fn().mockReturnValue(false) -export const connected = jest.fn().mockResolvedValue(true) +export const newID = createMockFunction(() => 'testid123') +export const isValidID = createMockFunction(() => false) +export const connected = createMockFunction(() => Promise.resolve(true)) + +resetMocks() diff --git a/database/index.js b/database/index.js index cf8d374a..8ae5c70d 100644 --- a/database/index.js +++ b/database/index.js @@ -1,6 +1,4 @@ import { MongoClient, ObjectId } from 'mongodb' -import dotenv from "dotenv" -dotenv.config() const client = new MongoClient(process.env.MONGO_CONNECTION_STRING) const newID = () => new ObjectId().toHexString() diff --git a/db-controller.js b/db-controller.js index 07aa6f65..43ee5201 100644 --- a/db-controller.js +++ b/db-controller.js @@ -3,7 +3,7 @@ /** * Main controller aggregating all RERUM operations * This file now imports from organized controller modules - * @author Claude Sonnet 4, cubap, thehabes + * @author cubap, thehabes */ // Import controller modules diff --git a/ecosystem.config.json b/ecosystem.config.json new file mode 100644 index 00000000..4ff8f9a7 --- /dev/null +++ b/ecosystem.config.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/pm2-ecosystem.json", + "apps": [ + { + "name": "rerum_v1", + "script": "./bin/rerum_v1.js", + "instances": "max", + "exec_mode": "cluster", + "env_development": { "NODE_ENV": "development" }, + "env_production": { "NODE_ENV": "production" }, + "listen_timeout": 10000, + "kill_timeout": 5000, + "wait_ready": false, + "max_memory_restart": "500M", + "log_date_format": "YYYY-MM-DD HH:mm:ss Z", + "merge_logs": true, + "autorestart": true, + "max_restarts": 10, + "min_uptime": "10s" + } + ] +} diff --git a/env-loader.js b/env-loader.js new file mode 100644 index 00000000..eda0db68 --- /dev/null +++ b/env-loader.js @@ -0,0 +1,81 @@ +/** + * Environment Variable Loader + * + * Preloads variables from a `.env` file into `process.env` before any + * application module reads them. Runs synchronously as a side-effect import. + * + * Two entry points use this loader, by design: + * 1. The app entry script `bin/rerum_v1.js` imports this file FIRST, + * before any other module. Because ES module imports evaluate in + * source order, this guarantees `process.env` is populated before + * `app.js` (or anything it pulls in) reads it. This is the path used + * under PM2 in development and production, and it is independent of + * PM2's `node_args` / cluster-worker `execArgv` plumbing — which was + * observed not to fire reliably on the RHEL servers + * (vlcdhp02 / vlcdhprdp02). + * 2. The test/coverage scripts in `package.json` pass it via + * `node --import ./env-loader.js`, because tests do not go through + * `bin/rerum_v1.js`. + * + * Replaces the previous `--env-file-if-exists=.env` Node CLI flag, which + * was unreliable on the RHEL servers under PM2. + * + * Behavior: + * - Resolves `.env` against this file's own directory (via + * `import.meta.url`), NOT `process.cwd()`. PM2 cluster workers on + * RHEL have shown inconsistent cwd handling; anchoring to the file + * location guarantees the same `.env` is found regardless of where + * the process was launched from. + * - Permissive: if `.env` is missing, logs a warning and continues + * with whatever is already in `process.env`. + * - Non-destructive: does NOT overwrite keys already set in + * `process.env`, so PM2-injected env, CI secrets, and shell exports + * still take precedence. + * - Strips a leading UTF-8 BOM (U+FEFF) if present, so `.env` files + * saved by Windows editors do not lose their first key. + * - No external dependency — uses only `node:fs`, `node:path`, and + * `node:url`. + */ + +import { readFileSync, existsSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const RESET = '\x1b[0m' +const YELLOW = '\x1b[33m' +const GREEN = '\x1b[32m' +const CYAN = '\x1b[36m' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const envPath = resolve(__dirname, '.env') + +if (!existsSync(envPath)) { + console.warn(`${YELLOW}[env-loader] .env not found at ${envPath} — continuing with existing process.env${RESET}`) +} else { + let loaded = 0 + let skipped = 0 + let contents = readFileSync(envPath, 'utf8') + if (contents.charCodeAt(0) === 0xFEFF) contents = contents.slice(1) + for (const raw of contents.split(/\r?\n/)) { + const line = raw.trim() + if (!line || line.startsWith('#')) continue + const eq = line.indexOf('=') + if (eq === -1) continue + const key = line.slice(0, eq).trim() + if (!key) continue + let val = line.slice(eq + 1).trim() + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1) + } + if (key in process.env) { + skipped++ + continue + } + process.env[key] = val + loaded++ + } + console.log(`${GREEN}[env-loader] loaded ${loaded} vars from ${envPath}${RESET}${skipped ? ` ${CYAN}(skipped ${skipped} already set)${RESET}` : ''}`) +} diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index c9a7e458..00000000 --- a/jest.config.js +++ /dev/null @@ -1,222 +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, - - // Automatically clear mock call history before every test (preserves default implementations) - clearMocks: true, - - // Redirect all database/index.js imports to the mock so tests never need a live DB - moduleNameMapper: { - '^.+/database/index\\.js$': '/database/__mocks__/index.js' - }, - - displayName: { - name: 'RERUM v1', - color: 'cyan', - }, - - // 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", - "**/db-controller.js", - "**/routes/*.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", - - // Sometimes the MongoDB or Network are choking and the tests take longer than 5s. - // testTimeout: 10000, - - // 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/contracts/core-provider.openapi.yaml b/openapi/contracts/core-provider.openapi.yaml similarity index 56% rename from contracts/core-provider.openapi.yaml rename to openapi/contracts/core-provider.openapi.yaml index 705d2fa8..7174b34a 100644 --- a/contracts/core-provider.openapi.yaml +++ b/openapi/contracts/core-provider.openapi.yaml @@ -17,6 +17,8 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '503': + $ref: '#/components/responses/ServiceUnavailable' /id/{id}: get: summary: Read object by id @@ -30,6 +32,8 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '404': + $ref: '#/components/responses/NotFound' head: summary: Read object headers by id operationId: headObjectById @@ -38,6 +42,8 @@ paths: responses: '200': description: Object headers + '404': + $ref: '#/components/responses/NotFound' /since/{id}: get: summary: Read updates since id @@ -51,6 +57,8 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericArray' + '404': + $ref: '#/components/responses/NotFound' head: summary: Read update headers since id operationId: headSince @@ -59,6 +67,8 @@ paths: responses: '200': description: Incremental update headers + '404': + $ref: '#/components/responses/NotFound' /history/{id}: get: summary: Read object history by id @@ -72,6 +82,8 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericArray' + '404': + $ref: '#/components/responses/NotFound' head: summary: Read object history headers by id operationId: headHistory @@ -80,6 +92,8 @@ paths: responses: '200': description: Version history headers + '404': + $ref: '#/components/responses/NotFound' /api/query: post: summary: Query objects @@ -97,12 +111,20 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericArray' + '400': + $ref: '#/components/responses/BadRequest' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' head: summary: Query object headers operationId: headQueryObjects responses: '200': description: Query result headers + '404': + $ref: '#/components/responses/NotFound' /api/search: post: summary: Search objects by keywords @@ -123,6 +145,12 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericArray' + '400': + $ref: '#/components/responses/BadRequest' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/search/phrase: post: summary: Search objects by phrase @@ -143,6 +171,12 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericArray' + '400': + $ref: '#/components/responses/BadRequest' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/create: post: summary: Create object @@ -154,12 +188,22 @@ paths: schema: $ref: '#/components/schemas/GenericObject' responses: - '200': + '201': description: Created object content: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/bulkCreate: post: summary: Create multiple objects @@ -171,12 +215,20 @@ paths: schema: $ref: '#/components/schemas/GenericArray' responses: - '200': + '201': description: Created objects content: application/json: schema: $ref: '#/components/schemas/GenericArray' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/update: put: summary: Update object @@ -194,6 +246,18 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/bulkUpdate: put: summary: Update multiple objects @@ -206,11 +270,19 @@ paths: $ref: '#/components/schemas/GenericArray' responses: '200': - description: Updated objects + description: Updated objects. Items that were not found or were already deleted are silently skipped. content: application/json: schema: $ref: '#/components/schemas/GenericArray' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/overwrite: put: summary: Overwrite object @@ -228,6 +300,20 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' /api/patch: patch: summary: Patch object @@ -245,6 +331,18 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' post: summary: Patch object via override-compatible POST operationId: patchObjectViaPost @@ -261,6 +359,20 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '405': + description: Method not allowed — POST is only permitted with X-HTTP-Method-Override. + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' /api/set: patch: summary: Add properties to object @@ -278,6 +390,18 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' post: summary: Add properties to object via override-compatible POST operationId: setObjectPropertiesViaPost @@ -294,6 +418,20 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '405': + description: Method not allowed — POST is only permitted with X-HTTP-Method-Override. + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' /api/unset: patch: summary: Remove properties from object @@ -311,6 +449,18 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' post: summary: Remove properties from object via override-compatible POST operationId: unsetObjectPropertiesViaPost @@ -327,6 +477,20 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '405': + description: Method not allowed — POST is only permitted with X-HTTP-Method-Override. + '413': + $ref: '#/components/responses/PayloadTooLarge' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '501': + $ref: '#/components/responses/NotImplemented' /api/delete/{id}: delete: summary: Delete object by id @@ -334,12 +498,14 @@ paths: parameters: - $ref: '#/components/parameters/ObjectId' responses: - '200': - description: Deletion result - content: - application/json: - schema: - $ref: '#/components/schemas/GenericObject' + '204': + description: Object marked deleted; no response body. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' /api/release/{id}: patch: summary: Release object by id @@ -353,6 +519,16 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericObject' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' components: parameters: ObjectId: @@ -369,3 +545,22 @@ components: type: array items: $ref: '#/components/schemas/GenericObject' + responses: + BadRequest: + description: Bad request — the request body was missing, empty, or otherwise invalid. + Unauthorized: + description: Unauthorized — missing, malformed, or invalid bearer token. + Forbidden: + description: Forbidden — the target object is deleted, released, or not owned by the requesting agent. + NotFound: + description: Not found — no object exists for the supplied id. + Conflict: + description: Conflict — the supplied optimistic-lock version does not match the current object version. + PayloadTooLarge: + description: Payload too large — the request body exceeded the configured size limit. + UnsupportedMediaType: + description: Unsupported media type — the Content-Type header was not acceptable for this endpoint. + NotImplemented: + description: Not implemented — operating on a non-RERUM object via this endpoint is not yet supported. + ServiceUnavailable: + description: Service unavailable — the server is in maintenance or read-only mode. diff --git a/package-lock.json b/package-lock.json index b6f164ea..55b08738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "cookie-parser": "~1.4.7", "cors": "^2.8.6", "debug": "~4.4.3", - "dotenv": "^17.3.1", "express": "^5.2.1", "express-oauth2-jwt-bearer": "^1.7.4", "express-urlrewrite": "~2.0.3", @@ -20,8 +19,7 @@ "morgan": "~1.10.1" }, "devDependencies": { - "@jest/globals": "^30.3.0", - "jest": "^30.3.0", + "c8": "^10.1.2", "supertest": "^7.2.2" }, "engines": { @@ -29,3885 +27,1260 @@ "npm": ">=11.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "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.28.5", - "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.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "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.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "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.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "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.29.0", - "@babel/types": "^7.29.0", - "@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.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "sparse-bitfield": "^3.0.3" } }, - "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/@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": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "node": "^14.21.3 || >=16" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "@noble/hashes": "^1.1.5" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "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": ">=6.9.0" + "node": ">=14" } }, - "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/@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/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@types/webidl-conversions": "*" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "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": ">=6.9.0" + "node": ">= 0.6" } }, - "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/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/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "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": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "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/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "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 + }, + "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/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" + "safe-buffer": "5.1.2" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.8" } }, - "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, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "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.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18" + }, + "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/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "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.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "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.28.6" - }, "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.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "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.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "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.28.6" + "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" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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-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/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.14.5" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "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": { - "@babel/helper-plugin-utils": "^7.28.6" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "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/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=8" } }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "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": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "color-name": "~1.1.4" }, "engines": { - "node": ">=6.9.0" + "node": ">=7.0.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==", + "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/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "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", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "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, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "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", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">= 0.6" } }, - "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==", + "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": "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" - }, + "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": ">=12" + "node": ">= 0.6" } }, - "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==", - "dev": true, - "license": "ISC", + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "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" + "cookie": "0.7.2", + "cookie-signature": "1.0.6" }, "engines": { - "node": ">=8" + "node": ">= 0.8.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==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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 + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, "engines": { - "node": ">=8" + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@jest/console": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", - "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "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": { - "@jest/types": "30.3.0", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "slash": "^3.0.0" + "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/core": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", - "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", - "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": { - "@jest/console": "30.3.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.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.3.0", - "jest-config": "30.3.0", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-resolve-dependencies": "30.3.0", - "jest-runner": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "jest-watcher": "30.3.0", - "pretty-format": "30.3.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "ms": "^2.1.3" }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "node-notifier": { + "supports-color": { "optional": true } } }, - "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "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": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.4.0" } }, - "node_modules/@jest/environment": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", - "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", - "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/fake-timers": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "jest-mock": "30.3.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.8" } }, - "node_modules/@jest/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "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", "dependencies": { - "expect": "30.3.0", - "jest-snapshot": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "asap": "^2.0.0", + "wrappy": "1" } }, - "node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", - "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": { - "@jest/get-type": "30.1.0" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/@jest/fake-timers": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", - "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.3.0", - "@sinonjs/fake-timers": "^15.0.0", - "@types/node": "*", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "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, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", - "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/types": "30.3.0", - "jest-mock": "30.3.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", - "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.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.5.0", - "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.3.0", - "jest-util": "30.3.0", - "jest-worker": "30.3.0", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "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_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "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_modules/@jest/snapshot-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", - "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.3.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_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==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/@jest/test-result": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", - "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.3.0", - "@jest/types": "30.3.0", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", - "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.3.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", - "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.3.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.3.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.3.0", - "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.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", - "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/@mongodb-js/saslprep": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", - "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "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.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "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.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "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": "15.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", - "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", - "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": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.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/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "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/anymatch/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/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.3.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", - "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.3.0", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", - "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", - "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.3.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", - "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.3.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.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "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.1", - "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/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "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.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "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/bson": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", - "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.19.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.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", - "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.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "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.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "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.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "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.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "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.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "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.313", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", - "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", - "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/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.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.3.0", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.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/express-oauth2-jwt-bearer": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.7.4.tgz", - "integrity": "sha512-teO/eyvU8OkJXiP4cRuoJMrp31nNvjnL47MIkso0D/21AqUGv1O+VEiLisrDA8xjkaCBTufYnV1zepCOCLK4vg==", - "license": "MIT", - "dependencies": { - "jose": "^4.15.5" - }, - "engines": { - "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0" - } - }, - "node_modules/express-urlrewrite": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-2.0.3.tgz", - "integrity": "sha512-NjsmtYZ1Lpie+XR7VIrvI6aeAmRQDf9cHyGjdIxlE9sc+NhTx3z6fJ0wfxV4rS7AY9ncCK7JDge+VX3e+DQ9Mg==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "path-to-regexp": "^6.3.0" - } - }, - "node_modules/express/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/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/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "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==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "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==", + "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/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.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "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/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-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "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/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, + "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": ">=8" + "node": ">= 0.8" } }, - "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/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": ">=6" + "node": ">= 0.4" } }, - "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==", - "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==", - "dev": true, + "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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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": "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" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "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", + "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": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "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==", + "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": "BSD-3-Clause", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/istanbul-reports": { + "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "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_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/jest": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", - "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", - "dev": true, + "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": { - "@jest/core": "30.3.0", - "@jest/types": "30.3.0", - "import-local": "^3.2.0", - "jest-cli": "30.3.0" - }, - "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": ">= 0.6" } }, - "node_modules/jest-changed-files": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", - "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", - "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": { - "execa": "^5.1.1", - "jest-util": "30.3.0", - "p-limit": "^3.1.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-circus": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", - "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.3.0", - "@jest/expect": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-runtime": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.0", - "p-limit": "^3.1.0", - "pretty-format": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", - "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", - "dev": true, + "node_modules/express-oauth2-jwt-bearer": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.7.4.tgz", + "integrity": "sha512-teO/eyvU8OkJXiP4cRuoJMrp31nNvjnL47MIkso0D/21AqUGv1O+VEiLisrDA8xjkaCBTufYnV1zepCOCLK4vg==", "license": "MIT", "dependencies": { - "@jest/core": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/types": "30.3.0", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" + "jose": "^4.15.5" }, "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": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0" } }, - "node_modules/jest-config": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", - "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", - "dev": true, + "node_modules/express-urlrewrite": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-2.0.3.tgz", + "integrity": "sha512-NjsmtYZ1Lpie+XR7VIrvI6aeAmRQDf9cHyGjdIxlE9sc+NhTx3z6fJ0wfxV4rS7AY9ncCK7JDge+VX3e+DQ9Mg==", "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.3.0", - "@jest/types": "30.3.0", - "babel-jest": "30.3.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-circus": "30.3.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-runner": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.0", - "parse-json": "^5.2.0", - "pretty-format": "30.3.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 - } + "debug": "^4.3.4", + "path-to-regexp": "^6.3.0" } }, - "node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", - "dev": true, + "node_modules/express/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", - "dependencies": { - "@jest/diff-sequences": "30.3.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.3.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6.6.0" } }, - "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==", - "dev": true, + "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 + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-each": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", - "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "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.3.0", - "chalk": "^4.1.2", - "jest-util": "30.3.0", - "pretty-format": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", - "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "jest-mock": "30.3.0", - "jest-util": "30.3.0", - "jest-validate": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", - "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "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.3.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.3.0", - "jest-worker": "30.3.0", - "picomatch": "^4.0.3", - "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.3.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", - "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "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.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "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.3.0", - "pretty-format": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", - "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.3.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3", - "pretty-format": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", - "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.3.0", - "@types/node": "*", - "jest-util": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", - "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", - "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.3.0", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.3.0", - "jest-validate": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", - "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", - "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.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", - "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.3.0", - "@jest/environment": "30.3.0", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.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.3.0", - "jest-haste-map": "30.3.0", - "jest-leak-detector": "30.3.0", - "jest-message-util": "30.3.0", - "jest-resolve": "30.3.0", - "jest-runtime": "30.3.0", - "jest-util": "30.3.0", - "jest-watcher": "30.3.0", - "jest-worker": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", - "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.3.0", - "@jest/fake-timers": "30.3.0", - "@jest/globals": "30.3.0", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.3.0", - "jest-snapshot": "30.3.0", - "jest-util": "30.3.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.3.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", - "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", - "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.3.0", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.3.0", - "graceful-fs": "^4.2.11", - "jest-diff": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "pretty-format": "30.3.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.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "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" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", - "dev": true, + "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", - "dependencies": { - "@jest/types": "30.3.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, "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-validate": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", - "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "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/get-type": "30.1.0", - "@jest/types": "30.3.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.3.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=8" } }, - "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==", - "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": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-watcher": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", - "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "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/test-result": "30.3.0", - "@jest/types": "30.3.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.3.0", - "string-length": "^4.0.2" + "has-symbols": "^1.0.3" }, "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-worker": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", - "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", - "dev": true, + "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": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.3.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" + "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-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/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": { - "has-flag": "^4.0.0" + "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/chalk/supports-color?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" + "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/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==", - "dev": true, + "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": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 0.10" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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", - "bin": { - "jsesc": "bin/jsesc" - }, "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, + "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==", "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } + "license": "ISC" }, - "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==", + "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", + "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" } }, - "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==", + "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" + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } }, - "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/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" } }, "node_modules/make-dir": { @@ -3939,16 +1312,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", @@ -3985,19 +1348,11 @@ "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", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4007,7 +1362,6 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -4040,16 +1394,6 @@ "url": "https://opencollective.com/express" } }, - "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.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -4184,29 +1528,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", @@ -4216,43 +1537,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.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "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", @@ -4304,22 +1588,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", @@ -4336,45 +1604,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", @@ -4382,25 +1611,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", @@ -4415,132 +1625,51 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", - "engines": { - "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", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "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": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "license": "MIT" - }, - "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": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "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" - }, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=8" } }, - "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": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "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": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4563,23 +1692,6 @@ "node": ">=6" } }, - "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.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -4619,13 +1731,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", @@ -4636,29 +1741,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", @@ -4676,10 +1758,9 @@ } }, "node_modules/router/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", + "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": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -4697,16 +1778,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.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -4866,37 +1937,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/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -4906,26 +1946,6 @@ "memory-pager": "^1.0.2" } }, - "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", @@ -4935,43 +1955,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", @@ -5076,45 +2059,11 @@ "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.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", "dev": true, - "license": "MIT", "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", @@ -5135,7 +2084,6 @@ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", "dev": true, - "license": "MIT", "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", @@ -5150,7 +2098,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.6.0" } @@ -5168,90 +2115,6 @@ "node": ">=8" } }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "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": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "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.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "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/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5273,37 +2136,6 @@ "node": ">=18" } }, - "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", @@ -5318,13 +2150,6 @@ "node": ">= 0.6" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5334,72 +2159,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.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "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", @@ -5424,16 +2183,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/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -5573,20 +2322,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", @@ -5597,13 +2332,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 b054eda2..952c5a1f 100644 --- a/package.json +++ b/package.json @@ -20,19 +20,21 @@ "author": "Research Computing Group (https://slu.edu)", "repository": "github:CenterForDigitalHumanities/rerum_server_nodejs", "engines": { - "node": ">=24.14.0", - "npm": ">=11.0.0" + "node": ">=24.14.0", + "npm": ">=11.0.0" }, "scripts": { - "start": "node ./bin/rerum_v1.js", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "runtest": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + "start": "node --import ./env-loader.js ./bin/rerum_v1.js", + "test": "node --import ./env-loader.js --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "test:ci": "node --import ./env-loader.js --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "test:e2e": "node --import ./env-loader.js --import ./test/bootstrap.js --test --test-name-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "coverage": "c8 --reporter=html --reporter=text --include='controllers/**/*.js' --include='routes/**/*.js' --include='auth/**/*.js' --include='db-controller.js' --include='rest.js' --include='utils.js' --exclude='**/__tests__/**' node --import ./env-loader.js --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "coverage:ci": "c8 --reporter=html --reporter=json --reporter=text --include='controllers/**/*.js' --include='routes/**/*.js' --include='auth/**/*.js' --include='db-controller.js' --include='rest.js' --include='utils.js' --exclude='**/__tests__/**' node --import ./env-loader.js --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js" }, "dependencies": { "cookie-parser": "~1.4.7", "cors": "^2.8.6", "debug": "~4.4.3", - "dotenv": "^17.3.1", "express": "^5.2.1", "express-oauth2-jwt-bearer": "^1.7.4", "express-urlrewrite": "~2.0.3", @@ -40,8 +42,7 @@ "morgan": "~1.10.1" }, "devDependencies": { - "@jest/globals": "^30.3.0", - "jest": "^30.3.0", + "c8": "^10.1.2", "supertest": "^7.2.2" } } diff --git a/rest.js b/rest.js index bff6df47..927e38fa 100644 --- a/rest.js +++ b/rest.js @@ -26,6 +26,23 @@ const checkPatchOverrideSupport = function (req, res) { return undefined !== override && override === "PATCH" } +/** + * Creates middleware to validate PATCH override support for POST requests. + * Returns 405 if the request does not have proper X-HTTP-Method-Override header. + * + * @param {string} message - Error message to send if validation fails + * @returns {Function} Express middleware function + */ +const createPatchOverrideMiddleware = (message) => { + return (req, res, next) => { + if (!checkPatchOverrideSupport(req, res)) { + res.statusMessage = message + return res.status(405).end() + } + next() + } +} + /** * Detects multiple MIME types smuggled into a single Content-Type header. * The following are the cases that should result in a 415 (not a 500) @@ -189,6 +206,9 @@ The requested web page or resource could not be found.` case 409: // These are all handled in db-controller.js already. break + case 413: + // Payload Too Large. Body exceeded the parser limit or document exceeded MongoDB's 16 MB BSON cap. + break case 415: // Unsupported Media Type. The Content-Type header is not acceptable for this endpoint. break @@ -210,4 +230,4 @@ It may not have completed at all, and most likely did not complete successfully. res.status(error.status).send(error.message) } -export default { checkPatchOverrideSupport, verifyJsonContentType, verifyEitherContentType, messenger } +export default { checkPatchOverrideSupport, createPatchOverrideMiddleware, verifyJsonContentType, verifyEitherContentType, messenger } diff --git a/routes/__tests__/bulkCreate.test.js b/routes/__tests__/bulkCreate.test.js index eec9ef89..a2cad396 100644 --- a/routes/__tests__/bulkCreate.test.js +++ b/routes/__tests__/bulkCreate.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -21,10 +22,13 @@ routeTester.use("/bulkCreate", [addAuth, controller.bulkCreate]) const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/bulkCreate' route functions", async () => { - // bulkCreate expects dbResponse.result.insertedIds as an array of objects with _id db.bulkWrite.mockResolvedValueOnce({ result: { insertedIds: [{ _id: 'id1' }, { _id: 'id2' }] }, insertedIds: { 0: 'id1', 1: 'id2' }, @@ -39,14 +43,14 @@ it("'/bulkCreate' route functions", async () => { { test: 'data-2' } ]) - expect(response.statusCode).toBe(201) - expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBe(2) - expect(response.body[0]._id).toBeUndefined() - expect(response.body[1]._id).toBeUndefined() + assert.strictEqual(response.statusCode, 201) + assert.ok(Array.isArray(response.body)) + assert.strictEqual(response.body.length, 2) + assert.strictEqual(response.body[0]._id, undefined) + assert.strictEqual(response.body[1]._id, undefined) const linkHeader = response.headers['link'] - expect(linkHeader).toBeDefined() - expect(String(linkHeader)).toContain(`${MOCK_PREFIX}id1`) - expect(String(linkHeader)).toContain(`${MOCK_PREFIX}id2`) + assert.ok(linkHeader) + assert.match(String(linkHeader), new RegExp(`${MOCK_PREFIX}id1`)) + assert.match(String(linkHeader), new RegExp(`${MOCK_PREFIX}id2`)) }) diff --git a/routes/__tests__/bulkUpdate.test.js b/routes/__tests__/bulkUpdate.test.js index b9b619d6..0defcabd 100644 --- a/routes/__tests__/bulkUpdate.test.js +++ b/routes/__tests__/bulkUpdate.test.js @@ -1,9 +1,11 @@ -import { jest } from "@jest/globals" +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' +import { db, resetMocks } from '../../database/index.js' // Here is the auth mock so we get a req.user and the controller can function without a NPE. const addAuth = (req, res, next) => { @@ -14,9 +16,49 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /bulkCreate route without auth that will use controller.bulkCreate +// Mount our own /bulkUpdate route without auth that will use controller.bulkUpdate routeTester.use("/bulkUpdate", [addAuth, controller.bulkUpdate]) -it.skip("'/bulkUpdate' route functions", async () => { - // TODO without hitting the v1/id/11111 object because it is already abused. +process.env.RERUM_ID_PREFIX ??= 'https://store.rerum.io/v1/id/' + +beforeEach(() => { + resetMocks() +}) + +it("'/bulkUpdate' route functions", async () => { + const originalId = '11111' + const originalObject = { + _id: originalId, + '@id': `${process.env.RERUM_ID_PREFIX}${originalId}`, + test: 'old-value', + __rerum: { + generatedBy: 'https://store.rerum.io/v1/id/agent007', + history: { prime: 'root', previous: '', next: [] }, + isReleased: '', + isOverwritten: '', + releases: { previous: '', next: [], replaces: '' }, + createdAt: '2025-01-01T00:00:00.000' + } + } + + db.findOne.mockResolvedValueOnce(originalObject) + db.bulkWrite.mockResolvedValueOnce({ + result: { insertedIds: [{ _id: 'bulk-update-id' }] }, + insertedIds: { 0: 'bulk-update-id' }, + insertedCount: 1 + }) + + const response = await request(routeTester) + .put('/bulkUpdate') + .set('Content-Type', 'application/json') + .send([ + { '@id': `${process.env.RERUM_ID_PREFIX}${originalId}`, test: 'updated-value' } + ]) + + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) + assert.strictEqual(response.body.length, 1) + assert.strictEqual(response.body[0]._id, undefined) + assert.strictEqual(response.body[0].test, 'updated-value') + assert.ok(String(response.headers.link).includes('bulk-update-id')) }) diff --git a/routes/__tests__/client.test.txt b/routes/__tests__/client.test.txt deleted file mode 100644 index 30404ce4..00000000 --- a/routes/__tests__/client.test.txt +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file diff --git a/routes/__tests__/compatability.test.txt b/routes/__tests__/compatability.test.txt deleted file mode 100644 index 30404ce4..00000000 --- a/routes/__tests__/compatability.test.txt +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file diff --git a/routes/__tests__/contentType.test.js b/routes/__tests__/contentType.test.js index c0989b4f..a6c20de5 100644 --- a/routes/__tests__/contentType.test.js +++ b/routes/__tests__/contentType.test.js @@ -1,3 +1,5 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' /** * Tests for the Content-Type validation middlewares verifyJsonContentType and verifyEitherContentType. * The following are examples of good Content-Type headers that should not result in a 415 @@ -45,235 +47,98 @@ routeTester.post("/json-or-text-endpoint", rest.verifyEitherContentType, (req, r routeTester.use(rest.messenger) describe("verifyJsonContentType middleware", () => { - - it("accepts application/json", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json") - .send({ test: "data" }) - expect(response.statusCode).toBe(200) - }) - - it("accepts application/ld+json", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/ld+json") - // Must stringify manually; supertest's .send(object) would override Content-Type to application/json - .send(JSON.stringify({ "@context": "http://example.org", test: "ld" })) - expect(response.statusCode).toBe(200) - }) - - it("returns 415 for trailing semicolon without parameter", async () => { - // A trailing semicolon is malformed per RFC 7231 and express.json() won't parse it - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json;") - .send('{"test":"trailing-semicolon"}') - expect(response.statusCode).toBe(415) - }) - - it("accepts application/json with charset parameter", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json; charset=utf-8") - .send({ test: "charset" }) - expect(response.statusCode).toBe(200) - }) - - it("accepts application/ld+json with charset parameter", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/ld+json; charset=utf-8") - // Must stringify manually; supertest's .send(object) would override Content-Type to application/json - .send(JSON.stringify({ "@context": "http://example.org", test: "ld-charset" })) - expect(response.statusCode).toBe(200) - }) - - it("accepts application/json with quoted comma in parameter", async () => { - // Exercises the hasMultipleContentTypes quoted-string bypass: a="b,c" contains a comma - // but it is inside quotes, so it should not be treated as a smuggled MIME type. - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", 'application/json; a="b,c"; xy=z') - .send({ test: "quoted-param" }) - expect(response.statusCode).toBe(200) - }) - - it("accepts Content-Type with unusual casing", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "Application/JSON") - .send({ test: "casing" }) - expect(response.statusCode).toBe(200) - }) - - it("returns 415 for missing Content-Type", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .unset("Content-Type") + const acceptedJsonCases = [ + { name: 'accepts application/json', contentType: 'application/json', body: { test: 'data' }, expectedStatus: 200 }, + { name: 'accepts application/ld+json', contentType: 'application/ld+json', body: JSON.stringify({ '@context': 'http://example.org', test: 'ld' }), expectedStatus: 200 }, + { name: 'accepts application/json with charset parameter', contentType: 'application/json; charset=utf-8', body: { test: 'charset' }, expectedStatus: 200 }, + { name: 'accepts application/ld+json with charset parameter', contentType: 'application/ld+json; charset=utf-8', body: JSON.stringify({ '@context': 'http://example.org', test: 'ld-charset' }), expectedStatus: 200 }, + { name: 'accepts application/json with quoted comma in parameter', contentType: 'application/json; a="b,c"; xy=z', body: { test: 'quoted-param' }, expectedStatus: 200 }, + { name: 'accepts Content-Type with unusual casing', contentType: 'Application/JSON', body: { test: 'casing' }, expectedStatus: 200 } + ] + + const rejectedJsonCases = [ + { name: 'returns 415 for trailing semicolon without parameter', contentType: 'application/json;', body: '{"test":"trailing-semicolon"}' }, + { name: 'returns 415 for text/plain', contentType: 'text/plain', body: 'some plain text' }, + { name: 'returns 415 for space-separated multiple Content-Type values', contentType: 'application/json text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for comma-separated multiple Content-Type values', contentType: 'application/json, text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for comma-injected Content-Type parameter', contentType: 'application/json; charset=utf-8, text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for semicolon-smuggled MIME type', contentType: 'application/json; text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for semicolon-smuggled MIME type with valid parameter', contentType: 'application/json; charset=utf-8; text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for space-smuggled MIME type after valid parameter', contentType: 'application/json; a=b; c=d text/plain', body: '{"test":"data"}' } + ] + + for (const testCase of acceptedJsonCases) { + it(testCase.name, async () => { + const response = await request(routeTester) + .post('/json-endpoint') + .set('Content-Type', testCase.contentType) + .send(testCase.body) + assert.strictEqual(response.statusCode, testCase.expectedStatus) + }) + } + + it('returns 415 for missing Content-Type', async () => { + const response = await request(routeTester) + .post('/json-endpoint') + .unset('Content-Type') .send(Buffer.from('{"test":"data"}')) - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for text/plain", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "text/plain") - .send("some plain text") - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for space-separated multiple Content-Type values", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for comma-separated multiple Content-Type values", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json, text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for comma-injected Content-Type parameter", async () => { - // Even though the MIME type portion is valid, the comma in the full header - // is rejected to prevent Content-Type smuggling via parameter injection. - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json; charset=utf-8, text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for semicolon-smuggled MIME type", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json; text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for semicolon-smuggled MIME type with valid parameter", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json; charset=utf-8; text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for space-smuggled MIME type after valid parameter", async () => { - const response = await request(routeTester) - .post("/json-endpoint") - .set("Content-Type", "application/json; a=b; c=d text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) + assert.strictEqual(response.statusCode, 415) + }) + + for (const testCase of rejectedJsonCases) { + it(testCase.name, async () => { + const response = await request(routeTester) + .post('/json-endpoint') + .set('Content-Type', testCase.contentType) + .send(testCase.body) + assert.strictEqual(response.statusCode, 415) + }) + } }) describe("verifyEitherContentType middleware", () => { - - it("accepts application/json", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json") - .send({ searchText: "hello" }) - expect(response.statusCode).toBe(200) - }) - - it("accepts application/ld+json", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/ld+json") - // Must stringify manually; supertest's .send(object) would override Content-Type to application/json - .send(JSON.stringify({ "@context": "http://example.org" })) - expect(response.statusCode).toBe(200) - }) - - it("accepts text/plain", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "text/plain") - .send("search terms") - expect(response.statusCode).toBe(200) - }) - - it("accepts text/plain with quoted comma in parameter", async () => { - // Exercises the hasMultipleContentTypes quoted-string bypass: a="b,c" contains a comma - // but it is inside quotes, so it should not be treated as a smuggled MIME type. - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", 'text/plain; a="b,c"') - .send("search terms") - expect(response.statusCode).toBe(200) - }) - - it("returns 415 for missing Content-Type", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .unset("Content-Type") - .send(Buffer.from("hello")) - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for application/xml", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/xml") - .send("") - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for space-separated multiple Content-Type values", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for comma-separated multiple Content-Type values", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json, text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for comma-injected Content-Type parameter", async () => { - // Even though the MIME type portion is valid, the comma in the full header - // is rejected to prevent Content-Type smuggling via parameter injection. - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json; charset=utf-8, text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for semicolon-smuggled MIME type", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json; text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for semicolon-smuggled MIME type with valid parameter", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json; charset=utf-8; text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) - - it("returns 415 for space-smuggled MIME type after valid parameter", async () => { - const response = await request(routeTester) - .post("/json-or-text-endpoint") - .set("Content-Type", "application/json; a=b; c=d text/plain") - .send('{"test":"data"}') - expect(response.statusCode).toBe(415) - }) + const acceptedEitherCases = [ + { name: 'accepts application/json', contentType: 'application/json', body: { searchText: 'hello' }, expectedStatus: 200 }, + { name: 'accepts application/ld+json', contentType: 'application/ld+json', body: JSON.stringify({ '@context': 'http://example.org' }), expectedStatus: 200 }, + { name: 'accepts text/plain', contentType: 'text/plain', body: 'search terms', expectedStatus: 200 }, + { name: 'accepts text/plain with quoted comma in parameter', contentType: 'text/plain; a="b,c"', body: 'search terms', expectedStatus: 200 } + ] + + const rejectedEitherCases = [ + { name: 'returns 415 for application/xml', contentType: 'application/xml', body: '' }, + { name: 'returns 415 for space-separated multiple Content-Type values', contentType: 'application/json text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for comma-separated multiple Content-Type values', contentType: 'application/json, text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for comma-injected Content-Type parameter', contentType: 'application/json; charset=utf-8, text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for semicolon-smuggled MIME type', contentType: 'application/json; text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for semicolon-smuggled MIME type with valid parameter', contentType: 'application/json; charset=utf-8; text/plain', body: '{"test":"data"}' }, + { name: 'returns 415 for space-smuggled MIME type after valid parameter', contentType: 'application/json; a=b; c=d text/plain', body: '{"test":"data"}' } + ] + + for (const testCase of acceptedEitherCases) { + it(testCase.name, async () => { + const response = await request(routeTester) + .post('/json-or-text-endpoint') + .set('Content-Type', testCase.contentType) + .send(testCase.body) + assert.strictEqual(response.statusCode, testCase.expectedStatus) + }) + } + + it('returns 415 for missing Content-Type', async () => { + const response = await request(routeTester) + .post('/json-or-text-endpoint') + .unset('Content-Type') + .send(Buffer.from('hello')) + assert.strictEqual(response.statusCode, 415) + }) + + for (const testCase of rejectedEitherCases) { + it(testCase.name, async () => { + const response = await request(routeTester) + .post('/json-or-text-endpoint') + .set('Content-Type', testCase.contentType) + .send(testCase.body) + assert.strictEqual(response.statusCode, 415) + }) + } }) diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 22171dcf..e83dc883 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -1,11 +1,10 @@ -import { jest } from "@jest/globals" +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' import express from "express" import request from "supertest" -import { db } from '../../database/index.js' +import { resetMocks } from '../../database/index.js' import controller from '../../db-controller.js' -const rerum_uri = `${process.env.RERUM_ID_PREFIX}123456` - // Here is the auth mock so we get a req.user and the controller can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} @@ -18,23 +17,22 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.create routeTester.use("/create", [addAuth, controller.create]) +beforeEach(() => { + resetMocks() +}) + it("'/create' route functions", async () => { - // insertOne mock default resolves { insertedId: 'testid123' } - // newID mock returns 'testid123', so @id = RERUM_ID_PREFIX + 'testid123' const response = await request(routeTester) .post("/create") .set("Content-Type", "application/json") .send({ test: "item" }) - expect(response.statusCode).toBe(201) - expect(response.body["@id"] ?? response.body.id).toBeTruthy() - expect(response.body._id).toBeUndefined() - expect(response.body.__rerum).toBeDefined() - expect(response.body.test).toBe("item") - // location header should match the returned @id / id - const returnedId = response.body["@id"] ?? response.body.id - expect(response.headers["location"]).toBe(returnedId) -}) -it.skip("Support setting valid '_id' on '/create' request body.", async () => { - // TODO + assert.strictEqual(response.statusCode, 201) + assert.ok(response.body["@id"] ?? response.body.id) + assert.strictEqual(response.body._id, undefined) + assert.ok(response.body.__rerum) + assert.strictEqual(response.body.test, "item") + + const returnedId = response.body["@id"] ?? response.body.id + assert.strictEqual(response.headers["location"], returnedId) }) diff --git a/routes/__tests__/crud_routes_function.txt b/routes/__tests__/crud_routes_function.txt deleted file mode 100644 index 511c3caa..00000000 --- a/routes/__tests__/crud_routes_function.txt +++ /dev/null @@ -1,584 +0,0 @@ -*************** - - DEPRECATED - -*************** - - - -import request from 'supertest' -//Fun fact, if you don't require app, you don't get coverage even though the tests run just fine. -import app from '../../app.js' -//This is so we can do Mongo specific things with the objects in this test, like actually remove them from the db. -import controller from '../../db-controller.js' - -//A super fun note. If you do request(app), the tests will fail due to race conditions. -//request = request(app) -let req = request("http://localhost:3333") - -describe( - 'Test that each available endpoint succeeds given a properly formatted req and req body.', - () => { - - it('End to end /v1/id/{_id}. It should respond 404, this object does not exist.', - async () => { - const response = await req.get('/v1/id/potato') - .set('Content-Type', 'application/json; charset=utf-8') - expect(response.statusCode).toBe(404) - } - ) - - it('End to end /v1/since/{_id}. It should respond 404, this object does not exist.', - done => { - req - .get('/v1/since/potato') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(404, done) - } - ) - - it('End to end /v1/history/{_id}. It should respond 404, this object does not exist.', - done => { - req - .get('/v1/history/potato') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(404, done) - } - ) - - it('End to end /v1/id/. Forget the _id in the URL pattern. ' + - 'It should respond 404, this page/object does not exist.', - done => { - req - .get('/v1/id/') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(404, done) - } - ) - - it('End to end /v1/since/. Forget the _id in the URL pattern. ' + - 'It should respond 404, this page/object does not exist.', - done => { - req - .get('/v1/since/') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(404, done) - } - ) - - it('End to end /v1/history/. Forget the _id in the URL pattern. ' + - 'It should respond 404, this page/object does not exist.', - done => { - req - .get('/v1/history/') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(404, done) - } - ) - - it('End to end /v1/id/{_id}. Do a properly formatted GET for an object by id. ' + - 'It should respond 200 with a body that is a JSON object with an "@id" property.', - done => { - req - .get('/v1/id/11111') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["cache-control"]).toBeTruthy() - expect(response.headers["last-modified"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.headers["location"]).toBeTruthy() - expect(response.body["@id"]).toBeTruthy() - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end HEAD req to /v1/id/{_id}.' + - 'It should respond 200 and the Content-Length response header should be set.', - done => { - req - .head('/v1/id/11111') - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end /v1/since/{_id}. Do a properly formatted /since call by GETting for an existing _id. ' + - 'It should respond 200 with a body that is of type Array.' + - 'It should strip the property "_id" from the response.', - done => { - req - .get('/v1/since/11111') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) - expect(response.body[0]._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end HEAD req to /v1/since/{_id}.' + - 'It should respond 200 and the Content-Length response header should be set.', - done => { - req - .head('/v1/since/11111') - .expect(200) - .then(response => { - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["content-length"]).toBeTruthy() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end /v1/history/{_id}. Do a properly formatted /history call by GETting for an existing _id. ' + - 'It should respond 200 with a body that is of type Array.' + - 'It should strip the property "_id" from the response.', - done => { - req - .get('/v1/history/11111') - .set('Content-Type', 'application/json; charset=utf-8') - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) -// cubap kill bad test for 11111 expect(response.body[0]._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end HEAD req to /v1/history/{_id}.' + - 'It should respond 200 and the Content-Length response header should be set.', - done => { - req - .head('/v1/history/11111') - .expect(200) - .then(response => { - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["content-length"]).toBeTruthy() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end /v1/api/create. Do a properly formatted /create call by POSTing a JSON body. ' + - 'The Authorization header is set, it is an access token encoded with the bot. ' + - 'It should respond with a 201 with enough JSON in the response body to discern the "@id". ' + - 'The Location header in the response should be present and populated.', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - req - .post('/v1/api/create') - .send({ "RERUM Create Test": unique }) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(201) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["location"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.body["@id"]).toBeTruthy() - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end /v1/api/bulkCreate. Do a properly formatted call by POSTing a JSON Array body. ' + - 'The Authorization header is set, it is an access token encoded with the bot. ' + - 'It should respond with a 201 with JSON in the response body matching "@id"s. ' + - 'The Link header in the response should be present and populated.', - done => { - const unique = () => new Date(Date.now()).toISOString().replace("Z", "") - req - .post('/v1/api/bulkCreate') - .send([ - { "RERUM Bulk Create Test1": unique }, - { "RERUM Bulk Create Test2": unique }, - { "RERUM Bulk Create Test3": unique }, - { "RERUM Bulk Create Test4": unique }, - ]) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(201) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["location"]).toBeUndefined() - expect(response.headers["link"]).toBeTruthy() - expect(response.body[0]).toHaveProperty("@id") - expect(response.body[0]).toHaveProperty("__rerum") - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - } - ) - - it('End to end Slug header support verification. Do a properly formatted /create call by POSTing a JSON body. ' + - 'The Location header in the response should be present and have the SLUG id.', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - const slug = `1123rcgslu1123${unique}` - //It is slightly possible this thing already exists, there could have been an error. - //Let's be super cautious and remove it first, then move on. That way we don't have to manually fix it. - controller.remove(slug).then(r => { - req - .post('/v1/api/create') - .send({ "RERUM Slug Support Test": unique }) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .set('Slug', slug) - .expect(201) - .then(response => { - expect(response.headers["location"]).toBe(response.body["@id"]) - expect(response.body.__rerum.slug).toBe(slug) - controller.remove(slug).then(s => done()) - }) - .catch(err => done(err)) - }) - .catch(err => done(err)) - }) - - it('End to end /v1/api/update. Do a properly formatted /update call by PUTing an existing entity. '+ - 'The Authorization header is set, it is an access token encoded with the bot. '+ - 'It should respond with a 200 with enough JSON in the response body to discern the "@id". '+ - 'The Location header in the response should be present and populated and not equal the originating entity "@id".', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - req - .put('/v1/api/update') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "RERUM Update Test":unique}) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.headers["location"]).toBeTruthy() - expect(response.headers["location"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.body["@id"]).toBeTruthy() - expect(response.body["@id"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - }) - - it('End to end import functionality. Do a properly formatted /update call by PUTing an existing entity. '+ - 'If that entity has an existing id or @id property which is not from RERUM, then import it in. '+ - 'This will effectively create the object, and its __rerum.history.previous should point to the origin URI. '+ - 'The Authorization header is set, it is an access token encoded with the bot. '+ - 'It should respond with a 200 with enough JSON in the response body to discern the "@id". '+ - 'The Location header in the response should be present and populated and not equal the originating entity "@id" or "id".', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - req - .put('/v1/api/update') - .send({"id": "https://not.from.rerum/v1/api/aaaeaeaeee34345", "RERUM Import Test":unique}) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.headers["location"]).toBeTruthy() - expect(response.headers["location"]).not.toBe("https://not.from.rerum/v1/api/aaaeaeaeee34345") - expect(response.body["@id"]).toBeTruthy() - expect(response.body["@id"]).not.toBe("https://not.from.rerum/v1/api/aaaeaeaeee34345") - expect(response.body._id).toBeUndefined() - expect(response.body.id).toBeUndefined() - expect(response.body.__rerum.history.previous).toBe("https://not.from.rerum/v1/api/aaaeaeaeee34345") - done() - }) - .catch(err => done(err)) - }) - - it('End to end /v1/api/patch. Do a properly formatted /patch call by PATCHing an existing entity. '+ - 'The Authorization header is set, it is an access token encoded with the bot. '+ - 'It should respond with a 200 with enough JSON in the response body to discern the "@id". '+ - 'The Location header in the response should be present and populated and not equal the originating entity "@id".', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - req - .patch('/v1/api/patch') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "test_obj":unique}) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.body["@id"]).toBeTruthy() -// cubap kill bad test for 11111 expect(response.body["@id"]).not.toBe(process.env.RERUM_ID_PREFIX + "11111") -// cubap kill bad test for 11111 expect(response.body["test_obj"]).toBe(unique) - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - }) - - it('End to end /v1/api/set. Do a properly formatted /set call by PATCHing an existing entity. '+ - 'The Authorization header is set, it is an access token encoded with the bot. '+ - 'It should respond with a 200 with enough JSON in the response body to discern the "@id" and the property that was set. '+ - 'The Location header in the response should be present and populated and not equal the originating entity "@id".', - done => { - const unique = new Date(Date.now()).toISOString().replace("Z", "") - req - .patch('/v1/api/set') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "test_set":unique}) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.body["@id"]).toBeTruthy() - expect(response.body["@id"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.body["test_set"]).toBe(unique) - expect(response.body._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - }) - - it('End to end /v1/api/unset. Do a properly formatted /unset call by PATCHing an existing entity. '+ - 'The Authorization header is set, it is an access token encoded with the bot. '+ - 'It should respond with a 200 with enough JSON in the response body to discern the "@id" and the absence of the unset property. '+ - 'The Location header in the response should be present and populated and not equal the originating entity "@id".', - done => { - req - .patch('/v1/api/unset') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "test_obj":null}) - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.body["@id"]).toBeTruthy() -// cubap kill bad test for 11111 expect(response.body["@id"]).not.toBe(process.env.RERUM_ID_PREFIX + "11111") - expect(response.body.hasOwnProperty("test_obj")).toBe(false) - expect(response.body._id).toBeUndefined() - done() - }) - }) - - it('End to end /v1/api/delete. Do a properly formatted /delete call by DELETEing an existing object. '+ - 'It will need to create an object first, then delete that object, and so must complete a /create call first. '+ - 'It will check the response to /create is 201 and the response to /delete is 204.', done => { - req - .post("/v1/api/create/") - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .send({"testing_delete":"Delete Me"}) - .expect(201) - .then(response => { - /** - * We cannot delete the same object over and over again, so we need to create an object to delete. - * Performing the extra /create in front of this adds unneceesary complexity - it has nothing to do with delete. - * TODO optimize - */ - const idToDelete = response.body["@id"].replace(process.env.RERUM_ID_PREFIX, "") - req - .delete(`/v1/api/delete/${idToDelete}`) - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .expect(204) - .then(r => { - //To be really strict, we could get the object and make sure it has __deleted. - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - done() - }) - }) - }) - - it('End to end /v1/api/query. Do a properly formatted /query call by POSTing a JSON query object. ' + - 'It should respond with a 200 and an array, even if there were no matches. ' + - 'It should strip the property "_id" from the response.' + - 'We are querying for an object we know exists, so the length of the response should be more than 0.', - done => { - req - .post('/v1/api/query') - .send({ "_id": "11111" }) - .set('Content-Type', 'application/json; charset=utf-8') - .expect(200) - .then(response => { - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBeTruthy() - expect(response.body[0]._id).toBeUndefined() - done() - }) - .catch(err => done(err)) - }) - - /* - * Under consideration, but not implemented in the API. HEAD reqs can't have bodies. - it('End to end HEAD req to /v1/api/query. '+ - */ - // 'It should respond 200 and the Content-Length response header should be set.', - // function(done) { - // req - // .head('/v1/api/query') - // .send({"_id" : "11111"}) - // .set('Content-Type', 'application/json; charset=utf-8') - // .expect(200) - // .then(response => { - // expect(response.headers["content-length"]).toBeTruthy() - // done() - // }) - // .catch(err => done(err)) - // }) - - it('End to end /v1/api/release.'+ - 'It will need to create an object first, then release that object, and so must complete a /create call first. '+ - 'It will check the response to /create is 201 and the response to /release is 200.', - done => { - req - .post("/v1/api/create/") - .set('Content-Type', 'application/json; charset=utf-8') - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .send({"testing_release":"Delete Me"}) - .expect(201) - .then(response => { - /** - * We cannot release the same object over and over again, so we need to create an object to release. - * Performing the extra /create in front of this adds unneceesary complexity - it has nothing to do with release. - * The same goes for the the remove call afterwards. - */ - const idToRelease = response.body["@id"].replace(process.env.RERUM_ID_PREFIX, "") - const slug = `rcgslu${new Date(Date.now()).toISOString().replace("Z", "")}` - controller.remove(slug).then(r => { - req - .patch(`/v1/api/release/${idToRelease}`) - .set('Authorization', `Bearer ${process.env.BOT_TOKEN}`) - .set('Slug', slug) - .expect(200) - .then(response => { - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.body.__rerum.isReleased).toBeTruthy() - expect(response.body.__rerum.slug).toBe(slug) - controller.remove(slug).then(s => done()) - }) - .catch(err => done(err)) - }) - .catch(err => done(err)) - }) - }) - - it('should use `limit` and `skip` correctly at /query', - done => { - req - .post('/v1/api/query?limit=10&skip=2') - .send({ "@id": { $exists: true } }) - .set('Content-Type', 'application/json; charset=utf-8') - .expect(200) - .then(response => { - //The following commented out headers are not what they are expected to be. TODO investigate if it matters. - //expect(response.headers["connection"]).toBe("Keep-Alive) - //expect(response.headers["keep-alive"]).toBeTruthy() - //expect(response.headers["access-control-allow-methods"]).toBeTruthy() - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["access-control-allow-origin"]).toBe("*") - expect(response.headers["access-control-expose-headers"]).toBe("*") - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBeLessThanOrEqual(10) - done() - }) - .catch(err => done(err)) - }) - - }) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index 7d98b458..a5218939 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -14,12 +15,6 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// FIXME here we need to create something to delete in order to test this route. -routeTester.use("/create", [addAuth, controller.create]) - -// TODO test the POST delete as well -//routeTester.use("/delete", [addAuth, controller.delete]) - // Mount our own /delete route without auth that will use controller.delete routeTester.use("/delete/:_id", [addAuth, controller.deleteObj]) @@ -41,19 +36,45 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/delete' route functions", async () => { - // create step (primarily validates route wiring) - const createResponse = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ test: "item" }) - expect(createResponse.statusCode).toBe(201) - - // delete step uses findOne + replaceOne internally db.findOne.mockResolvedValueOnce(mockDoc) const deleteResponse = await request(routeTester).delete(`/delete/${MOCK_ID}`) - // deleteObj returns 204 No Content on success - expect(deleteResponse.statusCode).toBe(204) + assert.strictEqual(deleteResponse.statusCode, 204) +}) + +// The replacement document written by the controller must carry a __deleted shape +// with the deletor's agent, an ISO timestamp, and a snapshot of the original. +// A mutation that drops any of these would erase the soft-delete audit trail. +it("writes a __deleted audit shape (deletor, time, object snapshot) to the replacement document", async () => { + db.findOne.mockResolvedValueOnce(mockDoc) + let captured + db.replaceOne.mockImplementationOnce(async (filter, replacement) => { + captured = { filter, replacement } + return { modifiedCount: 1 } + }) + + const response = await request(routeTester).delete(`/delete/${MOCK_ID}`) + + assert.strictEqual(response.statusCode, 204) + assert.ok(captured, "db.replaceOne should have been called") + assert.deepStrictEqual(captured.filter, { _id: MOCK_ID }) + assert.ok(captured.replacement.__deleted, "replacement must include __deleted") + assert.strictEqual(captured.replacement.__deleted.deletor, MOCK_AGENT) + assert.match( + captured.replacement.__deleted.time, + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, + "__deleted.time should be an ISO-like timestamp" + ) + assert.deepStrictEqual( + captured.replacement.__deleted.object, + mockDoc, + "__deleted.object should preserve a snapshot of the original" + ) + assert.strictEqual(captured.replacement["@id"], mockDoc["@id"], "@id is preserved on the deleted record") }) diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index 42bab868..8e5850cf 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -8,7 +9,9 @@ import controller from '../../db-controller.js' const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /history route without auth that will use controller.history +// Mount /history for both GET (controller.history) and HEAD (controller.historyHeadRequest). +// `.head()` must be registered before `.use()` to win over the method-agnostic mount. +routeTester.head("/history/:_id", controller.historyHeadRequest) routeTester.use("/history/:_id", controller.history) const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" @@ -29,13 +32,37 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/history/:id' route functions", async () => { - // history: findOne returns the root object; getAllVersions calls db.find().toArray() → [] - // getAllAncestors on a root object returns [] → response body is [] db.findOne.mockResolvedValueOnce(mockDoc) const response = await request(routeTester).get(`/history/${MOCK_ID}`) - expect(response.statusCode).toBe(200) - expect(Array.isArray(response.body)).toBe(true) + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) +}) + +describe('HEAD /history/:id', () => { + it("returns 200 with Content-Length matching the GET body length", async () => { + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const getResp = await request(routeTester).get(`/history/${MOCK_ID}`) + const getLen = Number(getResp.headers['content-length']) + + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const headResp = await request(routeTester).head(`/history/${MOCK_ID}`) + + assert.strictEqual(headResp.statusCode, 200) + assert.ok(getLen > 0, 'GET must report a Content-Length') + assert.strictEqual(Number(headResp.headers['content-length']), getLen) + assert.ok(!headResp.body || Object.keys(headResp.body).length === 0) + }) + + it("returns 404 when the object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester).head(`/history/${MOCK_ID}`) + assert.strictEqual(response.statusCode, 404) + }) }) diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index d171d195..cb469cba 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -8,7 +9,10 @@ import controller from '../../db-controller.js' const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /id route without auth that will use controller.id +// Mount our own /id route without auth that will use controller.id (GET) and +// controller.idHeadRequest (HEAD). `.use()` is method-agnostic, so the explicit +// `.head()` entry must come first to intercept HEAD before the catch-all GET handler. +routeTester.head("/id/:_id", controller.idHeadRequest) routeTester.use("/id/:_id", controller.id) const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" @@ -30,18 +34,63 @@ const mockDoc = { } // Import db mock so we can configure per-test behaviour -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/id/:id' route functions", async () => { db.findOne.mockResolvedValueOnce(mockDoc) const response = await request(routeTester).get(`/id/${MOCK_ID}`) - expect(response.statusCode).toBe(200) - // idNegotiation strips _id; @id present (or id for LD contexts) - expect(response.body["@id"] ?? response.body.id).toBeTruthy() - expect(response.body._id).toBeUndefined() - expect(response.body.__rerum).toBeDefined() + + assert.strictEqual(response.statusCode, 200) + assert.ok(response.body["@id"] ?? response.body.id) + assert.strictEqual(response.body._id, undefined) + assert.ok(response.body.__rerum) }) -it.skip("Proper '@id-id' negotation on GET by URI.", async () => { - // TODO +describe('HEAD /id/:id', () => { + it("returns 200 with Content-Length matching the GET body length", async () => { + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const getResp = await request(routeTester).get(`/id/${MOCK_ID}`) + const getLen = Number(getResp.headers['content-length']) + + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const headResp = await request(routeTester).head(`/id/${MOCK_ID}`) + + assert.strictEqual(headResp.statusCode, 200) + assert.ok(getLen > 0, 'GET must report a Content-Length') + assert.strictEqual(Number(headResp.headers['content-length']), getLen) + // HEAD must not carry a body. + assert.ok(!headResp.body || Object.keys(headResp.body).length === 0) + }) + + it("returns 404 when the object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester).head(`/id/${MOCK_ID}`) + assert.strictEqual(response.statusCode, 404) + }) +}) + +describe('id route overwrite headers', () => { + it('includes the current overwrite version header for existing objects', async () => { + const overwritten = structuredClone(mockDoc) + overwritten.__rerum.isOverwritten = '2025-06-24T10:00:00' + db.findOne.mockResolvedValueOnce(overwritten) + + const response = await request(routeTester).get(`/id/${MOCK_ID}`) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.headers['current-overwritten-version'], '2025-06-24T10:00:00') + }) + + it('uses an empty overwrite version header for never-overwritten objects', async () => { + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + + const response = await request(routeTester).get(`/id/${MOCK_ID}`) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.headers['current-overwritten-version'], '') + }) }) diff --git a/routes/__tests__/idNegotiation.test.js b/routes/__tests__/idNegotiation.test.js deleted file mode 100644 index c9b5c33a..00000000 --- a/routes/__tests__/idNegotiation.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" -import controller from '../../db-controller.js' - -it("Functional '@id-id' negotiation on objects returned.", async () => { - let negotiate = { - "@context": "http://iiif.io/api/presentation/3/context.json", - "_id": "example", - "@id": `${process.env.RERUM_ID_PREFIX}example`, - "test": "item" - } - negotiate = controller.idNegotiation(negotiate) - expect(negotiate._id).toBeUndefined() - expect(negotiate["@id"]).toBeUndefined() - expect(negotiate.id).toBe(`${process.env.RERUM_ID_PREFIX}example`) - expect(negotiate.test).toBe("item") - - let nonegotiate = { - "@context":"http://example.org/context.json", - "_id": "example", - "@id": `${process.env.RERUM_ID_PREFIX}example`, - "id": "test_example", - "test":"item" - } - nonegotiate = controller.idNegotiation(nonegotiate) - expect(nonegotiate._id).toBeUndefined() - expect(nonegotiate["@id"]).toBe(`${process.env.RERUM_ID_PREFIX}example`) - expect(nonegotiate.id).toBe("test_example") - expect(nonegotiate.test).toBe("item") -}) diff --git a/routes/__tests__/overwrite-optimistic-locking.test.txt b/routes/__tests__/overwrite-optimistic-locking.test.txt deleted file mode 100644 index 91d4f771..00000000 --- a/routes/__tests__/overwrite-optimistic-locking.test.txt +++ /dev/null @@ -1,184 +0,0 @@ -import { jest } from '@jest/globals' -import express from 'express' -import request from 'supertest' - -// Create mock functions -const mockFindOne = jest.fn() -const mockReplaceOne = jest.fn() - -// Mock the database module -jest.mock('../../database/index.js', () => ({ - db: { - findOne: mockFindOne, - replaceOne: mockReplaceOne - } -})) - -// Import controller after mocking -import controller from '../../db-controller.js' - -// Helper to add auth to requests -const addAuth = (req, res, next) => { - req.user = {"http://store.rerum.io/agent": "test-user"} - next() -} - -// Create a test Express app -const routeTester = express() -routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) - -// Mount our routes -routeTester.use('/overwrite', [addAuth, controller.overwrite]) -routeTester.use('/id/:_id', controller.id) - -describe('Overwrite Optimistic Locking', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - test('should succeed when no version is specified (backwards compatibility)', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockFindOne.mockResolvedValue(mockObject) - mockReplaceOne.mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(routeTester) - .put('/overwrite') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) - - test('should succeed when correct version is provided', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockFindOne.mockResolvedValue(mockObject) - mockReplaceOne.mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(routeTester) - .put('/overwrite') - .set('If-Overwritten-Version', '2025-06-24T10:00:00') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) - - test('should fail with 409 when version mismatch occurs', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:30:00', // Different from expected - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockFindOne.mockResolvedValue(mockObject) - - const response = await request(routeTester) - .put('/overwrite') - .set('If-Overwritten-Version', '2025-06-24T10:00:00') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(409) - expect(response.body.message).toContain('Version conflict detected') - expect(response.body.currentVersion).toBe('2025-06-24T10:30:00') - }) - - test('should accept version via request body as fallback', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockFindOne.mockResolvedValue(mockObject) - mockReplaceOne.mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(routeTester) - .put('/overwrite') - .send({ - '@id': 'http://example.com/test-id', - '__expectedVersion': '2025-06-24T10:00:00', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) -}) - -describe('ID endpoint includes version header', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - test('should include Current-Overwritten-Version header in GET /id response', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00' - }, - data: 'some-data' - } - - mockFindOne.mockResolvedValue(mockObject) - - const response = await request(routeTester) - .get('/id/test-id') - - expect(response.status).toBe(200) - }) - - test('should include empty string for new objects', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '__rerum': { - isOverwritten: '' - }, - data: 'some-data' - } - - mockFindOne.mockResolvedValue(mockObject) - - const response = await request(routeTester) - .get('/id/test-id') - - expect(response.status).toBe(200) - }) -}) diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js new file mode 100644 index 00000000..7650378b --- /dev/null +++ b/routes/__tests__/overwrite.test.js @@ -0,0 +1,123 @@ +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' +import express from 'express' +import request from 'supertest' + +import controller from '../../db-controller.js' +import { db, resetMocks } from '../../database/index.js' + +const addAuth = (req, res, next) => { + req.user = { 'http://store.rerum.io/agent': 'test-user' } + next() +} + +const routeTester = express() +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) +routeTester.put('/overwrite', addAuth, controller.overwrite) + +const baseObject = { + _id: 'test-id', + '@id': 'http://example.com/test-id', + '@context': 'http://example.com/context', + __rerum: { + isOverwritten: '', + generatedBy: 'test-user', + history: { prime: 'root', previous: '', next: [] }, + releases: { previous: '', next: [], replaces: '' }, + isReleased: '' + }, + data: 'original-data' +} + +beforeEach(() => { + resetMocks() +}) + +describe('overwrite route', () => { + it('supports overwrite without an optimistic-lock version', async () => { + db.findOne.mockResolvedValueOnce(structuredClone(baseObject)) + + const response = await request(routeTester) + .put('/overwrite') + .set('Content-Type', 'application/json') + .send({ '@id': baseObject['@id'], data: 'updated-data' }) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.data, 'updated-data') + assert.ok(response.headers['current-overwritten-version']) + assert.strictEqual(response.body._id, undefined) + }) + + it('accepts the If-Overwritten-Version header when it matches the current version', async () => { + const originalObject = structuredClone(baseObject) + originalObject.__rerum.isOverwritten = '2025-06-24T10:00:00' + db.findOne.mockResolvedValueOnce(originalObject) + + const response = await request(routeTester) + .put('/overwrite') + .set('Content-Type', 'application/json') + .set('If-Overwritten-Version', '2025-06-24T10:00:00') + .send({ '@id': baseObject['@id'], data: 'updated-data' }) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.data, 'updated-data') + assert.ok(response.headers['current-overwritten-version']) + }) + + it('accepts the request body overwrite version as a fallback', async () => { + const originalObject = structuredClone(baseObject) + originalObject.__rerum.isOverwritten = '2025-06-24T10:00:00' + db.findOne.mockResolvedValueOnce(originalObject) + + const response = await request(routeTester) + .put('/overwrite') + .set('Content-Type', 'application/json') + .send({ + '@id': baseObject['@id'], + data: 'updated-data', + __rerum: { isOverwritten: '2025-06-24T10:00:00' } + }) + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.data, 'updated-data') + }) + + it('persists the same isOverwritten value it returns in the response header', async () => { + db.findOne.mockResolvedValueOnce(structuredClone(baseObject)) + let captured + db.replaceOne.mockImplementationOnce(async (filter, replacement) => { + captured = { filter, replacement } + return { modifiedCount: 1 } + }) + + const response = await request(routeTester) + .put('/overwrite') + .set('Content-Type', 'application/json') + .send({ '@id': baseObject['@id'], data: 'updated-data' }) + + assert.strictEqual(response.statusCode, 200) + const headerVersion = response.headers['current-overwritten-version'] + assert.ok(headerVersion, 'response must carry a Current-Overwritten-Version header') + assert.ok(captured, 'db.replaceOne should have been called') + assert.strictEqual( + captured.replacement.__rerum.isOverwritten, + headerVersion, + 'persisted __rerum.isOverwritten must match the value the response advertises' + ) + }) + + it('returns 409 when the optimistic-lock version mismatches', async () => { + const originalObject = structuredClone(baseObject) + originalObject.__rerum.isOverwritten = '2025-06-24T10:30:00' + db.findOne.mockResolvedValueOnce(originalObject) + + const response = await request(routeTester) + .put('/overwrite') + .set('Content-Type', 'application/json') + .set('If-Overwritten-Version', '2025-06-24T10:00:00') + .send({ '@id': baseObject['@id'], data: 'updated-data' }) + + assert.strictEqual(response.statusCode, 409) + assert.strictEqual(response.body.currentVersion.__rerum.isOverwritten, '2025-06-24T10:30:00') + }) +}) diff --git a/routes/__tests__/overwrite.test.txt b/routes/__tests__/overwrite.test.txt deleted file mode 100644 index 129d7ea0..00000000 --- a/routes/__tests__/overwrite.test.txt +++ /dev/null @@ -1,175 +0,0 @@ -import request from 'supertest' -import app from '../../app.js' -import { jest } from '@jest/globals' - -// Mock the database and auth modules -jest.mock('../../db-controller.js') -jest.mock('../../auth/index.js') - -describe('Overwrite Optimistic Locking', () => { - let mockDb - let mockAuth - - beforeEach(() => { - // Reset mocks - jest.clearAllMocks() - - mockDb = require('../../db-controller.js') - mockAuth = require('../../auth/index.js') - - // Mock auth to always pass - mockAuth.checkJwt = jest.fn((req, res, next) => { - req.user = { sub: 'test-user' } - next() - }) - }) - - test('should succeed when no version is specified (backwards compatibility)', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - mockDb.replaceOne = jest.fn().mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(app) - .put('/overwrite') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) - - test('should succeed when correct version is provided', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - mockDb.replaceOne = jest.fn().mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(app) - .put('/overwrite') - .set('If-Overwritten-Version', '2025-06-24T10:00:00') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) - - test('should fail with 409 when version mismatch occurs', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:30:00', // Different from expected - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - - const response = await request(app) - .put('/overwrite') - .set('If-Overwritten-Version', '2025-06-24T10:00:00') - .send({ - '@id': 'http://example.com/test-id', - data: 'updated-data' - }) - - expect(response.status).toBe(409) - expect(response.body.message).toContain('Version conflict detected') - expect(response.body.currentVersion).toBe('2025-06-24T10:30:00') - }) - - test('should accept version via request body as fallback', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '@context': 'http://example.com/context', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00', - generatedBy: 'test-user' - }, - data: 'original-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - mockDb.replaceOne = jest.fn().mockResolvedValue({ modifiedCount: 1 }) - - const response = await request(app) - .put('/overwrite') - .send({ - '@id': 'http://example.com/test-id', - '__expectedVersion': '2025-06-24T10:00:00', - data: 'updated-data' - }) - - expect(response.status).toBe(200) - }) -}) - -describe('ID endpoint includes version header', () => { - let mockDb - - beforeEach(() => { - jest.clearAllMocks() - mockDb = require('../../db-controller.js') - }) - - test('should include Current-Overwritten-Version header in GET /id response', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '__rerum': { - isOverwritten: '2025-06-24T10:00:00' - }, - data: 'some-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - - const response = await request(app) - .get('/id/test-id') - - expect(response.status).toBe(200) - }) - - test('should include empty string for new objects', async () => { - const mockObject = { - _id: 'test-id', - '@id': 'http://example.com/test-id', - '__rerum': { - isOverwritten: '' - }, - data: 'some-data' - } - - mockDb.findOne = jest.fn().mockResolvedValue(mockObject) - - const response = await request(app) - .get('/id/test-id') - - expect(response.status).toBe(200) - }) -}) diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index b0651235..994923eb 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -1,12 +1,11 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" -dotenv.config() +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' -// Here is the auth mock so we get a req.user and the controller can function without a NPE. +// Here is the auth mock so we get a req.user so controller.patchUpdate can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} next() @@ -38,18 +37,35 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/patch' route functions", async () => { - // patchUpdate: findOne → original (has "RERUM Update Test"), patch it, insertOne + replaceOne db.findOne.mockResolvedValueOnce(mockDoc) const response = await request(routeTester) .patch("/patch") .set("Content-Type", "application/json") .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, "RERUM Update Test": unique }) - expect(response.statusCode).toBe(200) + + assert.strictEqual(response.statusCode, 200) const returnedId = response.body["@id"] ?? response.body.id - expect(returnedId).toBeTruthy() - expect(response.headers["location"]).toBe(returnedId) - expect(response.body._id).toBeUndefined() + assert.ok(returnedId) + assert.strictEqual(response.headers["location"], returnedId) + assert.strictEqual(response.body._id, undefined) +}) + +// controllers/patchUpdate.js:41 returns 501 (not 404) when the @id is not in RERUM. +// The contract declares 501 for this operation; without this test, removing the 501 guard +// would silently break the documented behavior while leaving the contract test passing. +it("'/patch' returns 501 when the target object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester) + .patch("/patch") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, "RERUM Update Test": unique }) + + assert.strictEqual(response.statusCode, 501) }) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index acace143..8d47f8af 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -8,7 +9,10 @@ import controller from '../../db-controller.js' const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /query route without auth that will use controller.query +// Mount our own /query route without auth that will use controller.query (POST) +// and controller.queryHeadRequest (HEAD). Order matters: `.head()` must precede +// `.use()` so the method-agnostic catch-all does not steal HEAD requests. +routeTester.head("/query", controller.queryHeadRequest) routeTester.use("/query", controller.query) const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" @@ -29,27 +33,69 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/query' route functions", async () => { - // Override the find cursor for this test to return one result const queryCursor = { - limit: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - toArray: jest.fn().mockResolvedValue([mockDoc]) + limit() { + return this + }, + skip() { + return this + }, + async toArray() { + return [mockDoc] + } } db.find.mockReturnValueOnce(queryCursor) const response = await request(routeTester) .post("/query") .set("Content-Type", "application/json") .send({ test: "item" }) - expect(response.statusCode).toBe(200) - expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBeGreaterThan(0) - expect(response.body[0]["@id"]).toBeTruthy() - expect(response.body[0]._id).toBeUndefined() + + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) + assert.ok(response.body.length > 0) + assert.ok(response.body[0]["@id"]) + assert.strictEqual(response.body[0]._id, undefined) }) -it.skip("Proper '@id-id' negotation on objects returned from '/query'.", async () => { - // TODO +describe('HEAD /query', () => { + const buildCursor = (docs) => ({ + limit() { return this }, + skip() { return this }, + async toArray() { return docs } + }) + + // The Content-Length parity check only depends on what db.find returns — + // the body the controller would have produced is the same whether the query + // filter has 0 or N keys. supertest cannot send a JSON body on HEAD cleanly + // (superagent rejects the object), so the request body is omitted here. + + it("returns 200 with Content-Length matching the POST body length", async () => { + db.find.mockReturnValueOnce(buildCursor([mockDoc])) + const postResp = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + const postLen = Number(postResp.headers['content-length']) + + db.find.mockReturnValueOnce(buildCursor([mockDoc])) + const headResp = await request(routeTester).head("/query") + + assert.strictEqual(headResp.statusCode, 200) + assert.ok(postLen > 0, 'POST must report a Content-Length') + assert.strictEqual(Number(headResp.headers['content-length']), postLen) + assert.ok(!headResp.body || Object.keys(headResp.body).length === 0) + }) + + it("returns 404 when no matches are found", async () => { + db.find.mockReturnValueOnce(buildCursor([])) + const response = await request(routeTester).head("/query") + assert.strictEqual(response.statusCode, 404) + }) }) diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index d9664d85..d018c6da 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -1,11 +1,12 @@ -import { jest } from "@jest/globals" +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' -// Here is the auth mock so we get a req.user so controller.create can function without a NPE. +// Here is the auth mock so we get a req.user so controller.release can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} next() @@ -14,9 +15,6 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// FIXME here we need to create something to release in order to test this route. -routeTester.use("/create", [addAuth, controller.create]) - // Mount our own /release route without auth that will use controller.release routeTester.use("/release/:_id", [addAuth, controller.release]) const slug = `rcgslu${new Date(Date.now()).toISOString().replace("Z", "")}` @@ -39,35 +37,56 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' -it("'/release' route functions", async () => { - // create something to release - const createResponse = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ test: "item" }) - expect(createResponse.statusCode).toBe(201) +beforeEach(() => { + resetMocks() +}) - // release with slug: - // 1st findOne for slug uniqueness check -> null - // 2nd findOne to fetch object being released -> mockDoc +it("'/release' route functions", async () => { db.findOne .mockResolvedValueOnce(null) .mockResolvedValueOnce(mockDoc) const releaseResponse = await request(routeTester) - .post(`/release/${MOCK_ID}`) + .patch(`/release/${MOCK_ID}`) .set("Slug", slug) .set("Content-Type", "application/json") - expect(releaseResponse.statusCode).toBe(200) - expect(releaseResponse.body._id).toBeUndefined() - expect(releaseResponse.body.__rerum).toBeDefined() - expect(releaseResponse.body.__rerum.isReleased).toBeTruthy() + assert.strictEqual(releaseResponse.statusCode, 200) + assert.strictEqual(releaseResponse.body._id, undefined) + assert.ok(releaseResponse.body.__rerum) + assert.match( + releaseResponse.body.__rerum.isReleased, + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, + "__rerum.isReleased should be an ISO-like timestamp" + ) const returnedId = releaseResponse.body["@id"] ?? releaseResponse.body.id - expect(releaseResponse.headers["location"]).toBe(returnedId) + assert.strictEqual(releaseResponse.headers["location"], returnedId) +}) + +// controllers/release.js:47-52 returns 404 when the target object is not in RERUM. +// The contract declares 404; without this test, dropping the guard would leave the contract +// test passing while production silently regressed to 500. +it("'/release' returns 404 when the target object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + + const response = await request(routeTester) + .patch(`/release/${MOCK_ID}`) + .set("Content-Type", "application/json") + + assert.strictEqual(response.statusCode, 404) +}) + +// A slug conflict bubbles through generateSlugId (controllers/utils.js:106) as code 11000, +// which utils.createExpressError maps to 409. The contract declares 409 for this reason. +it("'/release' returns 409 when the requested Slug is already taken", async () => { + db.findOne.mockResolvedValueOnce({ _id: "taken-slug" }) + + const response = await request(routeTester) + .patch(`/release/${MOCK_ID}`) + .set("Slug", "taken-slug") + .set("Content-Type", "application/json") - // cleanup slug object via internal helper path - await controller.remove(slug) + assert.strictEqual(response.statusCode, 409) }) diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js new file mode 100644 index 00000000..e4522a83 --- /dev/null +++ b/routes/__tests__/route_wrappers.test.js @@ -0,0 +1,268 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import express from 'express' +import request from 'supertest' + +import patchSetRouter from '../patchSet.js' +import patchUnsetRouter from '../patchUnset.js' +import patchUpdateRouter from '../patchUpdate.js' +import clientRouter from '../client.js' +import createRouter from '../create.js' +import bulkCreateRouter from '../bulkCreate.js' +import bulkUpdateRouter from '../bulkUpdate.js' +import deleteRouter from '../delete.js' +import historyRouter from '../history.js' +import idRouter from '../id.js' +import overwriteRouter from '../overwrite.js' +import staticRouter from '../static.js' +import sinceRouter from '../since.js' +import updateRouter from '../putUpdate.js' +import indexRouter from '../index.js' +import searchRouter from '../search.js' +import queryRouter from '../query.js' +import releaseRouter from '../release.js' +import apiRoutesRouter from '../api-routes.js' +import gogFragmentsRouter from '../_gog_fragments_from_manuscript.js' +import gogGlossesRouter from '../_gog_glosses_from_manuscript.js' + +function getRoute(router, path) { + const routeLayer = router.stack.find(layer => layer.route?.path === path) + assert.ok(routeLayer, `Expected route for path '${path}'`) + return routeLayer.route +} + +function getMethodLayers(router, path, method) { + return getRoute(router, path).stack.filter(layer => layer.method === method) +} + +function createResponse() { + return { + headers: {}, + statusCode: undefined, + statusMessage: undefined, + body: undefined, + ended: false, + set(name, value) { + this.headers[name] = value + return this + }, + status(code) { + this.statusCode = code + return this + }, + send(value) { + this.body = value + this.ended = true + return this + }, + sendFile(filePath, options) { + this.body = { filePath, options } + this.ended = true + return this + }, + end() { + this.ended = true + return this + } + } +} + +function invokeLayer(layer, req = {}, res = createResponse()) { + const nextCalls = [] + layer.handle(req, res, arg => nextCalls.push(arg)) + return { res, nextCalls } +} + +function getOverrideLayer(router) { + const postLayers = getMethodLayers(router, '/', 'post') + const overrideLayer = postLayers.at(-2) + assert.ok(overrideLayer, 'Expected override middleware layer') + return overrideLayer +} + +function assertInvalidOverride(router) { + const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { + header() { + return undefined + } + }) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +function assertValidOverride(router) { + const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { + header(name) { + return name === 'X-HTTP-Method-Override' ? 'PATCH' : undefined + } + }) + + assert.strictEqual(res.statusCode, undefined) + assert.strictEqual(res.ended, false) + assert.strictEqual(nextCalls.length, 1) + assert.strictEqual(nextCalls[0], undefined) +} + +function assertUnsupportedMethodOnPath(router, path) { + const fallbackLayer = getRoute(router, path).stack.at(-1) + assert.ok(fallbackLayer, `Expected fallback .all() layer for '${path}'`) + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +describe('patch route wrappers', () => { + it('rejects POST /set requests without PATCH override', () => { + assertInvalidOverride(patchSetRouter) + }) + + it('passes POST /set requests with PATCH override to the next handler', () => { + assertValidOverride(patchSetRouter) + }) + + it('rejects POST /unset requests without PATCH override', () => { + assertInvalidOverride(patchUnsetRouter) + }) + + it('passes POST /unset requests with PATCH override to the next handler', () => { + assertValidOverride(patchUnsetRouter) + }) + + it('rejects POST /patch requests without PATCH override', () => { + assertInvalidOverride(patchUpdateRouter) + }) + + it('passes POST /patch requests with PATCH override to the next handler', () => { + assertValidOverride(patchUpdateRouter) + }) +}) + +describe('client route wrappers', () => { + it('builds the Auth0 registration URL with the expected query params', async () => { + const audience = process.env.AUDIENCE + const clientId = process.env.CLIENT_ID + const rerumPrefix = process.env.RERUM_PREFIX + + process.env.AUDIENCE = 'https://example.org/audience' + process.env.CLIENT_ID = 'client-123' + process.env.RERUM_PREFIX = 'https://example.org/rerum' + + const app = express() + app.use('/client', clientRouter) + + try { + const response = await request(app).get('/client/register') + + assert.strictEqual(response.statusCode, 200) + const registrationUrl = new URL(response.text) + assert.strictEqual(registrationUrl.origin + registrationUrl.pathname, 'https://cubap.auth0.com/authorize') + assert.strictEqual(registrationUrl.searchParams.get('audience'), 'https://example.org/audience') + assert.strictEqual(registrationUrl.searchParams.get('scope'), 'offline_access') + assert.strictEqual(registrationUrl.searchParams.get('response_type'), 'code') + assert.strictEqual(registrationUrl.searchParams.get('client_id'), 'client-123') + assert.strictEqual(registrationUrl.searchParams.get('redirect_uri'), 'https://example.org/rerum') + assert.strictEqual(registrationUrl.searchParams.get('state'), 'register') + } + finally { + process.env.AUDIENCE = audience + process.env.CLIENT_ID = clientId + process.env.RERUM_PREFIX = rerumPrefix + } + }) + + it('returns a plain-text success response from the verified token handler', () => { + const verifyHandler = getMethodLayers(clientRouter, '/verify', 'get').at(-1) + assert.ok(verifyHandler, 'Expected verify handler after auth middleware') + + const { res, nextCalls } = invokeLayer(verifyHandler, { + user: { + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/test-agent' + } + }) + + assert.strictEqual(res.headers['Content-Type'], 'text/plain') + assert.strictEqual(res.statusCode, 200) + assert.deepStrictEqual(nextCalls, []) + }) +}) + +describe('unsupported-method 405 fallbacks', () => { + const cases = [ + { label: '/bulkCreate', router: bulkCreateRouter, path: '/' }, + { label: '/bulkUpdate', router: bulkUpdateRouter, path: '/' }, + { label: '/create', router: createRouter, path: '/' }, + { label: '/update', router: updateRouter, path: '/' }, + { label: '/overwrite', router: overwriteRouter, path: '/' }, + { label: '/search', router: searchRouter, path: '/' }, + { label: '/search/phrase', router: searchRouter, path: '/phrase' }, + { label: '/query', router: queryRouter, path: '/' }, + { label: '/set', router: patchSetRouter, path: '/' }, + { label: '/unset', router: patchUnsetRouter, path: '/' }, + { label: '/patch', router: patchUpdateRouter,path: '/' }, + { label: '/release/:_id', router: releaseRouter, path: '/:_id' }, + { label: '/delete/:_id', router: deleteRouter, path: '/:_id' }, + { label: '/history/:_id', router: historyRouter, path: '/:_id' }, + { label: '/id/:_id', router: idRouter, path: '/:_id' }, + { label: '/since/:_id', router: sinceRouter, path: '/:_id' }, + { label: '/_gog_fragments_from_manuscript', router: gogFragmentsRouter, path: '/' }, + { label: '/_gog_glosses_from_manuscript', router: gogGlossesRouter, path: '/' } + ] + + for (const { label, router, path } of cases) { + it(`rejects unsupported methods for ${label}`, () => { + assertUnsupportedMethodOnPath(router, path) + }) + } +}) + +describe('api routes discovery', () => { + it('directly serves the welcome page from the static route handler', () => { + const handler = getMethodLayers(staticRouter, '/', 'get').at(-1) + assert.ok(handler, 'Expected static router GET handler') + + const { res, nextCalls } = invokeLayer(handler) + + assert.deepStrictEqual(res.body, { filePath: 'index.html', options: undefined }) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) + }) + + it('serves the public index page from GET /', async () => { + const app = express() + app.use(indexRouter) + + const response = await request(app).get('/') + + assert.strictEqual(response.statusCode, 200) + assert.match(response.headers['content-type'], /^text\/html/) + }) + + it('returns the advertised endpoint map for GET /api', async () => { + const app = express() + app.use(apiRoutesRouter) + + const response = await request(app).get('/api') + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.message, 'Welcome to v1 in nodeJS! Below are the available endpoints, used like /v1/api/{endpoint}') + assert.deepStrictEqual(response.body.endpoints, { + '/create': 'POST - Create a new object.', + '/bulkCreate': 'POST - Create multiple new objects in one request.', + '/update': 'PUT - Update the body an existing object.', + '/bulkUpdate': 'PUT - Update multiple existing objects in one request.', + '/patch': 'PATCH - Update the properties of an existing object.', + '/set': 'PATCH - Update the body an existing object by adding a new property.', + '/unset': 'PATCH - Update the body an existing object by removing an existing property.', + '/delete': 'DELETE - Mark an object as deleted.', + '/query': 'POST - Supply a JSON object to match on, and query the db for an array of matches.', + '/search': 'POST - Full-text search across stored objects.', + '/release': 'PATCH - Lock a JSON object from changes and guarantee the content and URI.', + '/overwrite': 'PUT - Update a specific document in place, overwriting the existing body.' + }) + }) +}) diff --git a/routes/__tests__/search.test.js b/routes/__tests__/search.test.js new file mode 100644 index 00000000..54f1dbbb --- /dev/null +++ b/routes/__tests__/search.test.js @@ -0,0 +1,104 @@ +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' + +import express from 'express' +import request from 'supertest' + +import controller from '../../db-controller.js' +import rest from '../../rest.js' +import { db, resetMocks } from '../../database/index.js' + +const routeTester = express() +routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) +routeTester.use(express.text()) +routeTester.post('/search', controller.searchAsWords) +routeTester.post('/search/phrase', controller.searchAsPhrase) +routeTester.use(rest.messenger) + +beforeEach(() => { + resetMocks() +}) + +function mockAggregateResults(docs) { + // db.aggregate is called twice (presi3 + presi2 indexes) in parallel; mockReturnValue + // applies to every call until the next reset. + db.aggregate.mockReturnValue({ + toArray: () => Promise.resolve(docs) + }) +} + +describe('search controllers', () => { + it("searchAsWords returns 400 when the body is empty", async () => { + const response = await request(routeTester) + .post('/search') + .set('Content-Type', 'text/plain') + .send('') + assert.strictEqual(response.statusCode, 400) + }) + + it("searchAsWords returns 200 and an array of results for a text body", async () => { + const doc = { + _id: 'doc-1', + '@id': 'https://store.rerum.io/v1/id/doc-1', + text: 'matching content' + } + mockAggregateResults([doc]) + + const response = await request(routeTester) + .post('/search') + .set('Content-Type', 'text/plain') + .send('matching') + + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) + assert.ok(response.body.length > 0, 'response array should contain results') + assert.strictEqual(response.body[0]['@id'], doc['@id']) + }) + + it("searchAsPhrase returns 400 when the body is empty", async () => { + const response = await request(routeTester) + .post('/search/phrase') + .set('Content-Type', 'text/plain') + .send('') + assert.strictEqual(response.statusCode, 400) + }) + + it("searchAsPhrase returns 200 and an array of results for a text body", async () => { + const doc = { + _id: 'doc-2', + '@id': 'https://store.rerum.io/v1/id/doc-2', + text: 'phrase content' + } + mockAggregateResults([doc]) + + const response = await request(routeTester) + .post('/search/phrase') + .set('Content-Type', 'text/plain') + .send('exact phrase') + + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) + assert.ok(response.body.length > 0) + assert.strictEqual(response.body[0]['@id'], doc['@id']) + }) + + // The two parallel db.aggregate calls (presi3 + presi2) can return overlapping documents. + // mergeSearchResults must dedupe by _id; a regression that drops the dedupe would return + // duplicates here. + it("searchAsWords dedupes when both indexes return the same document", async () => { + const doc = { + _id: 'shared-doc', + '@id': 'https://store.rerum.io/v1/id/shared-doc', + text: 'shared' + } + mockAggregateResults([doc]) + + const response = await request(routeTester) + .post('/search') + .set('Content-Type', 'text/plain') + .send('shared') + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.length, 1, 'duplicate _id across indexes should be deduped') + }) +}) diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 2276c149..175d93e5 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -1,13 +1,12 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" -dotenv.config() +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' -// Here is the auth mock so we get a req.user and the controller can function without a NPE. +// Here is the auth mock so we get a req.user so controller.patchSet can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} next() @@ -16,7 +15,7 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /create route without auth that will use controller.create +// Mount our own /set route without auth that will use controller.patchSet routeTester.use("/set", [addAuth, controller.patchSet]) const unique = new Date(Date.now()).toISOString().replace("Z", "") @@ -38,7 +37,11 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/set' route functions", async () => { db.findOne.mockResolvedValueOnce(mockDoc) @@ -46,9 +49,23 @@ it("'/set' route functions", async () => { .patch("/set") .set("Content-Type", "application/json") .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_set: unique }) - expect(response.statusCode).toBe(200) - expect(response.body["test_set"]).toBe(unique) - expect(response.body._id).toBeUndefined() + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body["test_set"], unique) + assert.strictEqual(response.body._id, undefined) const returnedId = response.body["@id"] ?? response.body.id - expect(response.headers["location"]).toBe(returnedId) + assert.strictEqual(response.headers["location"], returnedId) +}) + +// controllers/patchSet.js:43 returns 501 (not 404) when the @id is not in RERUM. +// The contract declares 501 for this operation; without this test, removing the 501 guard +// would silently break the documented behavior while leaving the contract test passing. +it("'/set' returns 501 when the target object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester) + .patch("/set") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_set: unique }) + + assert.strictEqual(response.statusCode, 501) }) diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index 12d433f4..28e02e5a 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.test.js @@ -1,4 +1,5 @@ -import { jest } from "@jest/globals" +import { beforeEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -8,7 +9,9 @@ import controller from '../../db-controller.js' const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /create route without auth that will use controller.history +// Mount /since for both GET (controller.since) and HEAD (controller.sinceHeadRequest). +// `.head()` must be registered before `.use()` to win over the method-agnostic mount. +routeTester.head("/since/:_id", controller.sinceHeadRequest) routeTester.use("/since/:_id", controller.since) const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" @@ -29,13 +32,37 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/since/:id' route functions", async () => { - // since: findOne returns the root object; getAllVersions calls db.find().toArray() → [] - // getAllDescendants on object with next:[] returns [] → response body is [] db.findOne.mockResolvedValueOnce(mockDoc) const response = await request(routeTester).get(`/since/${MOCK_ID}`) - expect(response.statusCode).toBe(200) - expect(Array.isArray(response.body)).toBe(true) + assert.strictEqual(response.statusCode, 200) + assert.ok(Array.isArray(response.body)) +}) + +describe('HEAD /since/:id', () => { + it("returns 200 with Content-Length matching the GET body length", async () => { + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const getResp = await request(routeTester).get(`/since/${MOCK_ID}`) + const getLen = Number(getResp.headers['content-length']) + + db.findOne.mockResolvedValueOnce(structuredClone(mockDoc)) + const headResp = await request(routeTester).head(`/since/${MOCK_ID}`) + + assert.strictEqual(headResp.statusCode, 200) + assert.ok(getLen > 0, 'GET must report a Content-Length') + assert.strictEqual(Number(headResp.headers['content-length']), getLen) + assert.ok(!headResp.body || Object.keys(headResp.body).length === 0) + }) + + it("returns 404 when the object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester).head(`/since/${MOCK_ID}`) + assert.strictEqual(response.statusCode, 404) + }) }) diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index fe7baa56..408c7009 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -1,13 +1,12 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" -dotenv.config() +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' -// Here is the auth mock so we get a req.user so controller.create can function without a NPE. +// Here is the auth mock so we get a req.user so controller.patchUnset can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} next() @@ -16,7 +15,7 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /create route without auth that will use controller.create +// Mount our own /unset route without auth that will use controller.patchUnset routeTester.use("/unset", [addAuth, controller.patchUnset]) const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" @@ -38,7 +37,11 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/unset' route functions", async () => { db.findOne.mockResolvedValueOnce(mockDoc) @@ -46,11 +49,24 @@ it("'/unset' route functions", async () => { .patch("/unset") .set("Content-Type", "application/json") .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_obj: null }) - expect(response.statusCode).toBe(200) - expect(response.body["test_obj"]).toBeUndefined() - expect(response.body._id).toBeUndefined() + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body["test_obj"], undefined) + assert.strictEqual(response.body._id, undefined) const returnedId = response.body["@id"] ?? response.body.id - expect(response.headers["location"]).toBe(returnedId) + assert.strictEqual(response.headers["location"], returnedId) }) +// controllers/patchUnset.js:42 returns 501 (not 404) when the @id is not in RERUM. +// The contract declares 501 for this operation; without this test, removing the 501 guard +// would silently break the documented behavior while leaving the contract test passing. +it("'/unset' returns 501 when the target object is not in RERUM", async () => { + db.findOne.mockResolvedValueOnce(null) + const response = await request(routeTester) + .patch("/unset") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_obj: null }) + + assert.strictEqual(response.statusCode, 501) +}) diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index ca0524a3..7999d38d 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -1,12 +1,11 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" -dotenv.config() +import { beforeEach, it } from 'node:test' +import assert from 'node:assert/strict' // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" import request from "supertest" import controller from '../../db-controller.js' -// Here is the auth mock so we get a req.user so controller.create can function without a NPE. +// Here is the auth mock so we get a req.user so controller.putUpdate can function without a NPE. const addAuth = (req, res, next) => { req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"} next() @@ -15,7 +14,7 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// Mount our own /create route without auth that will use controller.create +// Mount our own /update route without auth that will use controller.putUpdate routeTester.use("/update", [addAuth, controller.putUpdate]) const unique = new Date(Date.now()).toISOString().replace("Z", "") @@ -38,20 +37,24 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) it("'/update' route functions", async () => { - // putUpdate: findOne → original, insertOne → new version, replaceOne → update original's next db.findOne.mockResolvedValueOnce(mockDoc) const response = await request(routeTester) .put("/update") .set("Content-Type", "application/json") .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, "RERUM Update Test": unique }) - expect(response.statusCode).toBe(200) + + assert.strictEqual(response.statusCode, 200) const returnedId = response.body["@id"] ?? response.body.id - expect(returnedId).toBeTruthy() - expect(response.headers["location"]).toBe(returnedId) - expect(response.headers["location"]).not.toBe(`${MOCK_PREFIX}${MOCK_ORIG_ID}`) - expect(response.body._id).toBeUndefined() - expect(response.body["RERUM Update Test"]).toBe(unique) + assert.ok(returnedId) + assert.strictEqual(response.headers["location"], returnedId) + assert.notStrictEqual(response.headers["location"], `${MOCK_PREFIX}${MOCK_ORIG_ID}`) + assert.strictEqual(response.body._id, undefined) + assert.strictEqual(response.body["RERUM Update Test"], unique) }) diff --git a/routes/_gog_fragments_from_manuscript.js b/routes/_gog_fragments_from_manuscript.js index d1f30193..109bd9dd 100644 --- a/routes/_gog_fragments_from_manuscript.js +++ b/routes/_gog_fragments_from_manuscript.js @@ -8,8 +8,7 @@ router.route('/') .post(auth.checkJwt, controller._gog_fragments_from_manuscript) .all((req, res, next) => { res.statusMessage = 'Improper request method. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/_gog_glosses_from_manuscript.js b/routes/_gog_glosses_from_manuscript.js index e5c57659..4d80970a 100644 --- a/routes/_gog_glosses_from_manuscript.js +++ b/routes/_gog_glosses_from_manuscript.js @@ -8,8 +8,7 @@ router.route('/') .post(auth.checkJwt, controller._gog_glosses_from_manuscript) .all((req, res, next) => { res.statusMessage = 'Improper request method. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router \ No newline at end of file diff --git a/routes/api-routes.js b/routes/api-routes.js index abee718e..d92af398 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -66,14 +66,17 @@ router.get('/api', (req, res) => { message: 'Welcome to v1 in nodeJS! Below are the available endpoints, used like /v1/api/{endpoint}', endpoints: { "/create": "POST - Create a new object.", + "/bulkCreate": "POST - Create multiple new objects in one request.", "/update": "PUT - Update the body an existing object.", + "/bulkUpdate": "PUT - Update multiple existing objects in one request.", "/patch": "PATCH - Update the properties of an existing object.", "/set": "PATCH - Update the body an existing object by adding a new property.", "/unset": "PATCH - Update the body an existing object by removing an existing property.", "/delete": "DELETE - Mark an object as deleted.", "/query": "POST - Supply a JSON object to match on, and query the db for an array of matches.", - "/release": "POST - Lock a JSON object from changes and guarantee the content and URI.", - "/overwrite": "POST - Update a specific document in place, overwriting the existing body." + "/search": "POST - Full-text search across stored objects.", + "/release": "PATCH - Lock a JSON object from changes and guarantee the content and URI.", + "/overwrite": "PUT - Update a specific document in place, overwriting the existing body." } }) }) diff --git a/routes/bulkCreate.js b/routes/bulkCreate.js index b4cb49f4..3b5286ae 100644 --- a/routes/bulkCreate.js +++ b/routes/bulkCreate.js @@ -11,8 +11,7 @@ router.route('/') .post(auth.checkJwt, rest.verifyJsonContentType, controller.bulkCreate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/bulkUpdate.js b/routes/bulkUpdate.js index 293cd113..c22dcfb8 100644 --- a/routes/bulkUpdate.js +++ b/routes/bulkUpdate.js @@ -11,8 +11,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.bulkUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use PUT.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/create.js b/routes/create.js index e015d129..9a879e58 100644 --- a/routes/create.js +++ b/routes/create.js @@ -10,8 +10,7 @@ router.route('/') .post(auth.checkJwt, rest.verifyJsonContentType, controller.create) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/delete.js b/routes/delete.js index 5254740e..f2f63565 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -8,8 +8,7 @@ router.route('/:_id') .delete(auth.checkJwt, deleteObj) .all((req, res, next) => { res.statusMessage = 'Improper request method for deleting, please use DELETE.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/history.js b/routes/history.js index 06470da0..9abd6dd9 100644 --- a/routes/history.js +++ b/routes/history.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.historyHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/id.js b/routes/id.js index 3c2e8988..0e3d8d70 100644 --- a/routes/id.js +++ b/routes/id.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.idHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/overwrite.js b/routes/overwrite.js index edb8ed06..657a4232 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -10,8 +10,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.overwrite) .all((req, res, next) => { res.statusMessage = 'Improper request method for overwriting, please use PUT to overwrite this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchSet.js b/routes/patchSet.js index 56db7e66..c5c41bb8 100644 --- a/routes/patchSet.js +++ b/routes/patchSet.js @@ -5,21 +5,14 @@ import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to add new keys to this object.') + router.route('/') .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchSet) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to add new keys to this object.' - res.status(405) - next(res) - return - } - controller.patchSet(req, res, next) - }) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchSet) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PATCH to add new keys to this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchUnset.js b/routes/patchUnset.js index 6def00ae..da9ced82 100644 --- a/routes/patchUnset.js +++ b/routes/patchUnset.js @@ -5,21 +5,14 @@ import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to remove keys from this object.') + router.route('/') .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUnset) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to remove keys from this object.' - res.status(405) - next(res) - return - } - controller.patchUnset(req, res, next) - }) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchUnset) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PATCH to remove keys from this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchUpdate.js b/routes/patchUpdate.js index 7c4918f1..2c496bf2 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -6,21 +6,14 @@ import controller from '../db-controller.js' import rest from '../rest.js' import auth from '../auth/index.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to alter existing keys on this object.') + router.route('/') - .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUpdate) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to alter the existing keys this object.' - res.status(405) - next(res) - return - } - controller.patchUpdate(req, res, next) - }) - .all((req, res, next) => { - res.statusMessage = 'Improper request method for updating, please use PATCH to alter existing keys on this object.' - res.status(405) - next(res) - }) + .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUpdate) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchUpdate) + .all((req, res, next) => { + res.statusMessage = 'Improper request method for updating, please use PATCH to alter existing keys on this object.' + res.status(405).end() + }) export default router diff --git a/routes/putUpdate.js b/routes/putUpdate.js index 88cc93f4..cca93137 100644 --- a/routes/putUpdate.js +++ b/routes/putUpdate.js @@ -10,8 +10,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.putUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PUT to update this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/query.js b/routes/query.js index 5be0c5ba..b9882b82 100644 --- a/routes/query.js +++ b/routes/query.js @@ -9,8 +9,7 @@ router.route('/') .head(controller.queryHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method for requesting objects with matching properties. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/release.js b/routes/release.js index 870c0d88..3a90ac15 100644 --- a/routes/release.js +++ b/routes/release.js @@ -9,8 +9,7 @@ router.route('/:_id') .patch(auth.checkJwt, controller.release) .all((req, res, next) => { res.statusMessage = 'Improper request method for releasing, please use PATCH to release this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/search.js b/routes/search.js index 9b9948ca..e9c3bd51 100644 --- a/routes/search.js +++ b/routes/search.js @@ -7,19 +7,17 @@ router.route('/') .post(rest.verifyEitherContentType, controller.searchAsWords) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) router.route('/phrase') .post(rest.verifyEitherContentType, controller.searchAsPhrase) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) // Note that there are more search functions available in the controller, such as controller.searchFuzzily // They can be used through additional endpoints here when we are ready. -export default router \ No newline at end of file +export default router diff --git a/routes/since.js b/routes/since.js index e0f7a841..c328fd5b 100644 --- a/routes/since.js +++ b/routes/since.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.sinceHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/test/bootstrap.js b/test/bootstrap.js new file mode 100644 index 00000000..dc78ea63 --- /dev/null +++ b/test/bootstrap.js @@ -0,0 +1,4 @@ +import { register } from 'node:module' +import { pathToFileURL } from 'node:url' + +register('./test/loader.js', pathToFileURL('./')) diff --git a/test/coverage-inventory.json b/test/coverage-inventory.json new file mode 100644 index 00000000..bb7b72ea --- /dev/null +++ b/test/coverage-inventory.json @@ -0,0 +1,102 @@ +{ + "runner": { + "test": "node:test", + "coverage": "c8", + "bootstrap": "test/bootstrap.js", + "loader": "test/loader.js" + }, + "validation": { + "suite": { + "tests": 108, + "suites": 15, + "pass": 105, + "fail": 0, + "skip": 3 + }, + "coverage": { + "statements": 98.77, + "branches": 100, + "functions": 100, + "lines": 98.77 + } + }, + "rootSuites": [ + "__tests__/core_provider_contract.test.js", + "__tests__/openapi_sync_artifacts.test.js", + "__tests__/provider_sync_artifacts.test.js", + "__tests__/routes_mounted.test.js" + ], + "nativeSuites": { + "added": [ + "auth/__tests__/token.test.js", + "routes/__tests__/overwrite.test.js", + "routes/__tests__/route_wrappers.test.js" + ], + "migrated": [ + "routes/__tests__/bulkCreate.test.js", + "routes/__tests__/bulkUpdate.test.js", + "routes/__tests__/contentType.test.js", + "routes/__tests__/create.test.js", + "routes/__tests__/delete.test.js", + "routes/__tests__/history.test.js", + "routes/__tests__/id.test.js", + "routes/__tests__/idNegotiation.test.js", + "routes/__tests__/patch.test.js", + "routes/__tests__/query.test.js", + "routes/__tests__/release.test.js", + "routes/__tests__/set.test.js", + "routes/__tests__/since.test.js", + "routes/__tests__/unset.test.js", + "routes/__tests__/update.test.js" + ], + "removed": [ + "auth/__tests__/token.test.txt", + "routes/__tests__/client.test.txt", + "routes/__tests__/compatability.test.txt", + "routes/__tests__/crud_routes_function.txt", + "routes/__tests__/overwrite-optimistic-locking.test.txt", + "routes/__tests__/overwrite.test.txt" + ] + }, + "harnessCleanup": { + "removed": [ + "jest.config.js", + "test/jest-globals.js", + "test/register-globals.js", + "test/setup.js", + "test/tiers.js", + "test/utils.js", + "test/mocks/db.js" + ], + "kept": [ + "database/__mocks__/index.js", + "test/bootstrap.js", + "test/loader.js" + ] + }, + "coverageScope": { + "included": [ + "controllers/**/*.js", + "routes/**/*.js", + "auth/**/*.js", + "db-controller.js", + "rest.js", + "utils.js" + ], + "excluded": [ + "**/__tests__/**" + ] + }, + "notableCoverageGaps": [ + { + "file": "routes/_gog_fragments_from_manuscript.js", + "lines": "10-12", + "statements": 80 + }, + { + "file": "routes/_gog_glosses_from_manuscript.js", + "lines": "10-12", + "statements": 80 + } + ] +} diff --git a/test/loader.js b/test/loader.js new file mode 100644 index 00000000..7295d886 --- /dev/null +++ b/test/loader.js @@ -0,0 +1,23 @@ +/** + * Test loader for node:test framework. + * Redirects the production database module to a test double. + * + * @module test/loader + */ + +const rootUrl = new URL('../', import.meta.url) +const mockDatabaseUrl = new URL('../database/__mocks__/index.js', import.meta.url) + +export async function resolve(specifier, context, nextResolve) { + if (specifier.endsWith('/database/index.js') || specifier === './database/index.js') { + const resolved = new URL(specifier, context.parentURL ?? rootUrl) + if (resolved.pathname.endsWith('/database/index.js')) { + return { + shortCircuit: true, + url: mockDatabaseUrl.href + } + } + } + + return nextResolve(specifier, context) +}