diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ef6e21 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c43d2d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +coverage/ +.fastembed_cache/ diff --git a/.mocharc.cjs b/.mocharc.cjs new file mode 100644 index 0000000..532607d --- /dev/null +++ b/.mocharc.cjs @@ -0,0 +1,4 @@ +module.exports = { + spec: 'test/**/*.test.js', + timeout: 5000 +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..960cce2 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# vc-api-mock + +A mock server and test suite for the VCALM (Verifiable Credential API +Lifecycle Management) HTTP API, based on the +[W3C VC API](https://w3c-ccg.github.io/vc-api/) and +[W3C VC Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/). + +Use it to develop against VCALM APIs without a real implementation, or as a +learning tool for understanding the full VC lifecycle. + +## Installation + +```sh +npm install +``` + +## Quick Start + +```sh +node src/index.js +``` + +The server listens on port 3000 by default. Set `PORT` to override. + +## Usage + +### Issue a credential + +```sh +curl -X POST http://localhost:3000/credentials/issue \ + -H 'Content-Type: application/json' \ + -d '{ + "credential": { + "@context": ["https://www.w3.org/ns/credentials/v2"], + "type": ["VerifiableCredential"], + "issuer": "did:example:alice", + "validFrom": "2024-01-01T00:00:00Z", + "credentialSubject": {"id": "did:example:bob", "name": "Bob"} + } + }' +``` + +### Verify a credential + +```sh +curl -X POST http://localhost:3000/credentials/verify \ + -H 'Content-Type: application/json' \ + -d '{"verifiableCredential": }' +``` + +### Derive (selective disclosure) + +```sh +curl -X POST http://localhost:3000/credentials/derive \ + -H 'Content-Type: application/json' \ + -d '{ + "verifiableCredential": , + "options": { + "selectivePointers": ["/@context", "/type", "/issuer", "/validFrom"] + } + }' +``` + +## Endpoints + +### Credentials + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/credentials/issue` | Issue a credential with a mock proof | +| POST | `/credentials/verify` | Verify a credential | +| POST | `/credentials/derive` | Selective disclosure derivation | +| POST | `/credentials/status` | Update a credential's status | +| GET | `/credentials/:id` | Retrieve a stored credential | +| DELETE | `/credentials/:id` | Soft-delete a credential | + +### Presentations + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/presentations` | Create a presentation | +| POST | `/presentations/verify` | Verify a presentation | +| GET | `/presentations` | List stored presentations | +| GET | `/presentations/:id` | Retrieve a presentation | +| DELETE | `/presentations/:id` | Soft-delete a presentation | + +### Challenges & Status Lists + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/challenges` | Issue a single-use nonce | +| POST | `/status-lists` | Create a StatusList2021 credential | +| GET | `/status-lists/:id` | Retrieve a status list (public, no auth) | + +### Workflows & Exchanges + +Exchanges are stateful. State transitions: `pending → active → complete` (or +`invalid` on error). See [Design Notes](#design-notes) for the full state +machine. + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/workflows` | Create a workflow | +| GET | `/workflows/:id` | Retrieve a workflow | +| POST | `/workflows/:id/exchanges` | Create an exchange | +| GET | `/workflows/:id/exchanges/:id` | Get exchange state | +| POST | `/workflows/:id/exchanges/:id` | Participate in an exchange | +| GET | `/workflows/:id/exchanges/:id/protocols` | Get supported protocol URLs | + +### Interactions + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/interactions/:id?iuv=1` | Get interaction protocols (`iuv=1` required) | + +## Options + +**`PORT`** — TCP port to listen on (default: `3000`) + +## Running Tests + +```sh +npm test # unit + integration + conformance tests +npm run test:conformance # conformance suite only (supports VCALM_BASE_URL) +npm run lint # lint src and test +npm run typecheck # JSDoc type checking via tsc +``` + +To run the conformance suite against a real VCALM server: + +```sh +VCALM_BASE_URL=https://your-server.example npm run test:conformance +``` + +The full OpenAPI 3.0 spec is at [`spec/oas.yaml`](spec/oas.yaml). + +## Design Notes + +- **No real cryptography.** Proofs are deterministic SHA-256 hashes prefixed with `mock-proof-`. Verification checks the prefix — sufficient for round-trip tests, not for production use. +- **In-memory store.** All state is lost on restart. +- **Single-use challenges.** Nonces issued via `POST /challenges` are consumed on first use at `/presentations/verify`. +- **`redirectUrl` is not supported.** Returns `400` per spec guidance that implementations should reject it. +- **Unsigned presentation `holder` field is ignored.** A warning is logged when `holder` is present but no presentation-level proof exists. + +## Contributing + +See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)! + +PRs accepted. + +If editing the Readme, please conform to the +[standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +[New BSD License (3-clause)](LICENSE) © Digital Bazaar diff --git a/USE_CASES.md b/USE_CASES.md new file mode 100644 index 0000000..188fb0a --- /dev/null +++ b/USE_CASES.md @@ -0,0 +1,148 @@ +# VCALM Use Cases + +A reference for real-world applications of the Verifiable Credential API Lifecycle Management (VCALM) protocol. Use this to guide mock server design, test fixture selection, and exchange flow implementation. + +--- + +## Core Concepts + +### Roles +- **Issuer** — signs and issues credentials (university, government agency, employer) +- **Holder** — stores credentials in a wallet and presents them +- **Verifier** — requests and verifies presentations + +The key paradigm shift from traditional identity: the issuer is **not in the loop at verification time**. The holder carries the credential; the verifier checks the cryptographic proof directly. + +### Single-Step vs. Multi-Step Exchanges +Most use cases are single-step: holder presents a credential, verifier accepts or rejects. Multi-step exchanges hold state across multiple round-trips, enabling progressive disclosure, step-up auth, and credential chaining. + +--- + +## Single-Step Use Cases + +### Digital Identity / Government IDs +Mobile driver's licenses, passport credentials. A holder presents to a bar, airport, or car rental without handing over a physical document. With BBS+ selective disclosure, the verifier receives only what they need (e.g., age verification) — not address, license number, or other claims. + +**Flow:** `POST /workflows/{id}/exchanges` → holder presents → verifier accepts + +--- + +### Employee Credentials +An employer issues a "works at Acme Corp" credential to the employee's DID. The employee presents it to a SaaS vendor for an enterprise discount, or to a background check service — without the vendor contacting HR directly. + +**Flow:** Single presentation, no credential issued in return + +--- + +### Education / Diplomas +A university issues a degree credential. A job applicant presents it directly to an employer. The employer verifies cryptographically — no transcript requests, no phone calls to the registrar. + +**Flow:** Single presentation against an employer's verification endpoint + +--- + +### Healthcare Records +A provider issues vaccination records or prescription credentials to a patient's wallet. The patient presents to any other provider. No faxing records between hospitals or manual release-of-information forms. + +**Flow:** Single presentation; verifier may return an access token or confirmation + +--- + +### KYC (Know Your Customer) +A bank verifies a user's identity once and issues a KYC credential. The user presents it to other fintechs to skip redundant verification — significant friction reduction in crypto and DeFi onboarding. + +**Flow:** Single presentation; verifier grants account access on success + +--- + +### Supply Chain Provenance +A factory issues a "certified organic" or "conflict-free materials" credential on a product batch. The credential travels with the product and any party in the supply chain can verify it independently. + +**Flow:** Single presentation at each handoff point + +--- + +## Multi-Step Use Cases + +Multi-step exchanges use the VCALM workflow/exchange protocol to hold state across multiple round-trips. The verifier does not front-load its requirements — each step reveals the minimum needed for that stage. + +--- + +### Step-Up Authentication +Progressive access based on what the holder can prove. + +1. Present a "verified human" credential → receive read access +2. Present an employee credential → receive write access +3. Present an MFA or hardware key credential → receive admin access + +Each step is only triggered when the user requests higher access. Most users never reach step 3, so their additional credentials are never disclosed unnecessarily. + +**VCALM endpoints:** `POST /workflows/{id}/exchanges/{exchangeId}` (repeated per step) + +--- + +### Credential Chaining (Present-to-Get) +Presenting one credential to receive another. + +1. Present a university diploma credential +2. Issuer validates and returns an "alumni discount eligible" credential +3. Holder presents the new credential to the actual service + +Common in government contexts: present a state ID → receive a federal benefits credential → present to a specific agency program. + +**VCALM endpoints:** Exchange returns a new VC in the response body at step 2 + +--- + +### Progressive Disclosure with BBS+ +Using selective disclosure across multiple steps within a single exchange. (For an overview of BBS+ and other cryptosuites, see [Cryptosuite Reference](#cryptosuite-reference) below.) + +1. Present minimal claims (over 18, US resident) +2. Verifier responds: insufficient, also need proof of employment +3. Holder presents employment credential in the same exchange + +The exchange retains state so the verifier can reason across all presented credentials together. BBS+ ensures each step reveals only the claims required for that step. + +**VCALM endpoints:** `POST /presentations/derive` to generate each selective disclosure proof + +--- + +### KYC Tiering +Incremental account upgrades driven by the holder's needs, not upfront demands. + +1. Present basic identity → open a low-limit account +2. Present proof of address + income → upgrade account limits +3. Present accredited investor credential → unlock investment products + +Each step is triggered when the user wants more access. Privacy is preserved because the holder never discloses more than necessary for their current goal. + +**VCALM endpoints:** Each tier upgrade initiates a new exchange on the same workflow + +--- + +## Cryptosuite Reference + +| Suite | Key Type | Selective Disclosure | Notes | +|---|---|---|---| +| `Ed25519Signature2020` | Ed25519 | No | Most common; fast, small signatures | +| `eddsa-rdfc-2022` | Ed25519 / Ed448 | No | Newer RDF canonicalization variant | +| `ecdsa-rdfc-2019` | P-256 / P-384 | No | Required for some government/enterprise contexts | +| `ecdsa-sd-2023` | P-256 | Yes (linkable) | ECDSA-based selective disclosure via JSON Pointers; requires `/presentations/derive` | +| `bbs-2023` | BBS+ (BLS12-381) | Yes (unlinkable) | Unlinkable selective disclosure; required for privacy-preserving `/presentations/derive` | + +> **Important:** Both `ecdsa-sd-2023` and `bbs-2023` support selective disclosure and require `POST /presentations/derive` to generate a derived (holder) proof. The key difference is **linkability**: `ecdsa-sd-2023` derived proofs are linkable (each disclosure uses the same base signature and public key, so multiple presentations of the same credential can be correlated), while `bbs-2023` derived proofs are unlinkable (each presentation is cryptographically independent). Use `ecdsa-sd-2023` when you need ECDSA compatibility (e.g., government or enterprise environments that mandate P-256 keys) and linkability is acceptable. Use `bbs-2023` when holder privacy and unlinkability are the primary concern. + +### How `ecdsa-sd-2023` Selective Disclosure Works + +`ecdsa-sd-2023` implements selective disclosure using a two-phase proof model defined in the [W3C VC Data Integrity ECDSA specification](https://w3c.github.io/vc-di-ecdsa/#selective-disclosure-functions): + +1. **Base proof (issuer → holder):** The issuer signs the credential with a long-term key and also generates a per-proof ephemeral P-256 key pair. The issuer signs each individual statement (N-Quad) in the credential with the ephemeral private key, producing a separate signature per statement. Mandatory claims (specified via JSON Pointers) are hashed together; the rest remain individually signed but undisclosed. The base proof is given only to the holder — it is never sent to a verifier. + +2. **Derived proof (holder → verifier):** The holder calls `POST /presentations/derive`, supplying the base proof and a set of JSON Pointers indicating which additional (non-mandatory) claims to reveal. The derived proof includes only the signatures for the selected statements, a label map (mapping canonical blank node IDs to HMAC-randomized IDs), and the mandatory claim indexes. The verifier receives only the revealed claims and can independently verify each statement's signature and the mandatory claim hash. + +**Key technical properties:** +- Uses **HMAC-based blank node relabeling** to prevent information leakage from blank node ordering +- Mandatory claims are always disclosed; selective claims are opt-in by the holder +- Proof values are CBOR-encoded and Multibase (base64url-no-pad) encoded +- Canonicalization uses RDF Dataset Canonicalization (RDFC-1.0) with SHA-256 +- Does **not** provide unlinkable disclosure — see `bbs-2023` if unlinkability is required diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9df2668 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,33 @@ +import eslintConfig from '@digitalbazaar/eslint-config'; + +export default [ + ...eslintConfig, + { + languageOptions: { + globals: { + // Node.js globals + process: 'readonly', + console: 'readonly', + crypto: 'readonly' + } + }, + rules: { + 'no-console': 'off', + '@stylistic/max-len': ['error', {code: 80, ignoreComments: true}] + } + }, + { + // Test files — add mocha globals + files: ['test/**/*.js'], + languageOptions: { + globals: { + describe: 'readonly', + it: 'readonly', + before: 'readonly', + after: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly' + } + } + } +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..743570a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4308 @@ +{ + "name": "vc-api-mock", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vc-api-mock", + "version": "1.0.0", + "license": "W3C-20150513", + "dependencies": { + "express": "^5.2.1" + }, + "devDependencies": { + "@digitalbazaar/eslint-config": "^8.0.1", + "@readme/openapi-parser": "^6.1.1", + "@types/chai": "^5.2.3", + "@types/express": "^5.0.6", + "@types/mocha": "^10.0.10", + "@types/node": "^25.8.0", + "@types/sinon": "^21.0.1", + "@types/supertest": "^7.2.0", + "chai": "^6.2.2", + "eslint": "^9.39.4", + "mocha": "^11.7.5", + "sinon": "^22.0.0", + "supertest": "^7.2.2", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", + "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@digitalbazaar/eslint-config": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/eslint-config/-/eslint-config-8.0.1.tgz", + "integrity": "sha512-93WXI6gBgvGTNWZB7xr1a4LyOnaluTA/tDOZtz34fNnNgGDY0znvwp3xXyw9TJJTcgVv66y71e2a3t3F90f/Tg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@eslint/js": "^9.39.2", + "@stylistic/eslint-plugin": "^5.7.1", + "eslint-plugin-jsdoc": "^62.5.3", + "eslint-plugin-unicorn": "^62.0.0", + "eslint-plugin-vue": "^10.7.0", + "globals": "^17.3.0", + "vue-eslint-parser": "^10.2.0" + }, + "peerDependencies": { + "eslint": "^9.39.2" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", + "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.58.0", + "comment-parser": "1.4.6", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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/@readme/better-ajv-errors": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.4.0.tgz", + "integrity": "sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@humanwhocodes/momoa": "^2.0.3", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/@readme/openapi-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@readme/openapi-parser/-/openapi-parser-6.1.1.tgz", + "integrity": "sha512-sfCU2LldkkPp2ThlGaTgw0TJCerUd2TF5k1s+jE9KQfuV8DTcroodSNeyy3CzBKtLvDJwf1QV6L1inJBfKRJjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^14.1.1", + "@readme/better-ajv-errors": "^2.3.2", + "@readme/openapi-schemas": "^3.1.0", + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-draft-04": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@readme/openapi-parser/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@readme/openapi-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@readme/openapi-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@readme/openapi-schemas": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz", + "integrity": "sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sinon": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", + "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/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/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/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.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/builtin-modules": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.2.0.tgz", + "integrity": "sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "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/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/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/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/comment-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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/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/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "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/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": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "62.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", + "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.86.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.6", + "debug": "^4.4.3", + "escape-string-regexp": "^4.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "62.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-62.0.0.tgz", + "integrity": "sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "@eslint-community/eslint-utils": "^4.9.0", + "@eslint/plugin-kit": "^0.4.0", + "change-case": "^5.4.4", + "ci-info": "^4.3.1", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.46.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.4.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.13.0", + "semver": "^7.7.3", + "strip-indent": "^4.1.1" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.1.tgz", + "integrity": "sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "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-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "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/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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": "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": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "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/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/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-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/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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "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/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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "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/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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/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/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/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/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": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-deep-merge": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.0.tgz", + "integrity": "sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "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-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-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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/string-width-cjs/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/string-width/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/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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/strip-ansi-cjs": { + "name": "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/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "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", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/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/yargs/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e86ea89 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "vc-api-mock", + "version": "1.0.0", + "description": "Mock VCALM server and test suite for the W3C Verifiable Credential API Lifecycle Management spec", + "type": "module", + "main": "src/server.js", + "scripts": { + "start": "node src/index.js", + "test": "mocha --recursive 'test/**/*.test.js'", + "test:conformance": "mocha --no-config 'test/conformance/conformance.test.js'", + "typecheck": "tsc --noEmit", + "lint": "eslint src test" + }, + "keywords": [ + "vcalm", + "verifiable-credentials", + "vc-api", + "mock", + "w3c" + ], + "author": "", + "license": "W3C-20150513", + "engines": { + "node": ">=18" + }, + "dependencies": { + "express": "^5.2.1" + }, + "devDependencies": { + "@digitalbazaar/eslint-config": "^8.0.1", + "@readme/openapi-parser": "^6.1.1", + "@types/chai": "^5.2.3", + "@types/express": "^5.0.6", + "@types/mocha": "^10.0.10", + "@types/node": "^25.8.0", + "@types/sinon": "^21.0.1", + "@types/supertest": "^7.2.0", + "chai": "^6.2.2", + "eslint": "^9.39.4", + "mocha": "^11.7.5", + "sinon": "^22.0.0", + "supertest": "^7.2.2", + "typescript": "^6.0.3" + } +} diff --git a/spec/oas.yaml b/spec/oas.yaml new file mode 100644 index 0000000..a7daba7 --- /dev/null +++ b/spec/oas.yaml @@ -0,0 +1,856 @@ +openapi: 3.0.3 +info: + title: VCALM Mock API + description: > + Mock implementation of the Verifiable Credential API Lifecycle Management + (VCALM) HTTP API. This spec documents the contract implemented by this + mock server. It is derived from the VCALM work-in-progress specification + at Digital Bazaar and the W3C VC API community group. + version: 1.0.0 + +tags: + - name: credentials + description: Credential issuance, verification, derivation, and management + - name: presentations + description: Presentation creation and verification + - name: status + description: Credential status and status list management + - name: challenges + description: Single-use challenge nonces for replay protection + - name: workflows + description: Workflow and exchange lifecycle management + - name: interactions + description: Interaction protocol discovery + +paths: + + # --------------------------------------------------------------------------- + # Credentials + # --------------------------------------------------------------------------- + + /credentials/issue: + post: + tags: [credentials] + summary: Issue a verifiable credential + description: > + Adds a mock proof to the provided credential and stores it. + The stored mandatory pointers are used later by /credentials/derive. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCredentialRequest' + responses: + '201': + description: Credential issued successfully + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiableCredential' + '400': + $ref: '#/components/responses/InvalidInput' + + /credentials/verify: + post: + tags: [credentials] + summary: Verify a verifiable credential + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyCredentialRequest' + responses: + '200': + description: Verification result (200 even when verified:false) + content: + application/json: + schema: + $ref: '#/components/schemas/VerificationResult' + '400': + $ref: '#/components/responses/InvalidInput' + + /credentials/derive: + post: + tags: [credentials] + summary: Derive a credential with selective disclosure + description: > + Returns a derived credential containing only the requested + selectivePointers. All spec-level mandatory pointers (@context, type, + issuer, validFrom) and any issuer-specified mandatoryPointers must be + included in selectivePointers or a 400 is returned. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeriveCredentialRequest' + responses: + '201': + description: Derived credential + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiableCredential' + '400': + $ref: '#/components/responses/InvalidInput' + + /credentials/status: + post: + tags: [credentials, status] + summary: Update a credential's status + description: > + Attaches or updates the credentialStatus field on a stored credential. + The credential must have been previously issued via /credentials/issue. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateStatusRequest' + responses: + '200': + description: Status updated successfully + content: + application/json: + schema: + type: object + '400': + $ref: '#/components/responses/InvalidInput' + '404': + $ref: '#/components/responses/NotFound' + + /credentials/{id}: + get: + tags: [credentials] + summary: Retrieve a stored credential by ID + parameters: + - $ref: '#/components/parameters/credentialId' + responses: + '200': + description: The verifiable credential + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiableCredential' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: [credentials] + summary: Soft-delete a credential + parameters: + - $ref: '#/components/parameters/credentialId' + responses: + '202': + description: Credential accepted for deletion + '404': + $ref: '#/components/responses/NotFound' + + # --------------------------------------------------------------------------- + # Presentations + # --------------------------------------------------------------------------- + + /presentations: + get: + tags: [presentations] + summary: List stored presentations + parameters: + - name: type + in: query + schema: + type: string + enum: [presentations, verifiablepresentations] + responses: + '200': + description: Array of presentations + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VerifiablePresentation' + post: + tags: [presentations] + summary: Create a verifiable presentation + description: > + Adds a mock proof to the provided presentation. If options.challenge + or options.domain are provided they are embedded in the proof for + later verification. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePresentationRequest' + responses: + '201': + description: Presentation created + content: + application/json: + schema: + type: object + required: [verifiablePresentation] + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + '400': + $ref: '#/components/responses/InvalidInput' + + /presentations/verify: + post: + tags: [presentations] + summary: Verify a presentation + description: > + Returns 200 in all cases — the body contains verified:true/false. + If options.challenge is provided it must match the proof and is + consumed (single-use). If the presentation has a holder field but + no proof, a warning is logged and verification proceeds on enclosed + credentials only. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyPresentationRequest' + responses: + '200': + description: Verification result + content: + application/json: + schema: + $ref: '#/components/schemas/VerificationResult' + '400': + $ref: '#/components/responses/InvalidInput' + '413': + description: Payload too large + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '429': + description: Too many requests + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + + /presentations/{id}: + get: + tags: [presentations] + summary: Retrieve a stored presentation + parameters: + - $ref: '#/components/parameters/presentationId' + responses: + '200': + description: The presentation + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiablePresentation' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: [presentations] + summary: Soft-delete a presentation + parameters: + - $ref: '#/components/parameters/presentationId' + responses: + '202': + description: Accepted for deletion + '404': + $ref: '#/components/responses/NotFound' + + # --------------------------------------------------------------------------- + # Challenges + # --------------------------------------------------------------------------- + + /challenges: + post: + tags: [challenges] + summary: Issue a single-use challenge nonce + description: > + Returns a UUID nonce with a 5-minute TTL. The nonce is consumed on + first use at /presentations/verify. Reuse returns 400. + requestBody: + required: false + content: + application/json: + schema: + type: object + responses: + '201': + description: Challenge issued + content: + application/json: + schema: + type: object + required: [challenge] + properties: + challenge: + type: string + description: Single-use nonce + + # --------------------------------------------------------------------------- + # Status lists + # --------------------------------------------------------------------------- + + /status-lists: + post: + tags: [status] + summary: Create a StatusList2021 credential + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateStatusListRequest' + responses: + '201': + description: Status list created + headers: + Location: + description: URL of the created status list + schema: + type: string + content: + application/json: + schema: + type: object + required: [id, verifiableCredential] + properties: + id: + type: string + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + '400': + $ref: '#/components/responses/InvalidInput' + + /status-lists/{id}: + get: + tags: [status] + summary: Retrieve a status list credential (public, no auth) + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: The status list credential + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiableCredential' + '404': + $ref: '#/components/responses/NotFound' + + # --------------------------------------------------------------------------- + # Workflows + # --------------------------------------------------------------------------- + + /workflows: + post: + tags: [workflows] + summary: Create a workflow + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowConfig' + responses: + '201': + description: Workflow created + headers: + Location: + description: URL of the created workflow + schema: + type: string + '400': + $ref: '#/components/responses/InvalidInput' + + /workflows/{localWorkflowId}: + get: + tags: [workflows] + summary: Retrieve a workflow configuration + parameters: + - $ref: '#/components/parameters/localWorkflowId' + responses: + '200': + description: Workflow configuration + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowConfig' + '404': + $ref: '#/components/responses/NotFound' + + /workflows/{localWorkflowId}/exchanges: + post: + tags: [workflows] + summary: Create an exchange within a workflow + parameters: + - $ref: '#/components/parameters/localWorkflowId' + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + expires: + type: string + format: date-time + variables: + type: object + responses: + '201': + description: Exchange created + headers: + Location: + description: URL of the created exchange + schema: + type: string + '404': + $ref: '#/components/responses/NotFound' + + /workflows/{localWorkflowId}/exchanges/{localExchangeId}: + get: + tags: [workflows] + summary: Get exchange state + parameters: + - $ref: '#/components/parameters/localWorkflowId' + - $ref: '#/components/parameters/localExchangeId' + responses: + '200': + description: Exchange state + content: + application/json: + schema: + $ref: '#/components/schemas/ExchangeState' + '404': + $ref: '#/components/responses/NotFound' + post: + tags: [workflows] + summary: Participate in an exchange + description: > + The state machine entry point. Client sends one of: + - Empty object → server returns VPR for current step + - verifiablePresentation → server validates, advances step, returns + next VPR, an issued VC, or empty body (complete) + - redirectUrl → always rejected with 400 + + Exchange state transitions: pending → active → complete | invalid + parameters: + - $ref: '#/components/parameters/localWorkflowId' + - $ref: '#/components/parameters/localExchangeId' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/ExchangeMessage' + responses: + '200': + description: Exchange response + content: + application/json: + schema: + $ref: '#/components/schemas/ExchangeResponse' + '400': + $ref: '#/components/responses/InvalidInput' + '404': + $ref: '#/components/responses/NotFound' + + /workflows/{localWorkflowId}/exchanges/{localExchangeId}/protocols: + get: + tags: [workflows] + summary: Get supported protocol URLs for an exchange + parameters: + - $ref: '#/components/parameters/localWorkflowId' + - $ref: '#/components/parameters/localExchangeId' + responses: + '200': + description: Protocol URLs + content: + application/json: + schema: + type: object + required: [protocols] + properties: + protocols: + type: object + properties: + vcapi: + type: string + format: uri + '404': + $ref: '#/components/responses/NotFound' + + # --------------------------------------------------------------------------- + # Interactions + # --------------------------------------------------------------------------- + + /interactions/{interactionId}: + get: + tags: [interactions] + summary: Get interaction protocols + parameters: + - name: interactionId + in: path + required: true + schema: + type: string + - name: iuv + in: query + required: true + schema: + type: string + enum: ['1'] + description: Interaction URI version — must be '1' + responses: + '200': + description: Protocols object + content: + application/json: + schema: + type: object + required: [protocols] + properties: + protocols: + type: object + properties: + vcapi: + type: string + format: uri + '400': + $ref: '#/components/responses/InvalidInput' + +# --------------------------------------------------------------------------- +# Components +# --------------------------------------------------------------------------- + +components: + + parameters: + credentialId: + name: id + in: path + required: true + schema: + type: string + presentationId: + name: id + in: path + required: true + schema: + type: string + localWorkflowId: + name: localWorkflowId + in: path + required: true + schema: + type: string + localExchangeId: + name: localExchangeId + in: path + required: true + schema: + type: string + + responses: + InvalidInput: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + + schemas: + + Proof: + type: object + required: [type] + properties: + type: + type: string + example: DataIntegrityProof + cryptosuite: + type: string + example: eddsa-rdfc-2022 + verificationMethod: + type: string + created: + type: string + format: date-time + proofPurpose: + type: string + proofValue: + type: string + challenge: + type: string + domain: + type: string + + VerifiableCredential: + type: object + required: [type, issuer] + properties: + '@context': + type: array + items: + type: string + id: + type: string + type: + oneOf: + - type: string + - type: array + items: + type: string + issuer: + oneOf: + - type: string + - type: object + validFrom: + type: string + format: date-time + issued: + type: string + format: date-time + credentialSubject: + type: object + credentialStatus: + type: object + proof: + $ref: '#/components/schemas/Proof' + + VerifiablePresentation: + type: object + required: [type] + properties: + '@context': + type: array + items: + type: string + id: + type: string + type: + oneOf: + - type: string + - type: array + items: + type: string + holder: + type: string + verifiableCredential: + type: array + items: + $ref: '#/components/schemas/VerifiableCredential' + proof: + $ref: '#/components/schemas/Proof' + + ProblemDetails: + type: object + required: [type, title, status, detail] + properties: + type: + type: string + format: uri + example: https://vcalm.example/errors#invalid-input + title: + type: string + status: + type: integer + detail: + type: string + + VerificationResult: + type: object + required: [verified] + properties: + verified: + type: boolean + results: + type: array + items: + type: object + + IssueCredentialRequest: + type: object + required: [credential] + properties: + credential: + $ref: '#/components/schemas/VerifiableCredential' + options: + type: object + properties: + credentialId: + type: string + mandatoryPointers: + type: array + items: + type: string + + VerifyCredentialRequest: + type: object + required: [verifiableCredential] + properties: + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + + DeriveCredentialRequest: + type: object + required: [verifiableCredential] + properties: + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + options: + type: object + properties: + credentialId: + type: string + selectivePointers: + type: array + items: + type: string + + UpdateStatusRequest: + type: object + required: [credentialId, credentialStatus] + properties: + credentialId: + type: string + credentialStatus: + type: object + required: [type] + properties: + type: + type: string + statusPurpose: + type: string + statusListIndex: + type: string + statusListCredential: + type: string + + CreatePresentationRequest: + type: object + required: [presentation] + properties: + presentation: + $ref: '#/components/schemas/VerifiablePresentation' + options: + type: object + properties: + challenge: + type: string + domain: + type: string + + VerifyPresentationRequest: + type: object + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + presentation: + $ref: '#/components/schemas/VerifiablePresentation' + options: + type: object + properties: + challenge: + type: string + domain: + type: string + + CreateStatusListRequest: + type: object + required: [statusPurpose] + properties: + statusPurpose: + type: string + example: revocation + id: + type: string + format: uri + options: + type: object + + WorkflowStep: + type: object + properties: + verifiablePresentationRequest: + type: object + issueRequests: + type: array + items: + type: string + nextStep: + type: string + + WorkflowConfig: + type: object + required: [initialStep, steps] + properties: + id: + type: string + initialStep: + type: string + steps: + type: object + additionalProperties: + $ref: '#/components/schemas/WorkflowStep' + credentialTemplates: + type: array + items: + type: object + + ExchangeState: + type: object + required: [id, workflowId, sequence, step, state] + properties: + id: + type: string + workflowId: + type: string + sequence: + type: integer + step: + type: string + state: + type: string + enum: [pending, active, complete, invalid] + expires: + type: string + nullable: true + variables: + type: object + lastError: + type: object + nullable: true + + ExchangeMessage: + type: object + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + redirectUrl: + type: string + description: > + Not supported. Sending this field always returns 400. + + ExchangeResponse: + type: object + properties: + verifiablePresentationRequest: + type: object + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..70bd84f --- /dev/null +++ b/src/index.js @@ -0,0 +1,12 @@ +import {createApp} from './server.js'; + +process.on('unhandledRejection', reason => { + console.error('Unhandled Rejection:', reason); + process.exit(1); +}); + +const PORT = process.env.PORT || 3000; +const app = createApp(); +app.listen(PORT, () => { + console.log(`vc-api-mock server running on port ${PORT}`); +}); diff --git a/src/middleware/problemDetails.js b/src/middleware/problemDetails.js new file mode 100644 index 0000000..3a25005 --- /dev/null +++ b/src/middleware/problemDetails.js @@ -0,0 +1,43 @@ +const BASE_URL = 'https://vcalm.example/errors#'; + +/** + * @typedef {object} ProblemDetails + * @property {string} type + * @property {string} title + * @property {number} status + * @property {string} detail + */ + +/** + * Creates a RFC 9457 ProblemDetails object. + * + * @param {string} slug - Error slug appended to base URL + * @param {string} title - Short human-readable title + * @param {number} status - HTTP status code + * @param {string} detail - Specific human-readable detail message + * @returns {ProblemDetails} + */ +export function problemDetails(slug, title, status, detail) { + return {type: `${BASE_URL}${slug}`, title, status, detail}; +} + +/** + * Express error handler middleware that formats errors as ProblemDetails. + * + * @param {ProblemDetails & Error} err + * @param {import('express').Request} _req + * @param {import('express').Response} res + * @returns {void} + */ +export function errorHandler(err, _req, res) { + const status = err.status || 500; + if(err.type?.startsWith(BASE_URL)) { + res.status(status).json(err); + return; + } + res.status(status).json( + problemDetails( + 'internal-error', 'Internal Server Error', status, err.message + ) + ); +} diff --git a/src/routes/credentials.js b/src/routes/credentials.js new file mode 100644 index 0000000..8444fb0 --- /dev/null +++ b/src/routes/credentials.js @@ -0,0 +1,162 @@ +import {generateProof, verifyProof} from '../utils/proof.js'; +import {computeMissing} from '../utils/pointers.js'; +import {problemDetails} from '../middleware/problemDetails.js'; +import {randomUUID} from 'node:crypto'; +import {Router} from 'express'; + +/** @typedef {import('../types.js').VerifiableCredential} VerifiableCredential */ + +/** + * @param {import('../store/index.js').createStore extends + * (...args: any[]) => infer R ? R : never} store + * @returns {Router} + */ +export function credentialsRouter(store) { + const router = Router(); + + // POST /credentials/issue + router.post('/issue', (req, res) => { + const {credential, options = {}} = req.body ?? {}; + if(!credential) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a credential property.') + ); + } + + const credentialId = + options.credentialId ?? credential.id ?? randomUUID(); + const mandatoryPointers = options.mandatoryPointers ?? []; + const vc = {...credential, proof: generateProof(credential)}; + + store.credentials.set( + credentialId, {vc, mandatoryPointers, deleted: false} + ); + return res.status(201).json(vc); + }); + + // POST /credentials/verify + router.post('/verify', (req, res) => { + const {verifiableCredential} = req.body ?? {}; + if(!verifiableCredential) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a verifiableCredential property.') + ); + } + const verified = verifyProof(verifiableCredential); + return res.status(200).json({verified, results: []}); + }); + + // POST /credentials/derive + router.post('/derive', (req, res) => { + const {verifiableCredential, options = {}} = req.body ?? {}; + if(!verifiableCredential) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a verifiableCredential property.') + ); + } + + // Find stored mandatory pointers — check options.credentialId, then + // credential.id, then fall back to scanning the store for a VC whose + // content matches + const vc = + /** @type {VerifiableCredential} */ (verifiableCredential); + const lookupId = options.credentialId ?? vc.id; + let entry = lookupId ? store.credentials.get(lookupId) : null; + if(!entry) { + // Scan store for matching VC by proof value (deterministic hash) + const proofValue = vc.proof?.proofValue; + if(proofValue) { + for(const e of store.credentials.values()) { + if(e.vc.proof?.proofValue === proofValue) { + entry = e; + break; + } + } + } + } + const storedMandatory = entry?.mandatoryPointers ?? []; + + const selectivePointers = options.selectivePointers ?? []; + const missing = computeMissing(storedMandatory, selectivePointers); + + if(missing.length > 0) { + return res.status(400).json( + problemDetails( + 'missing-mandatory-pointers', 'Missing Mandatory Pointers', 400, + `selectivePointers must include all mandatoryPointers: ` + + `${missing.join(', ')}`) + ); + } + + // Build derived credential with only selective fields + proof + const vcRecord = + /** @type {VerifiableCredential} */ (verifiableCredential); + const derived = /** @type {VerifiableCredential} */ (Object.fromEntries( + selectivePointers + .map((/** @type {string} */ pointer) => { + const key = pointer.replace(/^\//, ''); + return [key, vcRecord[key]]; + }) + .filter((/** @type {[string, unknown]} */ [, v]) => v !== undefined) + )); + derived.proof = generateProof(derived); + return res.status(201).json(derived); + }); + + // POST /credentials/status — update a credential's status entry + router.post('/status', (req, res) => { + const {credentialId, credentialStatus} = req.body ?? {}; + if(!credentialId || !credentialStatus) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include credentialId and credentialStatus.') + ); + } + const entry = store.credentials.get(credentialId); + if(!entry || entry.deleted) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Credential ${credentialId} not found.`) + ); + } + entry.vc.credentialStatus = credentialStatus; + return res.status(200).json({}); + }); + + // GET /credentials/:id + router.get('/:id', (req, res) => { + const entry = store.credentials.get(req.params.id); + if(!entry || entry.deleted) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Credential ${req.params.id} not found.`) + ); + } + return res.status(200).json(entry.vc); + }); + + // DELETE /credentials/:id + router.delete('/:id', (req, res) => { + const entry = store.credentials.get(req.params.id); + if(!entry || entry.deleted) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Credential ${req.params.id} not found.`) + ); + } + entry.deleted = true; + return res.status(202).send(); + }); + + return router; +} diff --git a/src/routes/interactions.js b/src/routes/interactions.js new file mode 100644 index 0000000..e8af0f8 --- /dev/null +++ b/src/routes/interactions.js @@ -0,0 +1,56 @@ +import {randomUUID} from 'node:crypto'; + +import {problemDetails} from '../middleware/problemDetails.js'; +import {Router} from 'express'; + +/** + * @param {import('../store/index.js').createStore extends + * (...args: any[]) => infer R ? R : never} store + * @returns {Router} + */ +export function interactionsRouter(store) { + const router = Router(); + + // POST /challenges + router.post('/challenges', (_req, res) => { + const nonce = randomUUID(); + const TTL_MS = 5 * 60 * 1000; // 5 minutes + store.challenges.set(nonce, {expires: Date.now() + TTL_MS}); + return res.status(201).json({challenge: nonce}); + }); + + // GET /interactions/:interactionId + router.get('/interactions/:interactionId', (req, res) => { + const {iuv} = req.query; + if(iuv !== '1') { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Query parameter iuv=1 is required.') + ); + } + const base = `${req.protocol}://${req.get('host')}`; + return res.status(200).json({ + protocols: { + vcapi: + `${base}/workflows/default/exchanges/` + + `${req.params.interactionId}` + } + }); + }); + + // POST /:inviteId/invite-request/response + router.post('/:inviteId/invite-request/response', (req, res) => { + const {url, purpose, referenceId} = req.body ?? {}; + if(!url || !purpose || !referenceId) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include url, purpose, and referenceId.') + ); + } + return res.status(200).send(); + }); + + return router; +} diff --git a/src/routes/presentations.js b/src/routes/presentations.js new file mode 100644 index 0000000..5e3d1cc --- /dev/null +++ b/src/routes/presentations.js @@ -0,0 +1,177 @@ +import {generateProof, verifyProof} from '../utils/proof.js'; +import {problemDetails} from '../middleware/problemDetails.js'; +import {randomUUID} from 'node:crypto'; +import {Router} from 'express'; + +/** + * @param {import('../store/index.js').createStore extends + * (...args: any[]) => infer R ? R : never} store + * @returns {Router} + */ +export function presentationsRouter(store) { + const router = Router(); + + // POST /presentations/verify + router.post('/verify', (req, res) => { + const {verifiablePresentation, presentation, options = {}} = + req.body ?? {}; + + // Proofless verify — just check enclosed credentials + if(presentation && !verifiablePresentation) { + const credentials = presentation.verifiableCredential ?? []; + const results = credentials.map( + (/** @type {import('../types.js').VerifiableCredential} */ vc) => + ({verified: verifyProof(vc)}) + ); + const verified = results.every( + (/** @type {{verified: boolean}} */ r) => r.verified + ); + return res.status(200).json({verified, results}); + } + + if(!verifiablePresentation) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a verifiablePresentation or ' + + 'presentation property.') + ); + } + + // Warn if holder is present but no proof + if(verifiablePresentation.holder && !verifiablePresentation.proof) { + console.warn( + 'holder field present but ignored — no presentation proof' + ); + } + + // Check challenge/domain binding + const {challenge, domain} = options; + + // Enforce single-use challenge + if(challenge) { + const stored = store.challenges.get(challenge); + if(!stored) { + return res.status(400).json( + problemDetails( + 'challenge-invalid', 'Invalid Challenge', 400, + 'The challenge is unknown or has already been used.') + ); + } + if(stored.expires < Date.now()) { + store.challenges.delete(challenge); + return res.status(400).json( + problemDetails( + 'challenge-expired', 'Challenge Expired', 400, + 'The challenge has expired.') + ); + } + store.challenges.delete(challenge); + } + + if(verifiablePresentation.proof) { + if(challenge && verifiablePresentation.proof.challenge !== challenge) { + return res.status(400).json( + problemDetails( + 'challenge-mismatch', 'Challenge Mismatch', 400, + 'The challenge in the presentation proof does not match ' + + 'the expected challenge.') + ); + } + if(domain && verifiablePresentation.proof.domain !== domain) { + return res.status(400).json( + problemDetails( + 'challenge-mismatch', 'Domain Mismatch', 400, + 'The domain in the presentation proof does not match ' + + 'the expected domain.') + ); + } + } + + const presentationVerified = verifiablePresentation.proof ? + verifyProof(verifiablePresentation) : + true; + + const credentials = verifiablePresentation.verifiableCredential ?? []; + const credResults = credentials.map( + (/** @type {import('../types.js').VerifiableCredential} */ vc) => + ({verified: verifyProof(vc)}) + ); + const verified = presentationVerified && + credResults.every( + (/** @type {{verified: boolean}} */ r) => r.verified + ); + + return res.status(200).json({verified, results: credResults}); + }); + + // POST /presentations (createPresentation) + router.post('/', (req, res) => { + const {presentation, options = {}} = req.body ?? {}; + if(!presentation) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a presentation property.') + ); + } + + const proof = { + ...generateProof(presentation), + ...(options.challenge ? {challenge: options.challenge} : {}), + ...(options.domain ? {domain: options.domain} : {}) + }; + const vp = {...presentation, proof}; + const id = presentation.id ?? randomUUID(); + store.presentations.set(id, {vp, deleted: false}); + return res.status(201).json({verifiablePresentation: vp}); + }); + + // GET /presentations + router.get('/', (req, res) => { + const {type} = req.query; + const all = [...store.presentations.values()] + .filter(e => !e.deleted) + .map(e => e.vp); + if(type === 'presentations') { + return res.status(200).json( + all.filter(vp => !vp.proof) + ); + } + if(type === 'verifiablepresentations') { + return res.status(200).json( + all.filter(vp => vp.proof) + ); + } + return res.status(200).json(all); + }); + + // GET /presentations/:id + router.get('/:id', (req, res) => { + const entry = store.presentations.get(req.params.id); + if(!entry || entry.deleted) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Presentation ${req.params.id} not found.`) + ); + } + return res.status(200).json(entry.vp); + }); + + // DELETE /presentations/:id + router.delete('/:id', (req, res) => { + const entry = store.presentations.get(req.params.id); + if(!entry || entry.deleted) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Presentation ${req.params.id} not found.`) + ); + } + entry.deleted = true; + return res.status(202).send(); + }); + + return router; +} diff --git a/src/routes/status.js b/src/routes/status.js new file mode 100644 index 0000000..e89c155 --- /dev/null +++ b/src/routes/status.js @@ -0,0 +1,73 @@ +import {generateProof} from '../utils/proof.js'; +import {problemDetails} from '../middleware/problemDetails.js'; +import {randomUUID} from 'node:crypto'; +import {Router} from 'express'; + +/** @typedef {import('../types.js').StatusListCredential} StatusListCredential */ + +/** + * @param {import('../store/index.js').createStore extends + * (...args: any[]) => infer R ? R : never} store + * @returns {Router} + */ +export function statusRouter(store) { + const router = Router(); + + // POST /status-lists + router.post('/', (req, res) => { + const {statusPurpose, id, options = {}} = req.body ?? {}; + if(!statusPurpose) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Request body must include a statusPurpose property.') + ); + } + + const listId = + id ?? `https://vcalm.example/status-lists/${randomUUID()}`; + const statusListCredential = /** @type {StatusListCredential} */ ({ + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/vc/status-list/2021/v1' + ], + id: listId, + type: ['VerifiableCredential', 'StatusList2021Credential'], + issuer: 'did:example:mock', + issued: new Date().toISOString(), + credentialSubject: { + id: `${listId}#list`, + type: 'StatusList2021', + statusPurpose, + encodedList: + 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAA' + + 'AAAAAAAIC3AYbSVKsAQAAA', + ...options + } + }); + statusListCredential.proof = generateProof(statusListCredential); + store.statusLists.set(listId, statusListCredential); + + res.setHeader('Location', listId); + return res.status(201).json( + {id: listId, verifiableCredential: statusListCredential} + ); + }); + + // GET /status-lists/:id — intentionally public (no auth) + router.get('/:id', (req, res) => { + // Reconstruct the full ID from path since it may contain slashes + const id = decodeURIComponent(req.params.id); + const statusList = store.statusLists.get(id); + if(!statusList) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Status list ${id} not found.`) + ); + } + return res.status(200).json(statusList); + }); + + return router; +} diff --git a/src/routes/workflows.js b/src/routes/workflows.js new file mode 100644 index 0000000..902712c --- /dev/null +++ b/src/routes/workflows.js @@ -0,0 +1,215 @@ +import {generateProof} from '../utils/proof.js'; +import {problemDetails} from '../middleware/problemDetails.js'; +import {randomUUID} from 'node:crypto'; +import {Router} from 'express'; + +/** @typedef {import('../types.js').VerifiableCredential} VerifiableCredential */ +/** @typedef {import('../types.js').WorkflowConfig} WorkflowConfig */ + +/** + * @param {import('../store/index.js').createStore extends + * (...args: any[]) => infer R ? R : never} store + * @returns {Router} + */ +export function workflowsRouter(store) { + const router = Router(); + + // POST /workflows + router.post('/', (req, res) => { + const body = /** @type {WorkflowConfig} */ (req.body ?? {}); + if(!body.steps || !body.initialStep) { + return res.status(400).json( + problemDetails( + 'invalid-input', 'Invalid Input', 400, + 'Workflow must include steps and initialStep.') + ); + } + const workflowId = body.id ?? randomUUID(); + store.workflows.set(workflowId, {...body, id: workflowId}); + const location = + `${req.protocol}://${req.get('host')}/workflows/${workflowId}`; + res.setHeader('Location', location); + return res.status(201).send(); + }); + + // GET /workflows/:workflowId + router.get('/:workflowId', (req, res) => { + const workflow = store.workflows.get(req.params.workflowId); + if(!workflow) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Workflow ${req.params.workflowId} not found.`) + ); + } + return res.status(200).json(workflow); + }); + + // POST /workflows/:workflowId/exchanges + router.post('/:workflowId/exchanges', (req, res) => { + const workflow = store.workflows.get(req.params.workflowId); + if(!workflow) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Workflow ${req.params.workflowId} not found.`) + ); + } + const {expires, variables = {}} = req.body ?? {}; + const exchangeId = randomUUID(); + const key = `${req.params.workflowId}:${exchangeId}`; + store.exchanges.set(key, { + id: exchangeId, + workflowId: req.params.workflowId, + sequence: 0, + step: workflow.initialStep, + state: 'pending', + expires: expires ?? null, + variables, + lastError: null + }); + const location = + `${req.protocol}://${req.get('host')}` + + `/workflows/${req.params.workflowId}/exchanges/${exchangeId}`; + res.setHeader('Location', location); + return res.status(201).send(); + }); + + // GET /workflows/:workflowId/exchanges/:exchangeId + router.get('/:workflowId/exchanges/:exchangeId', (req, res) => { + const {workflowId, exchangeId} = req.params; + const exchange = store.exchanges.get(`${workflowId}:${exchangeId}`); + if(!exchange) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Exchange ${exchangeId} not found.`) + ); + } + return res.status(200).json(exchange); + }); + + // POST /workflows/:workflowId/exchanges/:exchangeId — participate + router.post('/:workflowId/exchanges/:exchangeId', (req, res) => { + const {workflowId, exchangeId} = req.params; + const key = `${workflowId}:${exchangeId}`; + const exchange = store.exchanges.get(key); + + if(!exchange) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Exchange ${exchangeId} not found.`) + ); + } + + // Check expiry + if(exchange.expires && new Date(exchange.expires) < new Date()) { + exchange.state = 'invalid'; + return res.status(400).json( + problemDetails( + 'exchange-expired', 'Exchange Expired', 400, + `Exchange ${exchangeId} has expired.`) + ); + } + + // Reject redirectUrl + if(req.body?.redirectUrl) { + return res.status(400).json( + problemDetails( + 'redirect-url-not-supported', 'Redirect URL Not Supported', 400, + 'redirectUrl is not supported by this implementation.') + ); + } + + const workflow = store.workflows.get(workflowId); + const stepConfig = workflow?.steps?.[exchange.step] ?? {}; + + exchange.sequence += 1; + exchange.state = 'active'; + + // Client sends a VP — advance step + if(req.body?.verifiablePresentation) { + const nextStep = stepConfig.nextStep; + + // Handle issueRequests in this step + if((stepConfig.issueRequests?.length ?? 0) > 0) { + const template = workflow?.credentialTemplates?.[0]; + if(template) { + const issuedVc = /** @type {VerifiableCredential} */ ({ + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:mock', + validFrom: new Date().toISOString(), + credentialSubject: {id: 'did:example:holder'} + }); + issuedVc.proof = generateProof(issuedVc); + if(nextStep) { + exchange.step = nextStep; + return res.status(200).json({verifiablePresentation: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'], + verifiableCredential: [issuedVc] + }}); + } + exchange.state = 'complete'; + return res.status(200).json({verifiablePresentation: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'], + verifiableCredential: [issuedVc] + }}); + } + } + + if(nextStep) { + exchange.step = nextStep; + const nextStepConfig = workflow?.steps?.[nextStep] ?? {}; + if(nextStepConfig.verifiablePresentationRequest) { + return res.status(200).json({ + verifiablePresentationRequest: + nextStepConfig.verifiablePresentationRequest + }); + } + } + + exchange.state = 'complete'; + return res.status(200).json({}); + } + + // Empty body or VPR — return current step's VPR + if(stepConfig.verifiablePresentationRequest) { + return res.status(200).json({ + verifiablePresentationRequest: + stepConfig.verifiablePresentationRequest + }); + } + + exchange.state = 'complete'; + return res.status(200).json({}); + }); + + // GET /workflows/:workflowId/exchanges/:exchangeId/protocols + router.get( + '/:workflowId/exchanges/:exchangeId/protocols', + (req, res) => { + const {workflowId, exchangeId} = req.params; + const exchange = + store.exchanges.get(`${workflowId}:${exchangeId}`); + if(!exchange) { + return res.status(404).json( + problemDetails( + 'not-found', 'Not Found', 404, + `Exchange ${exchangeId} not found.`) + ); + } + const base = `${req.protocol}://${req.get('host')}`; + return res.status(200).json({ + protocols: { + vcapi: + `${base}/workflows/${workflowId}/exchanges/${exchangeId}` + } + }); + }); + + return router; +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..18def7e --- /dev/null +++ b/src/server.js @@ -0,0 +1,31 @@ +import {createStore} from './store/index.js'; +import {credentialsRouter} from './routes/credentials.js'; +import {errorHandler} from './middleware/problemDetails.js'; +import {interactionsRouter} from './routes/interactions.js'; +import {presentationsRouter} from './routes/presentations.js'; +import {statusRouter} from './routes/status.js'; +import {workflowsRouter} from './routes/workflows.js'; + +import express from 'express'; + +/** + * Creates and configures the Express app. + * Does not call listen — callers handle that (or use supertest directly). + * + * @param {ReturnType} [store] - Optional store override + * for testing + * @returns {import('express').Application} + */ +export function createApp(store = createStore()) { + const app = express(); + app.use(express.json()); + + app.use('/credentials', credentialsRouter(store)); + app.use('/presentations', presentationsRouter(store)); + app.use('/status-lists', statusRouter(store)); + app.use('/workflows', workflowsRouter(store)); + app.use('/', interactionsRouter(store)); + + app.use(errorHandler); + return app; +} diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..77c23ef --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,53 @@ +/** @typedef {import('../types.js').VerifiableCredential} VerifiableCredential */ +/** @typedef {import('../types.js').VerifiablePresentation} VerifiablePresentation */ +/** @typedef {import('../types.js').StatusListCredential} StatusListCredential */ +/** @typedef {import('../types.js').WorkflowConfig} WorkflowConfig */ + +/** + * @typedef {object} CredentialEntry + * @property {VerifiableCredential} vc + * @property {string[]} mandatoryPointers - Pointers marked mandatory at + * issuance + * @property {boolean} deleted - Whether the credential has been soft-deleted + */ + +/** + * @typedef {object} PresentationEntry + * @property {VerifiablePresentation} vp + * @property {boolean} deleted - Whether the presentation has been soft-deleted + */ + +/** + * @typedef {object} ExchangeState + * @property {string} id - Local exchange ID + * @property {string} workflowId - Parent workflow ID + * @property {number} sequence - Incremented on each POST + * @property {string} step - Current step name + * @property {'pending'|'active'|'complete'|'invalid'} state + * @property {string|null} expires - ISO datetime string or null + * @property {object} variables - Exchange variables + * @property {object|null} lastError - Last ProblemDetails error + */ + +/** + * Creates a fresh isolated in-memory store. + * + * @returns {{ + * credentials: Map, + * presentations: Map, + * workflows: Map, + * exchanges: Map, + * statusLists: Map, + * challenges: Map + * }} + */ +export function createStore() { + return { + credentials: new Map(), + presentations: new Map(), + workflows: new Map(), + exchanges: new Map(), + statusLists: new Map(), + challenges: new Map() + }; +} diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000..9534dd7 --- /dev/null +++ b/src/types.js @@ -0,0 +1,76 @@ +// Shared JSDoc typedefs for VCALM domain objects. +// Import in other files with e.g.: +// @typedef {import('./types.js').Proof} Proof + +/** + * @typedef {object} Proof + * @property {string} type - e.g. 'DataIntegrityProof' + * @property {string} [cryptosuite] - e.g. 'eddsa-rdfc-2022' + * @property {string} [verificationMethod] + * @property {string} [created] - ISO datetime + * @property {string} [proofPurpose] + * @property {string} [proofValue] + * @property {string} [challenge] + * @property {string} [domain] + */ + +// VerifiableCredential and VerifiablePresentation use Record +// because '@context' is a valid JS property name but cannot be expressed as a +// JSDoc @property tag. The intersection adds typed access for the fields that +// matter most — proof, type, id. + +/** + * @typedef {Record & { + * type: string|string[], + * id?: string, + * issuer?: string|object, + * validFrom?: string, + * issued?: string, + * credentialSubject?: Record, + * proof?: Proof + * }} VerifiableCredential + */ + +/** + * @typedef {Record & { + * type: string|string[], + * id?: string, + * holder?: string, + * verifiableCredential?: VerifiableCredential[], + * proof?: Proof + * }} VerifiablePresentation + */ + +/** + * @typedef {Record & { + * id: string, + * type: string|string[], + * issuer: string, + * issued: string, + * credentialSubject: StatusListSubject, + * proof?: Proof + * }} StatusListCredential + */ + +/** + * @typedef {object} StatusListSubject + * @property {string} id + * @property {string} type + * @property {string} statusPurpose + * @property {string} encodedList + */ + +/** + * @typedef {object} WorkflowStep + * @property {object} [verifiablePresentationRequest] + * @property {string[]} [issueRequests] + * @property {string} [nextStep] + */ + +/** + * @typedef {object} WorkflowConfig + * @property {string} id + * @property {string} initialStep + * @property {Record} steps + * @property {object[]} [credentialTemplates] + */ diff --git a/src/utils/pointers.js b/src/utils/pointers.js new file mode 100644 index 0000000..eb2557c --- /dev/null +++ b/src/utils/pointers.js @@ -0,0 +1,26 @@ +/** + * JSON pointer utilities for selective disclosure validation. + */ + +/** @type {string[]} */ +export const SPEC_MANDATORY_POINTERS = [ + '/@context', '/type', '/issuer', '/validFrom' +]; + +/** + * Computes the set of mandatory pointers not covered by selective pointers. + * Always includes spec-level mandatory pointers regardless of issuer + * configuration. + * + * @param {string[]} mandatoryPointers - Pointers the issuer marked mandatory + * at issuance + * @param {string[]} selectivePointers - Pointers the holder wants to reveal + * @returns {string[]} Pointers that are mandatory but not in selectivePointers + */ +export function computeMissing(mandatoryPointers, selectivePointers) { + const allMandatory = new Set( + [...SPEC_MANDATORY_POINTERS, ...mandatoryPointers] + ); + const selective = new Set(selectivePointers); + return [...allMandatory].filter(p => !selective.has(p)); +} diff --git a/src/utils/proof.js b/src/utils/proof.js new file mode 100644 index 0000000..d4e37b1 --- /dev/null +++ b/src/utils/proof.js @@ -0,0 +1,49 @@ +import {createHash} from 'node:crypto'; + +/** @typedef {import('../types.js').Proof} Proof */ +/** @typedef {import('../types.js').VerifiableCredential} VerifiableCredential */ +/** @typedef {import('../types.js').VerifiablePresentation} VerifiablePresentation */ +/** @typedef {Record & {proof?: Proof}} HasProof */ + +export const MOCK_VERIFICATION_METHOD = 'did:example:mock#key-1'; + +/** + * Generates a deterministic fake proof for a credential or presentation. + * Does NOT perform real cryptography — for mock/testing use only. + * + * @param {HasProof} document + * @returns {Proof} + */ +export function generateProof(document) { + const hash = createHash('sha256') + .update(JSON.stringify(document)) + .digest('hex'); + return { + type: 'DataIntegrityProof', + cryptosuite: 'eddsa-rdfc-2022', + verificationMethod: MOCK_VERIFICATION_METHOD, + created: new Date().toISOString(), + proofPurpose: 'assertionMethod', + proofValue: `mock-proof-${hash}` + }; +} + +/** + * Verifies that a document has a valid mock proof. + * + * @param {HasProof} document + * @returns {boolean} + */ +export function verifyProof(document) { + const {proof} = document; + if(!proof) { + return false; + } + if(proof.type !== 'DataIntegrityProof') { + return false; + } + if(!proof.proofValue?.startsWith('mock-proof-')) { + return false; + } + return true; +} diff --git a/test/conformance/conformance.test.js b/test/conformance/conformance.test.js new file mode 100644 index 0000000..061a360 --- /dev/null +++ b/test/conformance/conformance.test.js @@ -0,0 +1,351 @@ +/** + * Conformance smoke tests for VCALM HTTP API. + * + * Runs against the local mock by default. Point at a real server: + * VCALM_BASE_URL=https://real-server.example npm run test:conformance + * + * Covers the happy path for each role: issuer, verifier, holder. + * Every test is self-contained — no shared state between tests. + */ + +import {getAgent, SAMPLE_CREDENTIAL, SAMPLE_PRESENTATION} + from './helpers.js'; +import {strict as assert} from 'node:assert'; +import {validateResponse} from './validator.js'; + +// --------------------------------------------------------------------------- +// Issuer role +// --------------------------------------------------------------------------- + +describe('[conformance] issuer — issue a credential', () => { + it('POST /credentials/issue returns 201 with a proof', async () => { + const {agent} = getAgent(); + const res = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({credential: SAMPLE_CREDENTIAL}); + assert.equal(res.status, 201, res.text); + assert.ok(res.body.proof, 'issued VC must have a proof'); + assert.ok(res.body.issuer, 'issued VC must have an issuer'); + await validateResponse('VerifiableCredential', res.body); + }); + + it('GET /credentials/:id returns the issued VC', async () => { + const {agent} = getAgent(); + const credentialId = `conformance-get-${Date.now()}`; + await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({ + credential: SAMPLE_CREDENTIAL, + options: {credentialId} + }); + const res = await agent.get(`/credentials/${credentialId}`); + assert.equal(res.status, 200, res.text); + assert.ok(res.body.proof, 'retrieved VC must have a proof'); + await validateResponse('VerifiableCredential', res.body); + }); + + it('DELETE /credentials/:id soft-deletes, then GET returns 404', async () => { + const {agent} = getAgent(); + const credentialId = `conformance-delete-${Date.now()}`; + await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({ + credential: SAMPLE_CREDENTIAL, + options: {credentialId} + }); + const delRes = await agent.delete(`/credentials/${credentialId}`); + assert.equal(delRes.status, 202, delRes.text); + + const getRes = await agent.get(`/credentials/${credentialId}`); + assert.equal(getRes.status, 404, 'deleted credential should 404'); + }); +}); + +// --------------------------------------------------------------------------- +// Verifier role +// --------------------------------------------------------------------------- + +describe('[conformance] verifier — verify a credential', () => { + it('POST /credentials/verify returns verified:true for valid VC', + async () => { + const {agent} = getAgent(); + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({credential: SAMPLE_CREDENTIAL}); + const vc = issueRes.body; + + const res = await agent + .post('/credentials/verify') + .set('Content-Type', 'application/json') + .send({verifiableCredential: vc}); + assert.equal(res.status, 200, res.text); + assert.equal(res.body.verified, true, 'valid VC should verify'); + await validateResponse('VerificationResult', res.body); + }); + + it('POST /credentials/verify returns verified:false for tampered VC', + async () => { + const {agent} = getAgent(); + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({credential: SAMPLE_CREDENTIAL}); + const vc = { + ...issueRes.body, + proof: {...issueRes.body.proof, proofValue: 'tampered'} + }; + const res = await agent + .post('/credentials/verify') + .set('Content-Type', 'application/json') + .send({verifiableCredential: vc}); + assert.equal(res.status, 200, res.text); + assert.equal(res.body.verified, false, 'tampered VC should not verify'); + await validateResponse('VerificationResult', res.body); + }); +}); + +// --------------------------------------------------------------------------- +// Holder role — presentations +// --------------------------------------------------------------------------- + +describe('[conformance] holder — create and verify a presentation', () => { + it('POST /presentations returns 201 with a proof', async () => { + const {agent} = getAgent(); + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({credential: SAMPLE_CREDENTIAL}); + + const res = await agent + .post('/presentations') + .set('Content-Type', 'application/json') + .send({ + presentation: { + ...SAMPLE_PRESENTATION, + verifiableCredential: [issueRes.body] + } + }); + assert.equal(res.status, 201, res.text); + assert.ok(res.body.verifiablePresentation.proof, 'VP must have a proof'); + await validateResponse( + 'VerifiablePresentation', res.body.verifiablePresentation + ); + }); + + it('POST /presentations/verify returns verified:true for valid VP', + async () => { + const {agent} = getAgent(); + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({credential: SAMPLE_CREDENTIAL}); + const presentRes = await agent + .post('/presentations') + .set('Content-Type', 'application/json') + .send({ + presentation: { + ...SAMPLE_PRESENTATION, + verifiableCredential: [issueRes.body] + } + }); + + const res = await agent + .post('/presentations/verify') + .set('Content-Type', 'application/json') + .send({ + verifiablePresentation: presentRes.body.verifiablePresentation + }); + assert.equal(res.status, 200, res.text); + assert.equal(res.body.verified, true, 'valid VP should verify'); + await validateResponse('VerificationResult', res.body); + }); +}); + +// --------------------------------------------------------------------------- +// Selective disclosure (derive) +// --------------------------------------------------------------------------- + +describe('[conformance] selective disclosure — derive', () => { + it('POST /credentials/derive returns a derived VC with fewer fields', + async () => { + const {agent} = getAgent(); + const credentialId = `conformance-derive-${Date.now()}`; + const mandatoryPointers = ['/credentialSubject/name']; + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({ + credential: SAMPLE_CREDENTIAL, + options: {credentialId, mandatoryPointers} + }); + const vc = issueRes.body; + + const selectivePointers = [ + '/@context', '/type', '/issuer', '/validFrom', + '/credentialSubject/name' + ]; + const res = await agent + .post('/credentials/derive') + .set('Content-Type', 'application/json') + .send({ + verifiableCredential: vc, + options: {credentialId, selectivePointers} + }); + assert.equal(res.status, 201, res.text); + assert.ok(res.body.proof, 'derived VC must have a proof'); + await validateResponse('VerifiableCredential', res.body); + }); + + it('POST /credentials/derive returns 400 when mandatory pointer is missing', + async () => { + const {agent} = getAgent(); + const credentialId = `conformance-derive-miss-${Date.now()}`; + const mandatoryPointers = ['/credentialSubject/name']; + const issueRes = await agent + .post('/credentials/issue') + .set('Content-Type', 'application/json') + .send({ + credential: SAMPLE_CREDENTIAL, + options: {credentialId, mandatoryPointers} + }); + + const res = await agent + .post('/credentials/derive') + .set('Content-Type', 'application/json') + .send({ + verifiableCredential: issueRes.body, + options: { + credentialId, + selectivePointers: ['/@context', '/type', '/issuer', '/validFrom'] + } + }); + assert.equal(res.status, 400, res.text); + assert.ok( + res.body.type?.includes('missing-mandatory-pointers'), + 'error type should indicate missing mandatory pointers' + ); + }); +}); + +// --------------------------------------------------------------------------- +// Challenges +// --------------------------------------------------------------------------- + +describe('[conformance] challenges', () => { + it('POST /challenges returns a nonce string', async () => { + const {agent} = getAgent(); + const res = await agent + .post('/challenges') + .set('Content-Type', 'application/json') + .send({}); + assert.equal(res.status, 201, res.text); + assert.ok( + typeof res.body.challenge === 'string' && res.body.challenge.length > 0, + 'challenge must be a non-empty string' + ); + }); +}); + +// --------------------------------------------------------------------------- +// Status lists +// --------------------------------------------------------------------------- + +describe('[conformance] status lists', () => { + it('POST /status-lists returns 201 with a status list VC', async () => { + const {agent} = getAgent(); + const res = await agent + .post('/status-lists') + .set('Content-Type', 'application/json') + .send({statusPurpose: 'revocation'}); + assert.equal(res.status, 201, res.text); + assert.ok(res.body.id, 'response must include id'); + assert.ok( + res.body.verifiableCredential?.proof, + 'status list must have a proof' + ); + assert.ok(res.get('Location'), 'response must include Location header'); + }); + + it('GET /status-lists/:id returns the status list VC (public)', async () => { + const {agent} = getAgent(); + const createRes = await agent + .post('/status-lists') + .set('Content-Type', 'application/json') + .send({statusPurpose: 'revocation'}); + const {id} = createRes.body; + + const res = await agent.get(`/status-lists/${encodeURIComponent(id)}`); + assert.equal(res.status, 200, res.text); + assert.equal(res.body.id, id); + }); +}); + +// --------------------------------------------------------------------------- +// Workflows + exchanges +// --------------------------------------------------------------------------- + +describe('[conformance] workflows and exchanges', () => { + const WORKFLOW = { + initialStep: 'step1', + steps: { + step1: { + verifiablePresentationRequest: { + query: [{type: 'QueryByExample'}] + } + } + } + }; + + it('POST /workflows returns 201 with Location header', async () => { + const {agent} = getAgent(); + const res = await agent + .post('/workflows') + .set('Content-Type', 'application/json') + .send(WORKFLOW); + assert.equal(res.status, 201, res.text); + assert.ok(res.get('Location'), 'must return Location header'); + }); + + it('GET /workflows/:id returns the workflow config', async () => { + const {agent} = getAgent(); + const createRes = await agent + .post('/workflows') + .set('Content-Type', 'application/json') + .send(WORKFLOW); + const workflowId = createRes.get('Location').split('/').pop(); + + const res = await agent.get(`/workflows/${workflowId}`); + assert.equal(res.status, 200, res.text); + assert.equal(res.body.initialStep, 'step1'); + }); + + it('full exchange: create → empty POST → get VPR', async () => { + const {agent} = getAgent(); + const workflowRes = await agent + .post('/workflows') + .set('Content-Type', 'application/json') + .send(WORKFLOW); + const workflowId = workflowRes.get('Location').split('/').pop(); + + const exchangeRes = await agent + .post(`/workflows/${workflowId}/exchanges`) + .set('Content-Type', 'application/json') + .send({}); + assert.equal(exchangeRes.status, 201, exchangeRes.text); + const exchangeId = exchangeRes.get('Location').split('/').pop(); + + const participateRes = await agent + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .set('Content-Type', 'application/json') + .send({}); + assert.equal(participateRes.status, 200, participateRes.text); + assert.ok( + participateRes.body.verifiablePresentationRequest, + 'should return VPR for step1' + ); + }); +}); diff --git a/test/conformance/helpers.js b/test/conformance/helpers.js new file mode 100644 index 0000000..a712468 --- /dev/null +++ b/test/conformance/helpers.js @@ -0,0 +1,41 @@ +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; +import supertest from 'supertest'; + +/** + * Returns a supertest-compatible agent pointed at either the local mock server + * or a real VCALM server identified by VCALM_BASE_URL. + * + * Usage: + * VCALM_BASE_URL=https://real-server.example npm run test:conformance + * + * @returns {{ + * agent: import('supertest').Agent | import('supertest').SuperTest, + * isExternal: boolean + * }} + */ +export function getAgent() { + const baseUrl = process.env.VCALM_BASE_URL; + if(baseUrl) { + return {agent: supertest(baseUrl), isExternal: true}; + } + const store = createStore(); + const app = createApp(store); + return {agent: supertest(app), isExternal: false}; +} + +export const SAMPLE_CREDENTIAL = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:conformance-issuer', + validFrom: new Date().toISOString(), + credentialSubject: { + id: 'did:example:conformance-subject', + name: 'Conformance Test Subject' + } +}; + +export const SAMPLE_PRESENTATION = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'] +}; diff --git a/test/conformance/validator.js b/test/conformance/validator.js new file mode 100644 index 0000000..d737fc3 --- /dev/null +++ b/test/conformance/validator.js @@ -0,0 +1,63 @@ +/** + * Response schema validator backed by spec/oas.yaml. + * + * Usage: + * import {validateResponse} from './validator.js'; + * validateResponse('VerifiableCredential', res.body); // throws on mismatch + */ + +import Ajv from 'ajv'; +import {dereference} from '@readme/openapi-parser'; + +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SPEC_PATH = resolve(__dirname, '../../spec/oas.yaml'); + +/** @type {Record | null} */ +let _api = null; + +/** @type {InstanceType | null} */ +let _ajv = null; + +async function load() { + if(_api) { + return; + } + _api = /** @type {Record} */ ( + await dereference(SPEC_PATH) + ); + _ajv = new Ajv({allErrors: true}); +} + +/** + * Validates a response body against a named schema from spec/oas.yaml. + * Throws an AssertionError-style Error if validation fails. + * + * @param {string} schemaName - Key from components.schemas (e.g. + * 'VerifiableCredential') + * @param {unknown} body - The parsed response body to validate + */ +export async function validateResponse(schemaName, body) { + await load(); + const schema = + /** @type {any} */ (_api)?.components?.schemas?.[schemaName]; + if(!schema) { + throw new Error( + `Schema '${schemaName}' not found in spec/oas.yaml components.schemas` + ); + } + const ajv = /** @type {InstanceType} */ (_ajv); + const valid = ajv.validate(schema, body); + if(!valid) { + const errors = ajv.errors + ?.map( + e => ` ${/** @type {any} */ (e).instancePath || '(root)'} ${e.message}` + ) + .join('\n'); + throw new Error( + `Response does not match schema '${schemaName}':\n${errors}` + ); + } +} diff --git a/test/fixtures/credential.js b/test/fixtures/credential.js new file mode 100644 index 0000000..485cc13 --- /dev/null +++ b/test/fixtures/credential.js @@ -0,0 +1,37 @@ +/** + * Sample unsigned credential fixture for tests. + * @returns {object} + */ +export function unsignedCredential() { + return { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:issuer', + validFrom: '2024-01-01T00:00:00Z', + credentialSubject: { + id: 'did:example:alice', + name: 'Alice Smith', + birthCountry: 'US', + alumniOf: 'University of Example' + } + }; +} + +/** + * Sample unsigned credential with BBS+ cryptosuite hint for derive tests. + * @returns {object} + */ +export function bbsCredential() { + return { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:issuer', + validFrom: '2024-01-01T00:00:00Z', + credentialSubject: { + id: 'did:example:alice', + name: 'Alice Smith', + birthCountry: 'US', + age: 30 + } + }; +} diff --git a/test/integration/credentials.test.js b/test/integration/credentials.test.js new file mode 100644 index 0000000..211eb5a --- /dev/null +++ b/test/integration/credentials.test.js @@ -0,0 +1,184 @@ +import {bbsCredential, unsignedCredential} from '../fixtures/credential.js'; +import {strict as assert} from 'node:assert'; +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; + +import request from 'supertest'; + +describe('POST /credentials/issue', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should return 201 with a proof attached', async () => { + const res = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + assert.equal(res.status, 201); + assert.ok(res.body.proof); + assert.equal(res.body.proof.type, 'DataIntegrityProof'); + }); + + it('should return 400 when credential is missing', async () => { + const res = await request(app) + .post('/credentials/issue') + .send({}); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('invalid-input')); + }); + + it('should store the credential by credentialId option', async () => { + const credentialId = 'test-cred-123'; + await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential(), options: {credentialId}}); + const stored = store.credentials.get(credentialId); + assert.ok(stored); + assert.equal(/** @type {any} */ (stored.vc).issuer, 'did:example:issuer'); + }); +}); + +describe('GET /credentials/:id', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should retrieve an issued credential', async () => { + const credentialId = 'get-test-cred'; + await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential(), options: {credentialId}}); + const res = await request(app).get(`/credentials/${credentialId}`); + assert.equal(res.status, 200); + assert.equal(res.body.issuer, 'did:example:issuer'); + }); + + it('should return 404 for unknown credential', async () => { + const res = await request(app).get('/credentials/does-not-exist'); + assert.equal(res.status, 404); + }); +}); + +describe('DELETE /credentials/:id', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should soft delete and return 202', async () => { + const credentialId = 'delete-test-cred'; + await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential(), options: {credentialId}}); + const del = await request(app).delete(`/credentials/${credentialId}`); + assert.equal(del.status, 202); + const get = await request(app).get(`/credentials/${credentialId}`); + assert.equal(get.status, 404); + }); +}); + +describe('POST /credentials/verify', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should return verified:true for a mock-issued credential', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const verifyRes = await request(app) + .post('/credentials/verify') + .send({verifiableCredential: issueRes.body}); + assert.equal(verifyRes.status, 200); + assert.equal(verifyRes.body.verified, true); + }); + + it('should return verified:false for a credential with tampered proof', + async () => { + const vc = { + ...unsignedCredential(), + proof: {type: 'DataIntegrityProof', proofValue: 'bad'} + }; + const res = await request(app) + .post('/credentials/verify') + .send({verifiableCredential: vc}); + assert.equal(res.status, 200); + assert.equal(res.body.verified, false); + }); +}); + +describe('POST /credentials/derive', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should derive a credential with valid selectivePointers', async () => { + const credentialId = 'derive-test-cred'; + const issueRes = await request(app) + .post('/credentials/issue') + .send({ + credential: bbsCredential(), + options: {credentialId, mandatoryPointers: ['/credentialSubject/id']} + }); + const deriveRes = await request(app) + .post('/credentials/derive') + .send({ + verifiableCredential: issueRes.body, + options: { + selectivePointers: [ + '/@context', '/type', '/issuer', '/validFrom', + '/credentialSubject/id', '/credentialSubject/birthCountry' + ] + } + }); + assert.equal(deriveRes.status, 201); + assert.ok(deriveRes.body.proof); + }); + + it('should return 400 when selectivePointers missing a spec-level ' + + 'mandatory pointer', async () => { + const credentialId = 'derive-fail-cred'; + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: bbsCredential(), options: {credentialId}}); + const deriveRes = await request(app) + .post('/credentials/derive') + .send({ + verifiableCredential: issueRes.body, + options: {selectivePointers: ['/credentialSubject/name']} + }); + assert.equal(deriveRes.status, 400); + assert.ok(deriveRes.body.type.includes('missing-mandatory-pointers')); + assert.ok(deriveRes.body.detail.includes('/issuer')); + }); + + it('should return 400 when selectivePointers missing issuer-specified ' + + 'mandatory pointer', async () => { + const credentialId = 'derive-fail-mandatory'; + const issueRes = await request(app) + .post('/credentials/issue') + .send({ + credential: bbsCredential(), + options: {credentialId, mandatoryPointers: ['/credentialSubject/id']} + }); + const deriveRes = await request(app) + .post('/credentials/derive') + .send({ + verifiableCredential: issueRes.body, + options: { + selectivePointers: ['/@context', '/type', '/issuer', '/validFrom'] + } + }); + assert.equal(deriveRes.status, 400); + assert.ok(deriveRes.body.detail.includes('/credentialSubject/id')); + }); +}); diff --git a/test/integration/interactions.test.js b/test/integration/interactions.test.js new file mode 100644 index 0000000..6c1be5b --- /dev/null +++ b/test/integration/interactions.test.js @@ -0,0 +1,85 @@ +import {strict as assert} from 'node:assert'; +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; +import request from 'supertest'; + +describe('challenges', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + + beforeEach(() => { + store = createStore(); + app = createApp(store); + }); + + describe('POST /challenges', () => { + it('should return a nonce', async () => { + const res = await request(app) + .post('/challenges') + .send({}); + assert.equal(res.status, 201); + assert.ok(typeof res.body.challenge === 'string'); + assert.ok(res.body.challenge.length > 0); + }); + + it('should return a unique nonce each call', async () => { + const r1 = await request(app).post('/challenges').send({}); + const r2 = await request(app).post('/challenges').send({}); + assert.notEqual(r1.body.challenge, r2.body.challenge); + }); + }); + + describe('challenge single-use enforcement via /presentations/verify', () => { + it('should reject a reused challenge', async () => { + const challengeRes = await request(app).post('/challenges').send({}); + const {challenge} = challengeRes.body; + + const vp = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'], + proof: { + type: 'DataIntegrityProof', + challenge, + proofValue: 'mock-proof-abc' + } + }; + + await request(app) + .post('/presentations/verify') + .send({verifiablePresentation: vp, options: {challenge}}); + + const second = await request(app) + .post('/presentations/verify') + .send({verifiablePresentation: vp, options: {challenge}}); + + assert.equal(second.status, 400); + assert.ok(second.body.type.includes('challenge')); + }); + }); +}); + +describe('interactions', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + + beforeEach(() => { + store = createStore(); + app = createApp(store); + }); + + describe('GET /interactions/:id', () => { + it('should return protocols when iuv=1', async () => { + const res = await request(app) + .get('/interactions/test-interaction-abc?iuv=1'); + assert.equal(res.status, 200); + assert.ok(res.body.protocols); + }); + + it('should return 400 when iuv param is missing', async () => { + const res = await request(app) + .get('/interactions/some-id'); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('invalid-input')); + }); + }); +}); diff --git a/test/integration/presentations.test.js b/test/integration/presentations.test.js new file mode 100644 index 0000000..19780ad --- /dev/null +++ b/test/integration/presentations.test.js @@ -0,0 +1,164 @@ +import {strict as assert} from 'node:assert'; +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; +import {unsignedCredential} from '../fixtures/credential.js'; + +import request from 'supertest'; + +/** @param {object} vc */ +function buildPresentation(vc) { + return { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'], + holder: 'did:example:alice', + verifiableCredential: [vc] + }; +} + +describe('POST /presentations (create)', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should return 201 with a signed presentation', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const pres = buildPresentation(issueRes.body); + const res = await request(app) + .post('/presentations') + .send({presentation: pres}); + assert.equal(res.status, 201); + assert.ok(res.body.verifiablePresentation.proof); + }); + + it('should bind challenge and domain into the proof', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const pres = buildPresentation(issueRes.body); + const res = await request(app) + .post('/presentations') + .send({ + presentation: pres, + options: {challenge: 'abc123', domain: 'example.com'} + }); + assert.equal( + res.body.verifiablePresentation.proof.challenge, 'abc123' + ); + assert.equal( + res.body.verifiablePresentation.proof.domain, 'example.com' + ); + }); +}); + +describe('POST /presentations/verify', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should return verified:true for a valid signed presentation', + async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const createRes = await request(app) + .post('/presentations') + .send({presentation: buildPresentation(issueRes.body)}); + const verifyRes = await request(app) + .post('/presentations/verify') + .send({verifiablePresentation: createRes.body.verifiablePresentation}); + assert.equal(verifyRes.status, 200); + assert.equal(verifyRes.body.verified, true); + }); + + it('should return 200 verified:false for tampered proof', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const vp = { + ...buildPresentation(issueRes.body), + proof: {type: 'DataIntegrityProof', proofValue: 'tampered'} + }; + const res = await request(app) + .post('/presentations/verify') + .send({verifiablePresentation: vp}); + assert.equal(res.status, 200); + assert.equal(res.body.verified, false); + }); + + it('should return 400 on challenge mismatch', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const createRes = await request(app) + .post('/presentations') + .send({ + presentation: buildPresentation(issueRes.body), + options: {challenge: 'correct-challenge'} + }); + // Store 'wrong-challenge' so single-use guard passes, then mismatch fires + const TTL_MS = 5 * 60 * 1000; + store.challenges.set('wrong-challenge', {expires: Date.now() + TTL_MS}); + const res = await request(app) + .post('/presentations/verify') + .send({ + verifiablePresentation: createRes.body.verifiablePresentation, + options: {challenge: 'wrong-challenge'} + }); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('challenge-mismatch')); + }); + + it('should accept proofless presentation and verify enclosed credentials', + async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + const presentation = buildPresentation(issueRes.body); + const res = await request(app) + .post('/presentations/verify') + .send({presentation}); + assert.equal(res.status, 200); + assert.equal(res.body.verified, true); + }); + + it('should proceed with warning when holder present but no proof', + async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + // VP with holder but no proof + const vp = buildPresentation(issueRes.body); + const res = await request(app) + .post('/presentations/verify') + .send({verifiablePresentation: vp}); + assert.equal(res.status, 200); + // verified based on enclosed credentials only + assert.equal(res.body.verified, true); + }); +}); + +describe('GET /presentations', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should list stored presentations', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + await request(app) + .post('/presentations') + .send({presentation: buildPresentation(issueRes.body)}); + const res = await request(app).get('/presentations'); + assert.equal(res.status, 200); + assert.equal(res.body.length, 1); + }); +}); diff --git a/test/integration/status.test.js b/test/integration/status.test.js new file mode 100644 index 0000000..a9dd620 --- /dev/null +++ b/test/integration/status.test.js @@ -0,0 +1,158 @@ +import {strict as assert} from 'node:assert'; +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; +import request from 'supertest'; + +describe('status endpoints', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + + beforeEach(() => { + store = createStore(); + app = createApp(store); + }); + + describe('POST /status-lists', () => { + it('should create a status list and return 201', async () => { + const res = await request(app) + .post('/status-lists') + .send({statusPurpose: 'revocation'}); + assert.equal(res.status, 201); + assert.ok(res.body.id); + assert.ok(res.body.verifiableCredential); + assert.ok(res.get('Location')); + }); + + it('should return a status list VC with correct shape', async () => { + const res = await request(app) + .post('/status-lists') + .send({statusPurpose: 'suspension'}); + const {verifiableCredential: vc} = res.body; + assert.ok( + vc.type.includes('StatusList2021Credential'), + 'type should include StatusList2021Credential' + ); + assert.equal(vc.credentialSubject.statusPurpose, 'suspension'); + assert.ok(vc.proof, 'should have proof'); + }); + + it('should use provided id', async () => { + const id = 'https://vcalm.example/status-lists/test-123'; + const res = await request(app) + .post('/status-lists') + .send({statusPurpose: 'revocation', id}); + assert.equal(res.status, 201); + assert.equal(res.body.id, id); + }); + + it('should return 400 when statusPurpose is missing', async () => { + const res = await request(app) + .post('/status-lists') + .send({}); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('invalid-input')); + }); + }); + + describe('POST /credentials/status', () => { + it('should update a credential status and return 200', async () => { + const issueRes = await request(app) + .post('/credentials/issue') + .send({ + credential: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:mock', + validFrom: new Date().toISOString(), + credentialSubject: {id: 'did:example:alice'} + }, + options: {credentialId: 'cred-status-test'} + }); + assert.equal(issueRes.status, 201); + + const statusRes = await request(app) + .post('/credentials/status') + .send({ + credentialId: 'cred-status-test', + credentialStatus: { + type: 'StatusList2021Entry', + statusPurpose: 'revocation', + statusListIndex: '42', + statusListCredential: 'https://vcalm.example/status-lists/1' + } + }); + assert.equal(statusRes.status, 200); + }); + + it('should reflect updated status on GET', async () => { + await request(app) + .post('/credentials/issue') + .send({ + credential: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:mock', + validFrom: new Date().toISOString(), + credentialSubject: {id: 'did:example:alice'} + }, + options: {credentialId: 'cred-get-status-test'} + }); + + const credentialStatus = { + type: 'StatusList2021Entry', + statusPurpose: 'revocation', + statusListIndex: '7', + statusListCredential: 'https://vcalm.example/status-lists/1' + }; + await request(app) + .post('/credentials/status') + .send({credentialId: 'cred-get-status-test', credentialStatus}); + + const getRes = await request(app) + .get('/credentials/cred-get-status-test'); + assert.equal(getRes.status, 200); + assert.deepEqual(getRes.body.credentialStatus, credentialStatus); + }); + + it('should return 404 for unknown credential', async () => { + const res = await request(app) + .post('/credentials/status') + .send({ + credentialId: 'does-not-exist', + credentialStatus: {type: 'StatusList2021Entry'} + }); + assert.equal(res.status, 404); + assert.ok(res.body.type.includes('not-found')); + }); + + it('should return 400 when credentialId is missing', async () => { + const res = await request(app) + .post('/credentials/status') + .send({credentialStatus: {type: 'StatusList2021Entry'}}); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('invalid-input')); + }); + }); + + describe('GET /status-lists/:id', () => { + it('should retrieve a stored status list', async () => { + const createRes = await request(app) + .post('/status-lists') + .send({statusPurpose: 'revocation'}); + const {id} = createRes.body; + + const encodedId = encodeURIComponent(id); + const getRes = await request(app) + .get(`/status-lists/${encodedId}`); + assert.equal(getRes.status, 200); + assert.equal(getRes.body.id, id); + }); + + it('should return 404 for unknown status list', async () => { + const res = await request(app) + .get('/status-lists/unknown-id'); + assert.equal(res.status, 404); + assert.ok(res.body.type.includes('not-found')); + }); + }); +}); diff --git a/test/integration/workflows.test.js b/test/integration/workflows.test.js new file mode 100644 index 0000000..d8e9819 --- /dev/null +++ b/test/integration/workflows.test.js @@ -0,0 +1,151 @@ +import {strict as assert} from 'node:assert'; +import {createApp} from '../../src/server.js'; +import {createStore} from '../../src/store/index.js'; +import {unsignedCredential} from '../fixtures/credential.js'; + +import request from 'supertest'; + +const sampleWorkflow = { + initialStep: 'requestCredential', + steps: { + requestCredential: { + verifiablePresentationRequest: { + query: [{ + type: 'QueryByExample', + credentialQuery: [{reason: 'Please provide your credential'}] + }] + } + } + } +}; + +describe('POST /workflows', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should return 201 with Location header', async () => { + const res = await request(app) + .post('/workflows') + .send(sampleWorkflow); + assert.equal(res.status, 201); + assert.ok(res.headers.location); + }); + + it('should return 400 when steps or initialStep missing', async () => { + const res = await request(app) + .post('/workflows') + .send({initialStep: 'foo'}); + assert.equal(res.status, 400); + }); +}); + +describe('GET /workflows/:id', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + beforeEach(() => { + store = createStore(); app = createApp(store); + }); + + it('should retrieve a created workflow', async () => { + const createRes = await request(app) + .post('/workflows') + .send(sampleWorkflow); + const workflowId = createRes.headers.location.split('/').pop(); + const res = await request(app).get(`/workflows/${workflowId}`); + assert.equal(res.status, 200); + assert.equal(res.body.initialStep, 'requestCredential'); + }); +}); + +describe('Exchange lifecycle', () => { + /** @type {import('express').Application} */ let app; + /** @type {ReturnType} */ let store; + /** @type {string} */ let workflowId; + beforeEach(async () => { + store = createStore(); + app = createApp(store); + const res = await request(app).post('/workflows').send(sampleWorkflow); + workflowId = res.headers.location.split('/').pop() ?? ''; + }); + + it('should create an exchange and return Location header', async () => { + const res = await request(app) + .post(`/workflows/${workflowId}/exchanges`) + .send({}); + assert.equal(res.status, 201); + assert.ok(res.headers.location); + }); + + it('should return VPR when posting empty body to exchange', async () => { + const createRes = await request(app) + .post(`/workflows/${workflowId}/exchanges`) + .send({}); + const exchangeId = createRes.headers.location.split('/').pop(); + const res = await request(app) + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .send({}); + assert.equal(res.status, 200); + assert.ok(res.body.verifiablePresentationRequest); + }); + + it('should complete exchange when VP is submitted', async () => { + const createRes = await request(app) + .post(`/workflows/${workflowId}/exchanges`) + .send({}); + const exchangeId = createRes.headers.location.split('/').pop(); + + // First step: get VPR + await request(app) + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .send({}); + + // Issue a credential to present + const issueRes = await request(app) + .post('/credentials/issue') + .send({credential: unsignedCredential()}); + + // Submit VP + const vp = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiablePresentation'], + verifiableCredential: [issueRes.body] + }; + const res = await request(app) + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .send({verifiablePresentation: vp}); + assert.equal(res.status, 200); + + // Exchange should be complete + const stateRes = await request(app) + .get(`/workflows/${workflowId}/exchanges/${exchangeId}`); + assert.equal(stateRes.body.state, 'complete'); + }); + + it('should return 400 for redirectUrl client message', async () => { + const createRes = await request(app) + .post(`/workflows/${workflowId}/exchanges`) + .send({}); + const exchangeId = createRes.headers.location.split('/').pop(); + const res = await request(app) + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .send({redirectUrl: 'https://other.example/exchange/123'}); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('redirect-url-not-supported')); + }); + + it('should return 400 for expired exchange', async () => { + const past = new Date(Date.now() - 1000).toISOString(); + const createRes = await request(app) + .post(`/workflows/${workflowId}/exchanges`) + .send({expires: past}); + const exchangeId = createRes.headers.location.split('/').pop(); + const res = await request(app) + .post(`/workflows/${workflowId}/exchanges/${exchangeId}`) + .send({}); + assert.equal(res.status, 400); + assert.ok(res.body.type.includes('exchange-expired')); + }); +}); diff --git a/test/unit/pointers.test.js b/test/unit/pointers.test.js new file mode 100644 index 0000000..b22d4aa --- /dev/null +++ b/test/unit/pointers.test.js @@ -0,0 +1,67 @@ +import { + computeMissing, + SPEC_MANDATORY_POINTERS +} from '../../src/utils/pointers.js'; +import {strict as assert} from 'node:assert'; + +describe('pointers', () => { + describe('SPEC_MANDATORY_POINTERS', () => { + it('should include required VC fields', () => { + assert.ok(SPEC_MANDATORY_POINTERS.includes('/@context')); + assert.ok(SPEC_MANDATORY_POINTERS.includes('/type')); + assert.ok(SPEC_MANDATORY_POINTERS.includes('/issuer')); + assert.ok(SPEC_MANDATORY_POINTERS.includes('/validFrom')); + }); + }); + + describe('computeMissing', () => { + it('should return empty array when all mandatory pointers are covered', + () => { + const mandatory = /** @type {string[]} */ (['/issuer', '/validFrom']); + const selective = /** @type {string[]} */ ( + ['/@context', '/type', '/issuer', '/validFrom', + '/credentialSubject/name'] + ); + assert.deepEqual(computeMissing(mandatory, selective), []); + }); + + it('should return missing pointers when selective does not cover ' + + 'mandatory', () => { + const mandatory = /** @type {string[]} */ ( + ['/issuer', '/validFrom', '/credentialStatus'] + ); + const selective = /** @type {string[]} */ ( + ['/@context', '/type', '/issuer'] + ); + const missing = computeMissing(mandatory, selective); + assert.deepEqual(missing.sort(), ['/credentialStatus', '/validFrom']); + }); + + it('should always include spec-level mandatory pointers even if not in ' + + 'issuer mandatory', () => { + const mandatory = /** @type {string[]} */ ([]); + const selective = /** @type {string[]} */ (['/credentialSubject/name']); + const missing = computeMissing(mandatory, selective); + assert.ok(missing.includes('/@context')); + assert.ok(missing.includes('/type')); + assert.ok(missing.includes('/issuer')); + assert.ok(missing.includes('/validFrom')); + }); + + it('should return empty array when selective covers all spec-level and ' + + 'issuer mandatory', () => { + const mandatory = ['/credentialStatus']; + const selective = [ + '/@context', '/type', '/issuer', '/validFrom', '/credentialStatus' + ]; + assert.deepEqual(computeMissing(mandatory, selective), []); + }); + + it('should handle empty selective pointers', () => { + const mandatory = /** @type {string[]} */ ([]); + const selective = /** @type {string[]} */ ([]); + const missing = computeMissing(mandatory, selective); + assert.deepEqual(missing.sort(), [...SPEC_MANDATORY_POINTERS].sort()); + }); + }); +}); diff --git a/test/unit/proof.test.js b/test/unit/proof.test.js new file mode 100644 index 0000000..01d214c --- /dev/null +++ b/test/unit/proof.test.js @@ -0,0 +1,87 @@ +import { + generateProof, + MOCK_VERIFICATION_METHOD, + verifyProof +} from '../../src/utils/proof.js'; +import {strict as assert} from 'node:assert'; + +const sampleCredential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: 'did:example:issuer', + validFrom: '2024-01-01T00:00:00Z', + credentialSubject: {id: 'did:example:alice', name: 'Alice'} +}; + +describe('proof', () => { + describe('generateProof', () => { + it('should return a DataIntegrityProof object', () => { + const proof = generateProof(sampleCredential); + assert.equal(proof.type, 'DataIntegrityProof'); + }); + + it('should use eddsa-rdfc-2022 cryptosuite', () => { + const proof = generateProof(sampleCredential); + assert.equal(proof.cryptosuite, 'eddsa-rdfc-2022'); + }); + + it('should set proofPurpose to assertionMethod', () => { + const proof = generateProof(sampleCredential); + assert.equal(proof.proofPurpose, 'assertionMethod'); + }); + + it('should set verificationMethod to mock DID', () => { + const proof = generateProof(sampleCredential); + assert.equal(proof.verificationMethod, MOCK_VERIFICATION_METHOD); + }); + + it('should set proofValue starting with mock-proof-', () => { + const proof = generateProof(sampleCredential); + assert.ok(proof.proofValue?.startsWith('mock-proof-')); + }); + + it('should include a created timestamp', () => { + const proof = generateProof(sampleCredential); + assert.ok(typeof proof.created === 'string'); + assert.ok(new Date(proof.created).getTime() > 0); + }); + + it('should produce deterministic proofValue for same input', () => { + const p1 = generateProof(sampleCredential); + const p2 = generateProof(sampleCredential); + assert.equal(p1.proofValue, p2.proofValue); + }); + }); + + describe('verifyProof', () => { + it('should return true for a valid mock proof', () => { + const vc = {...sampleCredential, proof: generateProof(sampleCredential)}; + assert.equal(verifyProof(vc), true); + }); + + it('should return false when proof is missing', () => { + assert.equal(verifyProof(sampleCredential), false); + }); + + it('should return false when proof type is wrong', () => { + const vc = { + ...sampleCredential, + proof: { + ...generateProof(sampleCredential), type: 'SomeOtherProof' + } + }; + assert.equal(verifyProof(vc), false); + }); + + it('should return false when proofValue does not start with mock-proof-', + () => { + const vc = { + ...sampleCredential, + proof: { + ...generateProof(sampleCredential), proofValue: 'tampered-value' + } + }; + assert.equal(verifyProof(vc), false); + }); + }); +}); diff --git a/test/unit/store.test.js b/test/unit/store.test.js new file mode 100644 index 0000000..119c0a9 --- /dev/null +++ b/test/unit/store.test.js @@ -0,0 +1,61 @@ +import {strict as assert} from 'node:assert'; +import {createStore} from '../../src/store/index.js'; + +describe('store', () => { + /** @type {ReturnType} */ + let store; + + beforeEach(() => { + store = createStore(); + }); + + describe('credentials', () => { + it('should store and retrieve a credential', () => { + const vc = {id: 'cred-1', type: ['VerifiableCredential']}; + store.credentials.set('cred-1', { + vc, mandatoryPointers: [], deleted: false + }); + const entry = store.credentials.get('cred-1'); + assert.deepEqual(entry?.vc, vc); + }); + + it('should soft delete a credential', () => { + const vc = {id: 'cred-1', type: ['VerifiableCredential']}; + store.credentials.set('cred-1', { + vc, mandatoryPointers: [], deleted: false + }); + const entry = store.credentials.get('cred-1'); + if(entry) { + entry.deleted = true; + } + assert.equal(store.credentials.get('cred-1')?.deleted, true); + }); + }); + + describe('challenges', () => { + it('should store and retrieve a challenge', () => { + const expires = Date.now() + 60000; + store.challenges.set('nonce-abc', {expires}); + assert.equal(store.challenges.get('nonce-abc')?.expires, expires); + }); + + it('should allow deleting a used challenge', () => { + store.challenges.set('nonce-abc', {expires: Date.now() + 60000}); + store.challenges.delete('nonce-abc'); + assert.equal(store.challenges.get('nonce-abc'), undefined); + }); + }); + + describe('isolation', () => { + it('should create independent stores', () => { + const store1 = createStore(); + const store2 = createStore(); + store1.credentials.set('cred-1', { + vc: {id: 'cred-1', type: 'VerifiableCredential'}, + mandatoryPointers: [], + deleted: false + }); + assert.equal(store2.credentials.get('cred-1'), undefined); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed99242 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ESNext", + "lib": ["ESNext"], + "types": ["node", "mocha"] + }, + "include": ["src/**/*.js", "test/**/*.js"], + "exclude": ["node_modules"] +}