From 581481397a78ed32e4c75c5d5d515dd797bd771c Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 13 May 2026 15:49:20 -0500 Subject: [PATCH 01/28] Migrate tests to node:test and update CI Replace Jest-based test setup with native node:test/assert and update CI. Tests were refactored to use node:test and node:assert (many __tests__ updated), new auth token unit tests added, and routes_mounted.test.js reworked to a data-driven style. The database mock was rewritten to remove Jest-specific APIs and provide a lightweight mock/registry with resetMocks; jest.config.js was removed. GitHub Actions workflows now separate install, test (npm run test:ci) and coverage (npm run coverage:ci) steps. package.json dev tooling and test scripts were adjusted accordingly, and test bootstrap/loader/coverage artifacts were added while placeholder .txt test files were removed. --- .github/workflows/cd_dev.yaml | 10 +- .github/workflows/cd_prod.yaml | 10 +- __tests__/core_provider_contract.test.js | 78 +- __tests__/openapi_sync_artifacts.test.js | 21 +- __tests__/provider_sync_artifacts.test.js | 4 +- __tests__/routes_mounted.test.js | 252 +- app.js | 2 - auth/__tests__/token.test.js | 130 + auth/__tests__/token.test.txt | 56 - auth/index.js | 2 - bin/rerum_v1.js | 2 - database/__mocks__/index.js | 118 +- database/index.js | 2 - jest.config.js | 222 - package-lock.json | 4990 +++-------------- package.json | 17 +- routes/__tests__/bulkCreate.test.js | 26 +- routes/__tests__/bulkUpdate.test.js | 48 +- routes/__tests__/client.test.txt | 1 - routes/__tests__/compatability.test.txt | 1 - routes/__tests__/contentType.test.js | 319 +- routes/__tests__/create.test.js | 28 +- routes/__tests__/crud_routes_function.txt | 584 -- routes/__tests__/delete.test.js | 16 +- routes/__tests__/history.test.js | 15 +- routes/__tests__/id.test.js | 19 +- routes/__tests__/idNegotiation.test.js | 20 +- .../overwrite-optimistic-locking.test.txt | 184 - routes/__tests__/overwrite.test.js | 122 + routes/__tests__/overwrite.test.txt | 175 - routes/__tests__/patch.test.js | 21 +- routes/__tests__/query.test.js | 33 +- routes/__tests__/release.test.js | 26 +- routes/__tests__/set.test.js | 20 +- routes/__tests__/since.test.js | 15 +- routes/__tests__/unset.test.js | 20 +- routes/__tests__/update.test.js | 25 +- test/bootstrap.js | 4 + test/coverage-inventory.json | 107 + test/loader.js | 23 + 40 files changed, 1804 insertions(+), 5964 deletions(-) create mode 100644 auth/__tests__/token.test.js delete mode 100644 auth/__tests__/token.test.txt delete mode 100644 jest.config.js delete mode 100644 routes/__tests__/client.test.txt delete mode 100644 routes/__tests__/compatability.test.txt delete mode 100644 routes/__tests__/crud_routes_function.txt delete mode 100644 routes/__tests__/overwrite-optimistic-locking.test.txt create mode 100644 routes/__tests__/overwrite.test.js delete mode 100644 routes/__tests__/overwrite.test.txt create mode 100644 test/bootstrap.js create mode 100644 test/coverage-inventory.json create mode 100644 test/loader.js diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index e0fe036c..14e92239 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -38,10 +38,12 @@ 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: Run tests + run: npm run test:ci + - name: Generate coverage report + run: npm run coverage:ci deploy: if: github.event.pull_request.draft == false needs: diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 70ea945a..ba7bd863 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -27,10 +27,12 @@ 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: Run tests + run: npm run test:ci + - name: Generate coverage report + run: npm run coverage:ci deploy: needs: test strategy: diff --git a/__tests__/core_provider_contract.test.js b/__tests__/core_provider_contract.test.js index b358a896..b6353eee 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, '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,21 @@ 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()) +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' + ) }) }) diff --git a/__tests__/openapi_sync_artifacts.test.js b/__tests__/openapi_sync_artifacts.test.js index d63db01a..cbf2168e 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,11 +16,11 @@ 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\.0\.3/) + assert.match(artifact, /title: RERUM Shared Components/) + assert.match(artifact, /version: 0\.1\.0/) + assert.match(artifact, /components:/) + assert.match(artifact, /schemas: \{\}/) } }) @@ -26,9 +28,10 @@ describe("Shared OpenAPI artifact sync scaffolding", () => { 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*rerum_openapi/) + assert.match(workflow, /peter-evans\/create-pull-request@v7/) + assert.match(workflow, /schemas\/openapi\/rerum-shared-components\.openapi\.yaml/) }) }) diff --git a/__tests__/provider_sync_artifacts.test.js b/__tests__/provider_sync_artifacts.test.js index b3d3e070..9969ed16 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" @@ -11,6 +13,6 @@ describe("provider sync artifacts", () => { const workflowPath = path.join(repoRoot, ".github", "workflows", "sync-core-provider-contract.yml") const workflow = fs.readFileSync(workflowPath, "utf8") - expect(workflow).toContain("contracts/core-provider.openapi.yaml") + assert.match(workflow, /contracts\/core-provider\.openapi\.yaml/) }) }) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 0123514d..76d45e9a 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -1,173 +1,87 @@ -/** - * 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) - }) - -}) - -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 () => { - const response = await request(app) - .post('/v1/api/create') - .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) - }) - - it('/v1/api/unset -- mounted ', async () => { - const response = await request(app) - .patch('/v1/api/unset') - .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) - }) - - 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 () => { - const response = await request(app) - .post('/v1/api/search') - .set('Content-Type', 'text/plain') - .send('mounted search') - expect(response.statusCode).not.toBe(404) - }) - - 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() - }); +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: 301 }, + { 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 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/app.js b/app.js index e8c8a7a1..bd9b3555 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' diff --git a/auth/__tests__/token.test.js b/auth/__tests__/token.test.js new file mode 100644 index 00000000..05edf73b --- /dev/null +++ b/auth/__tests__/token.test.js @@ -0,0 +1,130 @@ +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 + +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 + } + } +} + +afterEach(() => { + process.env.READONLY = originalReadonly + process.env.BOT_AGENT = originalBotAgent + process.env.RERUM_AGENT_CLAIM = originalAgentClaim + mock.restoreAll() +}) + +describe('auth middleware helpers', () => { + it('exports the expected checkJwt middleware pipeline order', () => { + assert.strictEqual(auth.checkJwt.length, 4) + assert.strictEqual(auth.checkJwt[0], auth.READONLY) + }) + + 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('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('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 () => { + mock.method(globalThis, 'fetch', async () => ({ + async json() { + return { + error: true, + error_description: 'bad refresh token' + } + } + })) + + const response = createResponse() + await auth.generateNewAccessToken({ body: { refresh_token: 'bad-token' } }, response) + + assert.strictEqual(response.statusCode, 500) + assert.strictEqual(response.body, 'bad refresh token') + }) +}) 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..d5809b21 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"){ diff --git a/bin/rerum_v1.js b/bin/rerum_v1.js index 8b269269..9724270d 100644 --- a/bin/rerum_v1.js +++ b/bin/rerum_v1.js @@ -8,8 +8,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/database/__mocks__/index.js b/database/__mocks__/index.js index 39204ed4..f498cfd1 100644 --- a/database/__mocks__/index.js +++ b/database/__mocks__/index.js @@ -1,45 +1,91 @@ /** - * 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.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()), + 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/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/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..a3ce8ebf 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 --env-file-if-exists=.env ./bin/rerum_v1.js", + "test": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-name-pattern='(?!@e2e)' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "test:ci": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "test:e2e": "node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-name-pattern='(?!@e2e)' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "coverage:ci": "c8 --reporter=html --reporter=json --reporter=text --include='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --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/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..3e744eae 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) => { @@ -17,6 +19,46 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /bulkCreate route without auth that will use controller.bulkCreate 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..4266dbbe 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 { db, 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,21 +17,24 @@ 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 + + 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 - expect(response.headers["location"]).toBe(returnedId) + assert.strictEqual(response.headers["location"], returnedId) }) it.skip("Support setting valid '_id' on '/create' request body.", async () => { 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..fa1d8e4a 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" @@ -41,19 +42,20 @@ 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) + assert.strictEqual(createResponse.statusCode, 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) }) diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index 42bab868..978abea8 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.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" @@ -29,13 +30,15 @@ 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)) }) diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index d171d195..00a35a4b 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.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" @@ -30,16 +31,20 @@ 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 () => { diff --git a/routes/__tests__/idNegotiation.test.js b/routes/__tests__/idNegotiation.test.js index c9b5c33a..8f66b906 100644 --- a/routes/__tests__/idNegotiation.test.js +++ b/routes/__tests__/idNegotiation.test.js @@ -1,5 +1,5 @@ -import { jest } from "@jest/globals" -import dotenv from "dotenv" +import { it } from 'node:test' +import assert from 'node:assert/strict' import controller from '../../db-controller.js' it("Functional '@id-id' negotiation on objects returned.", async () => { @@ -10,10 +10,10 @@ it("Functional '@id-id' negotiation on objects returned.", async () => { "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") + assert.strictEqual(negotiate._id, undefined) + assert.strictEqual(negotiate["@id"], undefined) + assert.strictEqual(negotiate.id, `${process.env.RERUM_ID_PREFIX}example`) + assert.strictEqual(negotiate.test, "item") let nonegotiate = { "@context":"http://example.org/context.json", @@ -23,8 +23,8 @@ it("Functional '@id-id' negotiation on objects returned.", async () => { "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") + assert.strictEqual(nonegotiate._id, undefined) + assert.strictEqual(nonegotiate["@id"], `${process.env.RERUM_ID_PREFIX}example`) + assert.strictEqual(nonegotiate.id, "test_example") + assert.strictEqual(nonegotiate.test, "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..d2c7fe6c --- /dev/null +++ b/routes/__tests__/overwrite.test.js @@ -0,0 +1,122 @@ +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) +routeTester.get('/id/:_id', controller.id) + +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('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') + }) +}) + +describe('id route overwrite headers', () => { + it('includes the current overwrite version header for existing objects', async () => { + const originalObject = structuredClone(baseObject) + originalObject.__rerum.isOverwritten = '2025-06-24T10:00:00' + db.findOne.mockResolvedValueOnce(originalObject) + + const response = await request(routeTester).get('/id/test-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(baseObject)) + + const response = await request(routeTester).get('/id/test-id') + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.headers['current-overwritten-version'], '') + }) +}) 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..1f6ac9b7 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -1,6 +1,5 @@ -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" @@ -38,18 +37,22 @@ 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) }) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index acace143..ba78c24a 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.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" @@ -29,25 +30,35 @@ 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 () => { diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index d9664d85..923a8764 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.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" @@ -39,19 +40,19 @@ const mockDoc = { } } -import { db } from '../../database/index.js' +import { db, resetMocks } from '../../database/index.js' + +beforeEach(() => { + resetMocks() +}) 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) + assert.strictEqual(createResponse.statusCode, 201) - // release with slug: - // 1st findOne for slug uniqueness check -> null - // 2nd findOne to fetch object being released -> mockDoc db.findOne .mockResolvedValueOnce(null) .mockResolvedValueOnce(mockDoc) @@ -61,13 +62,12 @@ it("'/release' route functions", async () => { .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.ok(releaseResponse.body.__rerum.isReleased) const returnedId = releaseResponse.body["@id"] ?? releaseResponse.body.id - expect(releaseResponse.headers["location"]).toBe(returnedId) + assert.strictEqual(releaseResponse.headers["location"], returnedId) - // cleanup slug object via internal helper path await controller.remove(slug) }) diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 2276c149..31287676 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -1,6 +1,5 @@ -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" @@ -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,10 @@ 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) }) diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index 12d433f4..aeee2509 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.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" @@ -29,13 +30,15 @@ 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)) }) diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index fe7baa56..1601044c 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -1,6 +1,5 @@ -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" @@ -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,12 @@ 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) }) diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index ca0524a3..a9c3e16d 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -1,6 +1,5 @@ -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" @@ -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/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..ec9239c4 --- /dev/null +++ b/test/coverage-inventory.json @@ -0,0 +1,107 @@ +{ + "runner": { + "test": "node:test", + "coverage": "c8", + "bootstrap": "test/bootstrap.js", + "loader": "test/loader.js" + }, + "validation": { + "suite": { + "tests": 81, + "suites": 11, + "pass": 78, + "fail": 0, + "skip": 3 + }, + "coverage": { + "statements": 79.77, + "branches": 100, + "functions": 100, + "lines": 79.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" + ], + "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": [ + "db-controller.js", + "routes/**/*.js" + ], + "excluded": [ + "**/__tests__/**" + ] + }, + "notableCoverageGaps": [ + { + "file": "routes/client.js", + "lines": "7-17,25-28", + "statements": 51.61 + }, + { + "file": "routes/patchSet.js", + "lines": "11-17,20-22", + "statements": 60 + }, + { + "file": "routes/patchUnset.js", + "lines": "11-17,20-22", + "statements": 60 + }, + { + "file": "routes/patchUpdate.js", + "lines": "12-18,21-23", + "statements": 61.53 + } + ] +} 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) +} From ef7cd04470f4f5df062d895067f6b71d9a635f5a Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Wed, 13 May 2026 16:15:19 -0500 Subject: [PATCH 02/28] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/cd_dev.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index 14e92239..340cc7d5 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -40,8 +40,6 @@ jobs: ${{ runner.os }}- - name: Install dependencies run: npm install - - name: Run tests - run: npm run test:ci - name: Generate coverage report run: npm run coverage:ci deploy: From 7ab956654ae5d2240bfb225655351d0449e66aa4 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 13 May 2026 16:17:17 -0500 Subject: [PATCH 03/28] Delete coverage-inventory.json --- test/coverage-inventory.json | 107 ----------------------------------- 1 file changed, 107 deletions(-) delete mode 100644 test/coverage-inventory.json diff --git a/test/coverage-inventory.json b/test/coverage-inventory.json deleted file mode 100644 index ec9239c4..00000000 --- a/test/coverage-inventory.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "runner": { - "test": "node:test", - "coverage": "c8", - "bootstrap": "test/bootstrap.js", - "loader": "test/loader.js" - }, - "validation": { - "suite": { - "tests": 81, - "suites": 11, - "pass": 78, - "fail": 0, - "skip": 3 - }, - "coverage": { - "statements": 79.77, - "branches": 100, - "functions": 100, - "lines": 79.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" - ], - "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": [ - "db-controller.js", - "routes/**/*.js" - ], - "excluded": [ - "**/__tests__/**" - ] - }, - "notableCoverageGaps": [ - { - "file": "routes/client.js", - "lines": "7-17,25-28", - "statements": 51.61 - }, - { - "file": "routes/patchSet.js", - "lines": "11-17,20-22", - "statements": 60 - }, - { - "file": "routes/patchUnset.js", - "lines": "11-17,20-22", - "statements": 60 - }, - { - "file": "routes/patchUpdate.js", - "lines": "12-18,21-23", - "statements": 61.53 - } - ] -} From 8e174de0907e797bd58ae3150d5234394f6639f4 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 13 May 2026 16:29:41 -0500 Subject: [PATCH 04/28] rebranch --- .github/workflows/cd_dev.yaml | 10 +++++----- .github/workflows/cd_prod.yaml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index 340cc7d5..26c901c3 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -6,7 +6,7 @@ jobs: merge-branch: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@main - name: Merge with main uses: devmasx/merge-branch@master with: @@ -19,15 +19,15 @@ jobs: needs: merge-branch runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@main - 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@main with: node-version: "24" - name: Cache node modules - uses: actions/cache@master + uses: actions/cache@main env: cache-name: cache-node-modules with: @@ -55,7 +55,7 @@ jobs: - vlcdhp02 runs-on: ${{ matrix.machines }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@main - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index ba7bd863..13106d0b 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@main - 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@main with: node-version: "24" # Speed up subsequent runs with caching - name: Cache node modules - uses: actions/cache@master + uses: actions/cache@main env: cache-name: cache-node-modules with: @@ -43,7 +43,7 @@ jobs: - vlcdhprdp02 runs-on: ${{ matrix.machines }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@main - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then From 497d648cef8f9761ebc23a4f9e23ebeedf4824b1 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 13 May 2026 16:38:30 -0500 Subject: [PATCH 05/28] Update cd_dev.yaml --- .github/workflows/cd_dev.yaml | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index 26c901c3..09860d41 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@main - - 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@main + - uses: actions/checkout@v4 - name: Create .env from secrets run: echo "${{ secrets.DEV_FULL_ENV }}" > .env - name: Setup Node.js - uses: actions/setup-node@main + uses: actions/setup-node@v4 with: node-version: "24" - name: Cache node modules - uses: actions/cache@main + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -45,7 +32,6 @@ jobs: 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@main + - uses: actions/checkout@v4 - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then From 3b3c2dc98e9b212b804c019f815856d188b7ead9 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 13 May 2026 16:39:49 -0500 Subject: [PATCH 06/28] Update cd_prod.yaml --- .github/workflows/cd_prod.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 13106d0b..2e4c73af 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@main + - uses: actions/checkout@v4 - name: Create .env from secrets run: echo "${{ secrets.PROD_FULL_ENV }}" > .env - name: Setup Node.js - uses: actions/setup-node@main + uses: actions/setup-node@v4 with: node-version: "24" # Speed up subsequent runs with caching - name: Cache node modules - uses: actions/cache@main + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -43,7 +43,7 @@ jobs: - vlcdhprdp02 runs-on: ${{ matrix.machines }} steps: - - uses: actions/checkout@main + - uses: actions/checkout@v4 - name: Deploy the app on the server run: | if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then From fe699a4a6acdb5b16569764e49a2b087fed3bad2 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 11:08:49 -0500 Subject: [PATCH 07/28] hotfix the search test --- database/__mocks__/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/__mocks__/index.js b/database/__mocks__/index.js index f498cfd1..51ec6a2c 100644 --- a/database/__mocks__/index.js +++ b/database/__mocks__/index.js @@ -60,6 +60,7 @@ export function resetMocks() { 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) @@ -75,6 +76,7 @@ export function resetMocks() { export const db = { 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)), From 4df8c8832e99cccf58ee07e6f0013b51c83cdedf Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 11:39:54 -0500 Subject: [PATCH 08/28] Update structural validation + sync strategy in __tests__/openapi_sync_artifacts.test.js --- __tests__/openapi_sync_artifacts.test.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/__tests__/openapi_sync_artifacts.test.js b/__tests__/openapi_sync_artifacts.test.js index cbf2168e..7b816196 100644 --- a/__tests__/openapi_sync_artifacts.test.js +++ b/__tests__/openapi_sync_artifacts.test.js @@ -16,14 +16,28 @@ describe("Shared OpenAPI artifact sync scaffolding", () => { const targetArtifact = fs.readFileSync(targetArtifactPath, "utf8") for (const artifact of [providerArtifact, targetArtifact]) { - assert.match(artifact, /openapi: 3\.0\.3/) - assert.match(artifact, /title: RERUM Shared Components/) - assert.match(artifact, /version: 0\.1\.0/) - assert.match(artifact, /components:/) - assert.match(artifact, /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") From 027aa93516725da1a18d0a126e322f32074f075b Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 12:01:42 -0500 Subject: [PATCH 09/28] dedup test runs on prod --- .github/workflows/cd_prod.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 2e4c73af..1a06f138 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -29,8 +29,6 @@ jobs: ${{ runner.os }}- - name: Install dependencies run: npm install - - name: Run tests - run: npm run test:ci - name: Generate coverage report run: npm run coverage:ci deploy: From 6f9bef9f274f241e94b026881cbf8c97b1ac7d44 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 13:52:23 -0500 Subject: [PATCH 10/28] Bug fixes and tests for critical functionality --- __tests__/routes_mounted.test.js | 43 ++++++++++++++++++++++ __tests__/utils.test.js | 62 ++++++++++++++++++++++++++++++++ app.js | 4 +-- auth/__tests__/token.test.js | 23 ++++++++++++ auth/index.js | 1 + rest.js | 3 ++ routes/__tests__/release.test.js | 2 +- 7 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 __tests__/utils.test.js diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 76d45e9a..277cefdb 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -52,6 +52,49 @@ describe('Mounted route surface', () => { } }) +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 (auth() enforces RS256)', async () => { + const response = await request(app) + .post('/v1/api/create') + .set('Authorization', `Bearer ${fakeJwt}`) + .set('Content-Type', 'application/json') + .send({ test: 'value' }) + assert.strictEqual(response.statusCode, 401) + }) +}) + +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) + .post('/v1/api/create') + .set('Content-Type', 'application/json') + .send(oversizePayload) + assert.strictEqual(response.statusCode, 413) + }) + + 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(oversizeText) + assert.strictEqual(response.statusCode, 413) + }) +}) + describe('Critical project assets', () => { it('keeps required public files in place', () => { const requiredPublicFiles = [ diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js new file mode 100644 index 00000000..35aea382 --- /dev/null +++ b/__tests__/utils.test.js @@ -0,0 +1,62 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' + +import utils from '../utils.js' +import { generateSlugId } 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') + }) +}) diff --git a/app.js b/app.js index bd9b3555..ebeb80c7 100644 --- a/app.js +++ b/app.js @@ -55,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 index 05edf73b..42d9c5e4 100644 --- a/auth/__tests__/token.test.js +++ b/auth/__tests__/token.test.js @@ -77,6 +77,29 @@ describe('auth middleware helpers', () => { 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) + }) + + // Regression guard for the defensive check at auth/index.js:169. Without it, + // 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' diff --git a/auth/index.js b/auth/index.js index d5809b21..ee569fb5 100644 --- a/auth/index.js +++ b/auth/index.js @@ -166,6 +166,7 @@ const isGenerator = (obj, userObj) => { * @returns Boolean for matching ID. */ const isBot = (userObj) => { + if (!process.env.BOT_AGENT) return false return process.env.BOT_AGENT === userObj[process.env.RERUM_AGENT_CLAIM] } diff --git a/rest.js b/rest.js index bff6df47..4a45a195 100644 --- a/rest.js +++ b/rest.js @@ -189,6 +189,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 diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index 923a8764..252e0730 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -58,7 +58,7 @@ it("'/release' route functions", async () => { .mockResolvedValueOnce(mockDoc) const releaseResponse = await request(routeTester) - .post(`/release/${MOCK_ID}`) + .patch(`/release/${MOCK_ID}`) .set("Slug", slug) .set("Content-Type", "application/json") From 00e82a0ce09dcffbf21349d2c46586c81373bbb2 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 14:19:12 -0500 Subject: [PATCH 11/28] critical functionality tests --- __tests__/utils.test.js | 203 +++++++++++++++++++++++++++++++- auth/__tests__/token.test.js | 79 +++++++++++++ routes/__tests__/delete.test.js | 31 +++++ routes/__tests__/search.test.js | 104 ++++++++++++++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 routes/__tests__/search.test.js diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js index 35aea382..78ecb76c 100644 --- a/__tests__/utils.test.js +++ b/__tests__/utils.test.js @@ -2,7 +2,15 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' import utils from '../utils.js' -import { generateSlugId } from '../controllers/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', () => { @@ -60,3 +68,196 @@ describe('controllers/utils.js generateSlugId', () => { 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') + }) +}) + +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/auth/__tests__/token.test.js b/auth/__tests__/token.test.js index 42d9c5e4..af7ae943 100644 --- a/auth/__tests__/token.test.js +++ b/auth/__tests__/token.test.js @@ -26,6 +26,20 @@ function createResponse() { } } +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(() => { process.env.READONLY = originalReadonly process.env.BOT_AGENT = originalBotAgent @@ -112,6 +126,71 @@ describe('auth middleware helpers', () => { }) }) +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' diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index fa1d8e4a..5c9b4468 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -59,3 +59,34 @@ it("'/delete' route functions", async () => { const deleteResponse = await request(routeTester).delete(`/delete/${MOCK_ID}`) 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__/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') + }) +}) From e0518882eb97706089e00597cddc709b4d630dd0 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 14:22:02 -0500 Subject: [PATCH 12/28] These no longer need the create step for testing --- routes/__tests__/delete.test.js | 9 --------- routes/__tests__/release.test.js | 9 --------- 2 files changed, 18 deletions(-) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index 5c9b4468..ccdf69f3 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -15,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 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]) @@ -49,12 +46,6 @@ beforeEach(() => { }) it("'/delete' route functions", async () => { - const createResponse = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ test: "item" }) - assert.strictEqual(createResponse.statusCode, 201) - db.findOne.mockResolvedValueOnce(mockDoc) const deleteResponse = await request(routeTester).delete(`/delete/${MOCK_ID}`) assert.strictEqual(deleteResponse.statusCode, 204) diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index 252e0730..0b5b1bff 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -15,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", "")}` @@ -47,12 +44,6 @@ beforeEach(() => { }) it("'/release' route functions", async () => { - const createResponse = await request(routeTester) - .post("/create") - .set("Content-Type", "application/json") - .send({ test: "item" }) - assert.strictEqual(createResponse.statusCode, 201) - db.findOne .mockResolvedValueOnce(null) .mockResolvedValueOnce(mockDoc) From 571459ef39b72129683abf2f6bdf50966f087146 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 14:29:11 -0500 Subject: [PATCH 13/28] These no longer need the create step for testing --- __tests__/provider_sync_artifacts.test.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/__tests__/provider_sync_artifacts.test.js b/__tests__/provider_sync_artifacts.test.js index 9969ed16..cc8353d3 100644 --- a/__tests__/provider_sync_artifacts.test.js +++ b/__tests__/provider_sync_artifacts.test.js @@ -7,12 +7,27 @@ 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, "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) + }) - assert.match(workflow, /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+contracts\/core-provider\.openapi\.yaml\s+\S*rerum_openapi\/seams\/tinynode-to-rerum\/openapi\/baseline\.openapi\.yaml/ + ) + assert.match(workflow, /repository:\s*cubap\/rerum_openapi/) + assert.match(workflow, /peter-evans\/create-pull-request@v7/) }) }) From 0f074a38492fcaef81e233b72dce3f74eb0e8299 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 14:55:33 -0500 Subject: [PATCH 14/28] Fix exclusion flag --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a3ce8ebf..e69f1f99 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ }, "scripts": { "start": "node --env-file-if-exists=.env ./bin/rerum_v1.js", - "test": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-name-pattern='(?!@e2e)' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", + "test": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", "test:ci": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", "test:e2e": "node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js" }, "dependencies": { From 44d5e6037e96890c69322a8ab3b770799174803b Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 15:05:53 -0500 Subject: [PATCH 15/28] cleanup --- __tests__/utils.test.js | 27 +++++++++++++++++++++++ routes/__tests__/create.test.js | 4 ---- routes/__tests__/delete.test.js | 3 --- routes/__tests__/id.test.js | 24 ++++++++++++++++++--- routes/__tests__/idNegotiation.test.js | 30 -------------------------- routes/__tests__/overwrite.test.js | 23 -------------------- routes/__tests__/query.test.js | 4 ---- routes/__tests__/release.test.js | 2 -- 8 files changed, 48 insertions(+), 69 deletions(-) delete mode 100644 routes/__tests__/idNegotiation.test.js diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js index 78ecb76c..900ca7c3 100644 --- a/__tests__/utils.test.js +++ b/__tests__/utils.test.js @@ -233,6 +233,33 @@ describe('controllers/utils.js idNegotiation edge cases', () => { 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', () => { diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 4266dbbe..53b1d25a 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -36,7 +36,3 @@ it("'/create' route functions", async () => { const returnedId = response.body["@id"] ?? response.body.id assert.strictEqual(response.headers["location"], returnedId) }) - -it.skip("Support setting valid '_id' on '/create' request body.", async () => { - // TODO -}) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index ccdf69f3..a5218939 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -15,9 +15,6 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) -// 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]) diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index 00a35a4b..912b9cbf 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -1,4 +1,4 @@ -import { beforeEach, it } from 'node:test' +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. @@ -47,6 +47,24 @@ it("'/id/:id' route functions", async () => { assert.ok(response.body.__rerum) }) -it.skip("Proper '@id-id' negotation on GET by URI.", async () => { - // TODO +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 8f66b906..00000000 --- a/routes/__tests__/idNegotiation.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { it } from 'node:test' -import assert from 'node:assert/strict' -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) - assert.strictEqual(negotiate._id, undefined) - assert.strictEqual(negotiate["@id"], undefined) - assert.strictEqual(negotiate.id, `${process.env.RERUM_ID_PREFIX}example`) - assert.strictEqual(negotiate.test, "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) - assert.strictEqual(nonegotiate._id, undefined) - assert.strictEqual(nonegotiate["@id"], `${process.env.RERUM_ID_PREFIX}example`) - assert.strictEqual(nonegotiate.id, "test_example") - assert.strictEqual(nonegotiate.test, "item") -}) diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index d2c7fe6c..af839003 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -14,7 +14,6 @@ const addAuth = (req, res, next) => { const routeTester = express() routeTester.use(express.json({ type: ['application/json', 'application/ld+json'] })) routeTester.put('/overwrite', addAuth, controller.overwrite) -routeTester.get('/id/:_id', controller.id) const baseObject = { _id: 'test-id', @@ -98,25 +97,3 @@ describe('overwrite route', () => { assert.strictEqual(response.body.currentVersion.__rerum.isOverwritten, '2025-06-24T10:30:00') }) }) - -describe('id route overwrite headers', () => { - it('includes the current overwrite version header for existing objects', async () => { - const originalObject = structuredClone(baseObject) - originalObject.__rerum.isOverwritten = '2025-06-24T10:00:00' - db.findOne.mockResolvedValueOnce(originalObject) - - const response = await request(routeTester).get('/id/test-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(baseObject)) - - const response = await request(routeTester).get('/id/test-id') - - assert.strictEqual(response.statusCode, 200) - assert.strictEqual(response.headers['current-overwritten-version'], '') - }) -}) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index ba78c24a..0e324dba 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -60,7 +60,3 @@ it("'/query' route functions", async () => { 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 -}) diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index 0b5b1bff..11b298a0 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -59,6 +59,4 @@ it("'/release' route functions", async () => { assert.ok(releaseResponse.body.__rerum.isReleased) const returnedId = releaseResponse.body["@id"] ?? releaseResponse.body.id assert.strictEqual(releaseResponse.headers["location"], returnedId) - - await controller.remove(slug) }) From a167e5719e0f80b1e620177bce3afbbf835dbd22 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 15:34:31 -0500 Subject: [PATCH 16/28] update start script to avoid dotenv dependency --- .github/workflows/cd_dev.yaml | 2 +- .github/workflows/cd_prod.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index 09860d41..433eebb9 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -56,4 +56,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 start --node-args="--env-file-if-exists=.env" -i max bin/rerum_v1.js diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 1a06f138..597abef2 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -55,4 +55,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 start --node-args="--env-file-if-exists=.env" -i max bin/rerum_v1.js From 2421e3906189368499d7840ba1221ef91fc671e4 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 16:05:18 -0500 Subject: [PATCH 17/28] Line up contract. Add HEAD tests because contract requires them. --- __tests__/core_provider_contract.test.js | 90 +++++++++- auth/__tests__/token.test.js | 1 - contracts/core-provider.openapi.yaml | 211 ++++++++++++++++++++++- controllers/release.js | 13 +- routes/__tests__/history.test.js | 28 ++- routes/__tests__/id.test.js | 28 ++- routes/__tests__/query.test.js | 43 ++++- routes/__tests__/release.test.js | 6 +- routes/__tests__/since.test.js | 28 ++- 9 files changed, 426 insertions(+), 22 deletions(-) diff --git a/__tests__/core_provider_contract.test.js b/__tests__/core_provider_contract.test.js index b6353eee..429ca628 100644 --- a/__tests__/core_provider_contract.test.js +++ b/__tests__/core_provider_contract.test.js @@ -133,11 +133,82 @@ function getContractOperations() { return operations.sort() } +/** + * 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'], + '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, @@ -145,3 +216,20 @@ describe('Core Provider Contract', () => { ) }) }) + +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/auth/__tests__/token.test.js b/auth/__tests__/token.test.js index af7ae943..bef0cba8 100644 --- a/auth/__tests__/token.test.js +++ b/auth/__tests__/token.test.js @@ -102,7 +102,6 @@ describe('auth middleware helpers', () => { assert.strictEqual(result, false) }) - // Regression guard for the defensive check at auth/index.js:169. Without it, // 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', () => { diff --git a/contracts/core-provider.openapi.yaml b/contracts/core-provider.openapi.yaml index 705d2fa8..a5c6ba80 100644 --- a/contracts/core-provider.openapi.yaml +++ b/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,6 +111,12 @@ 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 @@ -123,6 +143,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 +169,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 +186,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 +213,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 +244,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 +268,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 +298,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 +329,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 +357,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 +388,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 +416,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 +447,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 +475,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 +496,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 +517,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 +543,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/controllers/release.js b/controllers/release.js index afd466e3..d396f399 100644 --- a/controllers/release.js +++ b/controllers/release.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}`, diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index 978abea8..8e5850cf 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.test.js @@ -1,4 +1,4 @@ -import { beforeEach, it } from 'node:test' +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. @@ -9,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" @@ -42,3 +44,25 @@ it("'/history/:id' route functions", async () => { 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 912b9cbf..cb469cba 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -9,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" @@ -47,6 +50,29 @@ it("'/id/:id' route functions", async () => { assert.ok(response.body.__rerum) }) +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) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index 0e324dba..8d47f8af 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -1,4 +1,4 @@ -import { beforeEach, it } from 'node:test' +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. @@ -9,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" @@ -60,3 +63,39 @@ it("'/query' route functions", async () => { assert.ok(response.body[0]["@id"]) assert.strictEqual(response.body[0]._id, undefined) }) + +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 11b298a0..c48535e8 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -56,7 +56,11 @@ it("'/release' route functions", async () => { assert.strictEqual(releaseResponse.statusCode, 200) assert.strictEqual(releaseResponse.body._id, undefined) assert.ok(releaseResponse.body.__rerum) - assert.ok(releaseResponse.body.__rerum.isReleased) + 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 assert.strictEqual(releaseResponse.headers["location"], returnedId) }) diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index aeee2509..28e02e5a 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.test.js @@ -1,4 +1,4 @@ -import { beforeEach, it } from 'node:test' +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. @@ -9,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" @@ -42,3 +44,25 @@ it("'/since/:id' route functions", async () => { 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) + }) +}) From f26c5b932cabbd9532462c5c0de373846992f555 Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 19 May 2026 16:17:40 -0500 Subject: [PATCH 18/28] load env --- .github/workflows/cd_dev.yaml | 2 +- .github/workflows/cd_prod.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index e0fe036c..dd264604 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -70,4 +70,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 start bin/rerum_v1.js -i max --name rerum_v1 --update-env --node-args="--env-file=.env" diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml index 70ea945a..5c183853 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -55,4 +55,4 @@ jobs: git stash git pull npm install - pm2 start -i max bin/rerum_v1.js + pm2 start bin/rerum_v1.js -i max --name rerum_v1 --update-env --node-args="--env-file=.env" From 224451343cbea54fbe82bf9c4c36d9d45aa3ef7f Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 19 May 2026 16:39:04 -0500 Subject: [PATCH 19/28] last bit of cleanup and bolstering --- __tests__/core_provider_contract.test.js | 6 +++++ contracts/core-provider.openapi.yaml | 2 ++ routes/__tests__/bulkUpdate.test.js | 2 +- routes/__tests__/create.test.js | 2 +- routes/__tests__/patch.test.js | 15 ++++++++++++- routes/__tests__/release.test.js | 28 +++++++++++++++++++++++- routes/__tests__/set.test.js | 17 ++++++++++++-- routes/__tests__/unset.test.js | 16 ++++++++++++-- routes/__tests__/update.test.js | 4 ++-- 9 files changed, 82 insertions(+), 10 deletions(-) diff --git a/__tests__/core_provider_contract.test.js b/__tests__/core_provider_contract.test.js index 429ca628..9ca52f3f 100644 --- a/__tests__/core_provider_contract.test.js +++ b/__tests__/core_provider_contract.test.js @@ -199,6 +199,12 @@ const requiredResponseCodes = { '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'] diff --git a/contracts/core-provider.openapi.yaml b/contracts/core-provider.openapi.yaml index a5c6ba80..7174b34a 100644 --- a/contracts/core-provider.openapi.yaml +++ b/contracts/core-provider.openapi.yaml @@ -123,6 +123,8 @@ paths: responses: '200': description: Query result headers + '404': + $ref: '#/components/responses/NotFound' /api/search: post: summary: Search objects by keywords diff --git a/routes/__tests__/bulkUpdate.test.js b/routes/__tests__/bulkUpdate.test.js index 3e744eae..0defcabd 100644 --- a/routes/__tests__/bulkUpdate.test.js +++ b/routes/__tests__/bulkUpdate.test.js @@ -16,7 +16,7 @@ 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]) process.env.RERUM_ID_PREFIX ??= 'https://store.rerum.io/v1/id/' diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 53b1d25a..e83dc883 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -2,7 +2,7 @@ import { beforeEach, it } from 'node:test' import assert from 'node:assert/strict' import express from "express" import request from "supertest" -import { db, resetMocks } from '../../database/index.js' +import { resetMocks } from '../../database/index.js' 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. diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index 1f6ac9b7..994923eb 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -5,7 +5,7 @@ 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() @@ -56,3 +56,16 @@ it("'/patch' route functions", async () => { 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__/release.test.js b/routes/__tests__/release.test.js index c48535e8..d018c6da 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -6,7 +6,7 @@ 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() @@ -64,3 +64,29 @@ it("'/release' route functions", async () => { const returnedId = releaseResponse.body["@id"] ?? releaseResponse.body.id 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") + + assert.strictEqual(response.statusCode, 409) +}) diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 31287676..175d93e5 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -6,7 +6,7 @@ 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() @@ -15,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", "") @@ -56,3 +56,16 @@ it("'/set' route functions", async () => { const returnedId = response.body["@id"] ?? response.body.id 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__/unset.test.js b/routes/__tests__/unset.test.js index 1601044c..408c7009 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -6,7 +6,7 @@ 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() @@ -15,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" @@ -57,4 +57,16 @@ it("'/unset' route functions", async () => { 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 a9c3e16d..7999d38d 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -5,7 +5,7 @@ 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() @@ -14,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", "") From b6a71c97dd40a0c807785385f246857099d09fd8 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Wed, 20 May 2026 10:44:24 -0500 Subject: [PATCH 20/28] Coverage (#274) * Add middleware for PATCH override support Introduce createPatchOverrideMiddleware in rest.js to centralize validation of X-HTTP-Method-Override for POST-to-PATCH requests. Replace duplicated inline checks in routes/patchSet.js, routes/patchUnset.js, and routes/patchUpdate.js with the new middleware (each using a route-specific statusMessage). Also standardize error handling by ending 405 responses with res.status(405).end() and export the new middleware from rest.js. * End 405 responses and add route wrapper tests Replace patterns that set status and call next(res) with res.status(405).end() in route fallbacks (routes/query.js, routes/release.js, routes/search.js) to ensure the response is terminated immediately. Add route wrapper tests (routes/__tests__/route_wrappers.test.js) to validate method-override handling, unsupported-method fallbacks, client verify behavior, and API discovery. Include a test coverage inventory (test/coverage-inventory.json) to record current test/coverage state. * End responses for unsupported-method handlers Replace next(res) with res.status(405).end() in multiple route fallbacks to ensure responses are terminated immediately (bulkCreate, bulkUpdate, create, delete, history, id, overwrite, putUpdate, since). Update route wrapper tests to import additional routers, add helper asserting fallback behavior on specific paths, and add tests for static/index handlers. Update test coverage inventory to reflect the new/updated tests. --- rest.js | 19 +- routes/__tests__/route_wrappers.test.js | 383 ++++++++++++++++++++++++ routes/bulkCreate.js | 3 +- routes/bulkUpdate.js | 3 +- routes/create.js | 3 +- routes/delete.js | 3 +- routes/history.js | 3 +- routes/id.js | 3 +- routes/overwrite.js | 3 +- routes/patchSet.js | 15 +- routes/patchUnset.js | 15 +- routes/patchUpdate.js | 23 +- routes/putUpdate.js | 3 +- routes/query.js | 3 +- routes/release.js | 3 +- routes/search.js | 8 +- routes/since.js | 3 +- test/coverage-inventory.json | 98 ++++++ 18 files changed, 529 insertions(+), 65 deletions(-) create mode 100644 routes/__tests__/route_wrappers.test.js create mode 100644 test/coverage-inventory.json diff --git a/rest.js b/rest.js index 4a45a195..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) @@ -213,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__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js new file mode 100644 index 00000000..3b14f86c --- /dev/null +++ b/routes/__tests__/route_wrappers.test.js @@ -0,0 +1,383 @@ +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' + +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, expectedMessage) { + const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { + header() { + return undefined + } + }) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, expectedMessage) + 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 assertUnsupportedMethod(router, expectedMessage) { + const fallbackLayer = getRoute(router, '/').stack.at(-1) + assert.ok(fallbackLayer, 'Expected fallback .all() layer') + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, expectedMessage) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +function assertUnsupportedMethodOnPath(router, path, expectedMessage) { + 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.statusMessage, expectedMessage) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +describe('patch route wrappers', () => { + it('rejects POST /set requests without PATCH override', () => { + assertInvalidOverride( + patchSetRouter, + 'Improper request method for updating, please use PATCH to add new keys to this object.' + ) + }) + + it('passes POST /set requests with PATCH override to the next handler', () => { + assertValidOverride(patchSetRouter) + }) + + it('rejects unsupported methods for /set', () => { + assertUnsupportedMethod( + patchSetRouter, + 'Improper request method for updating, please use PATCH to add new keys to this object.' + ) + }) + + it('rejects POST /unset requests without PATCH override', () => { + assertInvalidOverride( + patchUnsetRouter, + 'Improper request method for updating, please use PATCH to remove keys from this object.' + ) + }) + + it('passes POST /unset requests with PATCH override to the next handler', () => { + assertValidOverride(patchUnsetRouter) + }) + + it('rejects unsupported methods for /unset', () => { + assertUnsupportedMethod( + patchUnsetRouter, + 'Improper request method for updating, please use PATCH to remove keys from this object.' + ) + }) + + it('rejects POST /patch requests without PATCH override', () => { + assertInvalidOverride( + patchUpdateRouter, + 'Improper request method for updating, please use PATCH to alter the existing keys this object.' + ) + }) + + it('passes POST /patch requests with PATCH override to the next handler', () => { + assertValidOverride(patchUpdateRouter) + }) + + it('rejects unsupported methods for /patch', () => { + assertUnsupportedMethod( + patchUpdateRouter, + 'Improper request method for updating, please use PATCH to alter existing keys on this object.' + ) + }) +}) + +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.strictEqual(res.body, 'The token was verified by Auth0') + assert.deepStrictEqual(nextCalls, []) + }) +}) + +describe('search, query, and release fallbacks', () => { + it('rejects unsupported methods for /bulkCreate', () => { + assertUnsupportedMethod( + bulkCreateRouter, + 'Improper request method for creating, please use POST.' + ) + }) + + it('rejects unsupported methods for /bulkUpdate', () => { + assertUnsupportedMethod( + bulkUpdateRouter, + 'Improper request method for creating, please use PUT.' + ) + }) + + it('rejects unsupported methods for /create', () => { + assertUnsupportedMethod( + createRouter, + 'Improper request method for creating, please use POST.' + ) + }) + + it('rejects unsupported methods for /update', () => { + assertUnsupportedMethod( + updateRouter, + 'Improper request method for updating, please use PUT to update this object.' + ) + }) + + it('rejects unsupported methods for /overwrite', () => { + assertUnsupportedMethod( + overwriteRouter, + 'Improper request method for overwriting, please use PUT to overwrite this object.' + ) + }) + + it('rejects unsupported methods for /search', () => { + assertUnsupportedMethod( + searchRouter, + 'Improper request method for search. Please use POST.' + ) + }) + + it('rejects unsupported methods for /search/phrase', () => { + const fallbackLayer = getRoute(searchRouter, '/phrase').stack.at(-1) + assert.ok(fallbackLayer, 'Expected fallback .all() layer for /phrase') + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, 'Improper request method for search. Please use POST.') + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) + }) + + it('rejects unsupported methods for /query', () => { + assertUnsupportedMethod( + queryRouter, + 'Improper request method for requesting objects with matching properties. Please use POST.' + ) + }) + + it('rejects unsupported methods for /release/:id', () => { + assertUnsupportedMethodOnPath( + releaseRouter, + '/:_id', + 'Improper request method for releasing, please use PATCH to release this object.' + ) + }) + + it('rejects unsupported methods for /delete/:id', () => { + assertUnsupportedMethodOnPath( + deleteRouter, + '/:_id', + 'Improper request method for deleting, please use DELETE.' + ) + }) + + it('rejects unsupported methods for /history/:id', () => { + assertUnsupportedMethodOnPath( + historyRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) + + it('rejects unsupported methods for /id/:id', () => { + assertUnsupportedMethodOnPath( + idRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) + + it('rejects unsupported methods for /since/:id', () => { + assertUnsupportedMethodOnPath( + sinceRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) +}) + +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/) + assert.match(response.text, /RERUM/i) + }) + + 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.', + '/update': 'PUT - Update the body an existing object.', + '/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.' + }) + }) +}) 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..5a9731f5 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 the existing keys 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/coverage-inventory.json b/test/coverage-inventory.json new file mode 100644 index 00000000..01a838d3 --- /dev/null +++ b/test/coverage-inventory.json @@ -0,0 +1,98 @@ +{ + "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": [ + "db-controller.js", + "routes/**/*.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 + } + ] +} From 270b89f1d42ab872b71ffb56a7016471a498329b Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Wed, 20 May 2026 10:50:04 -0500 Subject: [PATCH 21/28] Guard for bot checks. Stop releases from hanging on a tree error. --- auth/index.js | 2 +- controllers/release.js | 87 ++++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/auth/index.js b/auth/index.js index ee569fb5..d33e4940 100644 --- a/auth/index.js +++ b/auth/index.js @@ -166,7 +166,7 @@ const isGenerator = (obj, userObj) => { * @returns Boolean for matching ID. */ const isBot = (userObj) => { - if (!process.env.BOT_AGENT) return false + 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/controllers/release.js b/controllers/release.js index d396f399..86cdf6ef 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -76,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 From ccce2c5108c0a242c689f365b92644d55f472b96 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Wed, 20 May 2026 11:55:11 -0500 Subject: [PATCH 22/28] Changes from review. Reduce wording locks in tests. --- __tests__/routes_mounted.test.js | 7 +- auth/__tests__/token.test.js | 10 +- routes/__tests__/overwrite.test.js | 24 +++ routes/__tests__/route_wrappers.test.js | 189 +++++------------------- routes/api-routes.js | 7 +- routes/patchUpdate.js | 2 +- 6 files changed, 74 insertions(+), 165 deletions(-) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 277cefdb..0b16cedd 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -64,13 +64,18 @@ describe('Auth pipeline', () => { })).toString('base64url') const fakeJwt = `${header}.${payload}.fakesignature` - it('rejects an HS256-signed token (auth() enforces RS256)', async () => { + 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({ 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) }) }) diff --git a/auth/__tests__/token.test.js b/auth/__tests__/token.test.js index bef0cba8..c61c98e0 100644 --- a/auth/__tests__/token.test.js +++ b/auth/__tests__/token.test.js @@ -48,11 +48,6 @@ afterEach(() => { }) describe('auth middleware helpers', () => { - it('exports the expected checkJwt middleware pipeline order', () => { - assert.strictEqual(auth.checkJwt.length, 4) - assert.strictEqual(auth.checkJwt[0], auth.READONLY) - }) - it('READONLY blocks writes when the server is in readonly mode', () => { process.env.READONLY = 'true' const response = createResponse() @@ -213,11 +208,12 @@ describe('auth token refresh helpers', () => { }) 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: 'bad refresh token' + error_description: errorDescription } } })) @@ -226,6 +222,6 @@ describe('auth token refresh helpers', () => { await auth.generateNewAccessToken({ body: { refresh_token: 'bad-token' } }, response) assert.strictEqual(response.statusCode, 500) - assert.strictEqual(response.body, 'bad refresh token') + assert.strictEqual(response.body, errorDescription) }) }) diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index af839003..7650378b 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -82,6 +82,30 @@ describe('overwrite route', () => { 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' diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js index 3b14f86c..a5f95d6a 100644 --- a/routes/__tests__/route_wrappers.test.js +++ b/routes/__tests__/route_wrappers.test.js @@ -78,7 +78,7 @@ function getOverrideLayer(router) { return overrideLayer } -function assertInvalidOverride(router, expectedMessage) { +function assertInvalidOverride(router) { const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { header() { return undefined @@ -86,7 +86,6 @@ function assertInvalidOverride(router, expectedMessage) { }) assert.strictEqual(res.statusCode, 405) - assert.strictEqual(res.statusMessage, expectedMessage) assert.strictEqual(res.ended, true) assert.deepStrictEqual(nextCalls, []) } @@ -104,84 +103,41 @@ function assertValidOverride(router) { assert.strictEqual(nextCalls[0], undefined) } -function assertUnsupportedMethod(router, expectedMessage) { - const fallbackLayer = getRoute(router, '/').stack.at(-1) - assert.ok(fallbackLayer, 'Expected fallback .all() layer') - - const { res, nextCalls } = invokeLayer(fallbackLayer) - - assert.strictEqual(res.statusCode, 405) - assert.strictEqual(res.statusMessage, expectedMessage) - assert.strictEqual(res.ended, true) - assert.deepStrictEqual(nextCalls, []) -} - -function assertUnsupportedMethodOnPath(router, path, expectedMessage) { +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.statusMessage, expectedMessage) assert.strictEqual(res.ended, true) assert.deepStrictEqual(nextCalls, []) } describe('patch route wrappers', () => { it('rejects POST /set requests without PATCH override', () => { - assertInvalidOverride( - patchSetRouter, - 'Improper request method for updating, please use PATCH to add new keys to this object.' - ) + assertInvalidOverride(patchSetRouter) }) it('passes POST /set requests with PATCH override to the next handler', () => { assertValidOverride(patchSetRouter) }) - it('rejects unsupported methods for /set', () => { - assertUnsupportedMethod( - patchSetRouter, - 'Improper request method for updating, please use PATCH to add new keys to this object.' - ) - }) - it('rejects POST /unset requests without PATCH override', () => { - assertInvalidOverride( - patchUnsetRouter, - 'Improper request method for updating, please use PATCH to remove keys from this object.' - ) + assertInvalidOverride(patchUnsetRouter) }) it('passes POST /unset requests with PATCH override to the next handler', () => { assertValidOverride(patchUnsetRouter) }) - it('rejects unsupported methods for /unset', () => { - assertUnsupportedMethod( - patchUnsetRouter, - 'Improper request method for updating, please use PATCH to remove keys from this object.' - ) - }) - it('rejects POST /patch requests without PATCH override', () => { - assertInvalidOverride( - patchUpdateRouter, - 'Improper request method for updating, please use PATCH to alter the existing keys this object.' - ) + assertInvalidOverride(patchUpdateRouter) }) it('passes POST /patch requests with PATCH override to the next handler', () => { assertValidOverride(patchUpdateRouter) }) - - it('rejects unsupported methods for /patch', () => { - assertUnsupportedMethod( - patchUpdateRouter, - 'Improper request method for updating, please use PATCH to alter existing keys on this object.' - ) - }) }) describe('client route wrappers', () => { @@ -229,112 +185,35 @@ describe('client route wrappers', () => { assert.strictEqual(res.headers['Content-Type'], 'text/plain') assert.strictEqual(res.statusCode, 200) - assert.strictEqual(res.body, 'The token was verified by Auth0') assert.deepStrictEqual(nextCalls, []) }) }) -describe('search, query, and release fallbacks', () => { - it('rejects unsupported methods for /bulkCreate', () => { - assertUnsupportedMethod( - bulkCreateRouter, - 'Improper request method for creating, please use POST.' - ) - }) - - it('rejects unsupported methods for /bulkUpdate', () => { - assertUnsupportedMethod( - bulkUpdateRouter, - 'Improper request method for creating, please use PUT.' - ) - }) - - it('rejects unsupported methods for /create', () => { - assertUnsupportedMethod( - createRouter, - 'Improper request method for creating, please use POST.' - ) - }) - - it('rejects unsupported methods for /update', () => { - assertUnsupportedMethod( - updateRouter, - 'Improper request method for updating, please use PUT to update this object.' - ) - }) - - it('rejects unsupported methods for /overwrite', () => { - assertUnsupportedMethod( - overwriteRouter, - 'Improper request method for overwriting, please use PUT to overwrite this object.' - ) - }) - - it('rejects unsupported methods for /search', () => { - assertUnsupportedMethod( - searchRouter, - 'Improper request method for search. Please use POST.' - ) - }) - - it('rejects unsupported methods for /search/phrase', () => { - const fallbackLayer = getRoute(searchRouter, '/phrase').stack.at(-1) - assert.ok(fallbackLayer, 'Expected fallback .all() layer for /phrase') - - const { res, nextCalls } = invokeLayer(fallbackLayer) - - assert.strictEqual(res.statusCode, 405) - assert.strictEqual(res.statusMessage, 'Improper request method for search. Please use POST.') - assert.strictEqual(res.ended, true) - assert.deepStrictEqual(nextCalls, []) - }) - - it('rejects unsupported methods for /query', () => { - assertUnsupportedMethod( - queryRouter, - 'Improper request method for requesting objects with matching properties. Please use POST.' - ) - }) - - it('rejects unsupported methods for /release/:id', () => { - assertUnsupportedMethodOnPath( - releaseRouter, - '/:_id', - 'Improper request method for releasing, please use PATCH to release this object.' - ) - }) - - it('rejects unsupported methods for /delete/:id', () => { - assertUnsupportedMethodOnPath( - deleteRouter, - '/:_id', - 'Improper request method for deleting, please use DELETE.' - ) - }) - - it('rejects unsupported methods for /history/:id', () => { - assertUnsupportedMethodOnPath( - historyRouter, - '/:_id', - 'Improper request method, please use GET.' - ) - }) - - it('rejects unsupported methods for /id/:id', () => { - assertUnsupportedMethodOnPath( - idRouter, - '/:_id', - 'Improper request method, please use GET.' - ) - }) - - it('rejects unsupported methods for /since/:id', () => { - assertUnsupportedMethodOnPath( - sinceRouter, - '/:_id', - 'Improper request method, please use GET.' - ) - }) +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' } + ] + + for (const { label, router, path } of cases) { + it(`rejects unsupported methods for ${label}`, () => { + assertUnsupportedMethodOnPath(router, path) + }) + } }) describe('api routes discovery', () => { @@ -357,7 +236,6 @@ describe('api routes discovery', () => { assert.strictEqual(response.statusCode, 200) assert.match(response.headers['content-type'], /^text\/html/) - assert.match(response.text, /RERUM/i) }) it('returns the advertised endpoint map for GET /api', async () => { @@ -370,14 +248,17 @@ describe('api routes discovery', () => { 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.', - '/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/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/patchUpdate.js b/routes/patchUpdate.js index 5a9731f5..2c496bf2 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -6,7 +6,7 @@ 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 the existing keys this object.') +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) From 2719fa12623543f75e49a9e630111141838008cc Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Wed, 20 May 2026 12:23:35 -0500 Subject: [PATCH 23/28] last bit of cleanup from review --- auth/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/index.js b/auth/index.js index d33e4940..7de3a892 100644 --- a/auth/index.js +++ b/auth/index.js @@ -17,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) } @@ -28,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) From 07be9813103b7fef89dddf2da292571b4bbc4bdc Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 21 May 2026 09:46:24 -0500 Subject: [PATCH 24/28] Introduce ecosystem file and env-loader for RHEL (#276) * Introduce ecosystem file and env-loader for RHEL * Introduce ecosystem file and env-loader for RHEL * changes from testing * cmon RHEL * back to 500M * 100% coverage --- .github/workflows/cd_dev.yaml | 2 +- .github/workflows/cd_prod.yaml | 2 +- bin/rerum_v1.js | 5 ++ ecosystem.config.json | 22 +++++++ env-loader.js | 81 ++++++++++++++++++++++++ package.json | 12 ++-- routes/__tests__/route_wrappers.test.js | 6 +- routes/_gog_fragments_from_manuscript.js | 3 +- routes/_gog_glosses_from_manuscript.js | 3 +- 9 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 ecosystem.config.json create mode 100644 env-loader.js diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml index 433eebb9..c11bb8e4 100644 --- a/.github/workflows/cd_dev.yaml +++ b/.github/workflows/cd_dev.yaml @@ -56,4 +56,4 @@ jobs: git stash git pull npm install - pm2 start --node-args="--env-file-if-exists=.env" -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 597abef2..09a685c4 100644 --- a/.github/workflows/cd_prod.yaml +++ b/.github/workflows/cd_prod.yaml @@ -55,4 +55,4 @@ jobs: git stash git pull npm install - pm2 start --node-args="--env-file-if-exists=.env" -i max bin/rerum_v1.js + pm2 startOrReload ecosystem.config.json --env production diff --git a/bin/rerum_v1.js b/bin/rerum_v1.js index 9724270d..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. */ 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/package.json b/package.json index e69f1f99..4a6c336f 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,12 @@ "npm": ">=11.0.0" }, "scripts": { - "start": "node --env-file-if-exists=.env ./bin/rerum_v1.js", - "test": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test --test-skip-pattern='@e2e' __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", - "test:ci": "node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.js", - "test:e2e": "node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --env-file-if-exists=.env --import ./test/bootstrap.js --test __tests__/*.test.js routes/__tests__/*.test.js auth/__tests__/*.test.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='db-controller.js' --include='routes/**/*.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='db-controller.js' --include='routes/**/*.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", diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js index a5f95d6a..e4522a83 100644 --- a/routes/__tests__/route_wrappers.test.js +++ b/routes/__tests__/route_wrappers.test.js @@ -22,6 +22,8 @@ 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) @@ -206,7 +208,9 @@ describe('unsupported-method 405 fallbacks', () => { { 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: '/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) { 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 From b2b13072e7191a6e0ea319f0c52dadd182039447 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 21 May 2026 11:01:01 -0500 Subject: [PATCH 25/28] Changes during review --- __tests__/routes_mounted.test.js | 2 +- auth/__tests__/token.test.js | 13 ++++++++++--- package.json | 4 ++-- test/coverage-inventory.json | 6 +++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 0b16cedd..aacb2094 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -6,7 +6,7 @@ import request from 'supertest' import app from '../app.js' const mountedTopLevelRoutes = [ - { name: '/v1', method: 'get', path: '/v1', expectedStatus: 301 }, + { 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 }, diff --git a/auth/__tests__/token.test.js b/auth/__tests__/token.test.js index c61c98e0..fa67f2dd 100644 --- a/auth/__tests__/token.test.js +++ b/auth/__tests__/token.test.js @@ -7,6 +7,13 @@ 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, @@ -41,9 +48,9 @@ function makeBearer(payload, header = { alg: 'RS256', typ: 'JWT' }) { } afterEach(() => { - process.env.READONLY = originalReadonly - process.env.BOT_AGENT = originalBotAgent - process.env.RERUM_AGENT_CLAIM = originalAgentClaim + restoreEnv('READONLY', originalReadonly) + restoreEnv('BOT_AGENT', originalBotAgent) + restoreEnv('RERUM_AGENT_CLAIM', originalAgentClaim) mock.restoreAll() }) diff --git a/package.json b/package.json index 4a6c336f..952c5a1f 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "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='db-controller.js' --include='routes/**/*.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='db-controller.js' --include='routes/**/*.js' --exclude='**/__tests__/**' node --import ./env-loader.js --import ./test/bootstrap.js --test __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", diff --git a/test/coverage-inventory.json b/test/coverage-inventory.json index 01a838d3..bb7b72ea 100644 --- a/test/coverage-inventory.json +++ b/test/coverage-inventory.json @@ -76,8 +76,12 @@ }, "coverageScope": { "included": [ + "controllers/**/*.js", + "routes/**/*.js", + "auth/**/*.js", "db-controller.js", - "routes/**/*.js" + "rest.js", + "utils.js" ], "excluded": [ "**/__tests__/**" From b68fb9fb34036658d2377f779c8176e23bb996fd Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 21 May 2026 11:35:20 -0500 Subject: [PATCH 26/28] test: lock down secret name and cp target in sync workflow tests Adds two regex assertions to each sync-artifact test: - secrets.OPENAPI must appear verbatim (catches a rename that would silently break the receiver checkout step at runtime) - the literal 'cp source target' line must point at the expected paths (already covered for provider sync; now mirrored on shared sync) Backports improvements developed during TinyNode's parallel sync test where the same regression class actually bit us this week. --- __tests__/openapi_sync_artifacts.test.js | 10 ++++++++++ __tests__/provider_sync_artifacts.test.js | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/__tests__/openapi_sync_artifacts.test.js b/__tests__/openapi_sync_artifacts.test.js index 7b816196..6ae1c84b 100644 --- a/__tests__/openapi_sync_artifacts.test.js +++ b/__tests__/openapi_sync_artifacts.test.js @@ -47,5 +47,15 @@ describe("Shared OpenAPI artifact sync scaffolding", () => { assert.match(workflow, /path:\s*rerum_openapi/) assert.match(workflow, /peter-evans\/create-pull-request@v7/) assert.match(workflow, /schemas\/openapi\/rerum-shared-components\.openapi\.yaml/) + assert.match( + workflow, + /cp\s+openapi\/components\/rerum-shared-components\.openapi\.yaml\s+\S*rerum_openapi\/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 cc8353d3..fb7e0c89 100644 --- a/__tests__/provider_sync_artifacts.test.js +++ b/__tests__/provider_sync_artifacts.test.js @@ -29,5 +29,10 @@ describe("provider sync artifacts", () => { ) assert.match(workflow, /repository:\s*cubap\/rerum_openapi/) assert.match(workflow, /peter-evans\/create-pull-request@v7/) + 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" + ) }) }) From 7de7ad0794d29b64e74631ceed5b15e9addb3caf Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 21 May 2026 13:17:35 -0500 Subject: [PATCH 27/28] chore: align RERUM sync workflows with TinyNode/TinyPen pattern - bump peter-evans/create-pull-request v7 -> v8 - rename receiver checkout path rerum_openapi -> receiver - add `add-paths`, explicit `base: main`, `ref: main` on receiver checkout - add `test -f` verify-stub guard on both workflows (receiver stubs exist) - branch naming automation/sync-rerum-* -> sync/rerum-* - commit-message/title gain `chore:` prefix - PR body now matches the bullet-list format used by TinyNode and TinyPen - update tests to use `path:\s*receiver`, `@v\d+`, and new cp target regex --- .../workflows/sync-core-provider-contract.yml | 51 ++++++++++--------- .../workflows/sync-rerum-shared-openapi.yml | 44 ++++++++-------- __tests__/openapi_sync_artifacts.test.js | 6 +-- __tests__/provider_sync_artifacts.test.js | 4 +- 4 files changed, 57 insertions(+), 48 deletions(-) diff --git a/.github/workflows/sync-core-provider-contract.yml b/.github/workflows/sync-core-provider-contract.yml index 0e412125..fe1d26f3 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 - 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 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: `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__/openapi_sync_artifacts.test.js b/__tests__/openapi_sync_artifacts.test.js index 6ae1c84b..c06563d4 100644 --- a/__tests__/openapi_sync_artifacts.test.js +++ b/__tests__/openapi_sync_artifacts.test.js @@ -44,12 +44,12 @@ describe("Shared OpenAPI artifact sync scaffolding", () => { assert.match(workflow, /openapi\/components\/rerum-shared-components\.openapi\.yaml/) assert.match(workflow, /repository:\s*cubap\/rerum_openapi/) - assert.match(workflow, /path:\s*rerum_openapi/) - assert.match(workflow, /peter-evans\/create-pull-request@v7/) + 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*rerum_openapi\/schemas\/openapi\/rerum-shared-components\.openapi\.yaml/, + /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( diff --git a/__tests__/provider_sync_artifacts.test.js b/__tests__/provider_sync_artifacts.test.js index fb7e0c89..01dad666 100644 --- a/__tests__/provider_sync_artifacts.test.js +++ b/__tests__/provider_sync_artifacts.test.js @@ -25,10 +25,10 @@ describe("provider sync artifacts", () => { // path appears in the PR body text too, so a substring match alone is too loose. assert.match( workflow, - /cp\s+contracts\/core-provider\.openapi\.yaml\s+\S*rerum_openapi\/seams\/tinynode-to-rerum\/openapi\/baseline\.openapi\.yaml/ + /cp\s+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@v7/) + assert.match(workflow, /peter-evans\/create-pull-request@v\d+/) assert.match( workflow, /secrets\.OPENAPI(?!\w)/, From 335920fa024686e07a48a396378ea3c5e81bb345 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Thu, 21 May 2026 15:13:25 -0500 Subject: [PATCH 28/28] Remove claude from authorship lines. Move contracts/ under openapi/contracts like other repos for consistency --- .github/workflows/sync-core-provider-contract.yml | 6 +++--- __tests__/core_provider_contract.test.js | 2 +- __tests__/provider_sync_artifacts.test.js | 4 ++-- controllers/bulk.js | 2 +- controllers/crud.js | 2 +- controllers/delete.js | 2 +- controllers/gog.js | 2 +- controllers/history.js | 2 +- controllers/overwrite.js | 2 +- controllers/patchSet.js | 2 +- controllers/patchUnset.js | 2 +- controllers/patchUpdate.js | 2 +- controllers/putUpdate.js | 2 +- controllers/release.js | 2 +- controllers/update.js | 2 +- controllers/utils.js | 2 +- db-controller.js | 2 +- {contracts => openapi/contracts}/core-provider.openapi.yaml | 0 18 files changed, 20 insertions(+), 20 deletions(-) rename {contracts => openapi/contracts}/core-provider.openapi.yaml (100%) diff --git a/.github/workflows/sync-core-provider-contract.yml b/.github/workflows/sync-core-provider-contract.yml index fe1d26f3..88a81cc2 100644 --- a/.github/workflows/sync-core-provider-contract.yml +++ b/.github/workflows/sync-core-provider-contract.yml @@ -5,7 +5,7 @@ on: branches: - main paths: - - contracts/core-provider.openapi.yaml + - openapi/contracts/core-provider.openapi.yaml - routes/** workflow_dispatch: @@ -32,7 +32,7 @@ jobs: run: test -f receiver/seams/tinynode-to-rerum/openapi/baseline.openapi.yaml - name: Copy canonical provider contract - run: cp contracts/core-provider.openapi.yaml receiver/seams/tinynode-to-rerum/openapi/baseline.openapi.yaml + 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 @@ -49,5 +49,5 @@ jobs: Syncs the canonical RERUM core provider contract from CenterForDigitalHumanities/rerum_server_nodejs. - Source commit: ${{ github.sha }} - - Source artifact: `contracts/core-provider.openapi.yaml` + - Source artifact: `openapi/contracts/core-provider.openapi.yaml` - Target artifact: `seams/tinynode-to-rerum/openapi/baseline.openapi.yaml` diff --git a/__tests__/core_provider_contract.test.js b/__tests__/core_provider_contract.test.js index 9ca52f3f..9cea0178 100644 --- a/__tests__/core_provider_contract.test.js +++ b/__tests__/core_provider_contract.test.js @@ -7,7 +7,7 @@ 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 contractPath = path.join(repoRoot, 'openapi', 'contracts', 'core-provider.openapi.yaml') const skippedMountedRouters = new Set([ './static.js', diff --git a/__tests__/provider_sync_artifacts.test.js b/__tests__/provider_sync_artifacts.test.js index 01dad666..d6ec98a3 100644 --- a/__tests__/provider_sync_artifacts.test.js +++ b/__tests__/provider_sync_artifacts.test.js @@ -7,7 +7,7 @@ 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, "contracts", "core-provider.openapi.yaml") +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", () => { @@ -25,7 +25,7 @@ describe("provider sync artifacts", () => { // path appears in the PR body text too, so a substring match alone is too loose. assert.match( workflow, - /cp\s+contracts\/core-provider\.openapi\.yaml\s+\S*receiver\/seams\/tinynode-to-rerum\/openapi\/baseline\.openapi\.yaml/ + /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+/) 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 86cdf6ef..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' 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/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/contracts/core-provider.openapi.yaml b/openapi/contracts/core-provider.openapi.yaml similarity index 100% rename from contracts/core-provider.openapi.yaml rename to openapi/contracts/core-provider.openapi.yaml