diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bc8a687cd..5cb9856283 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,12 @@ jobs: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} restore-keys: ${{ runner.os }}-nuget- + - name: Install OpenID4VP Verifier dependencies + working-directory: Applications/ConsumerApi/src/OpenId4VpVerifierSite + run: npm ci + - name: Run OpenID4VP Verifier unit tests + working-directory: Applications/ConsumerApi/src/OpenId4VpVerifierSite + run: npm test - name: Run tests run: ./.ci/test.sh env: diff --git a/.gitignore b/.gitignore index 1c9c273811..ad9fdd983f 100644 --- a/.gitignore +++ b/.gitignore @@ -270,6 +270,8 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ +Applications/ConsumerApi/src/OpenId4VpVerifierSite/dist/ +Applications/ConsumerApi/src/wwwroot/openid4vp-verifier/ # Visual Studio 6 build log *.plg diff --git a/Applications/ConsumerApi/src/ConsumerApi.csproj b/Applications/ConsumerApi/src/ConsumerApi.csproj index 178e0a3159..6ccf639008 100644 --- a/Applications/ConsumerApi/src/ConsumerApi.csproj +++ b/Applications/ConsumerApi/src/ConsumerApi.csproj @@ -2,6 +2,9 @@ f114fba8-95dd-4fee-8385-af8e8a343c68 + $(MSBuildProjectDirectory)/OpenId4VpVerifierSite/ + $(OpenId4VpVerifierSiteRoot)dist/openid4vp-verifier/ + $(MSBuildProjectDirectory)/wwwroot/openid4vp-verifier/ @@ -42,4 +45,28 @@ + + + + + + + + + + + + + + + + + diff --git a/Applications/ConsumerApi/src/Controllers/Onboarding/AppOnboardingController.cs b/Applications/ConsumerApi/src/Controllers/Onboarding/AppOnboardingController.cs index 7e22319fc3..013c7796ee 100644 --- a/Applications/ConsumerApi/src/Controllers/Onboarding/AppOnboardingController.cs +++ b/Applications/ConsumerApi/src/Controllers/Onboarding/AppOnboardingController.cs @@ -24,7 +24,7 @@ public AppOnboardingController(IOptions configuration, [ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [AllowAnonymous] - public IActionResult GetReference([FromRoute(Name = "referenceId")] string? _, [FromQuery] string? app) + public IActionResult GetReference([FromRoute(Name = "referenceId")] string? referenceId, [FromQuery] string? app) { if (_configuration == null) return NotFound(); @@ -38,7 +38,7 @@ public IActionResult GetReference([FromRoute(Name = "referenceId")] string? _, [ var stores = ListStoresForUserAgent(selectedAppConfiguration); - return View("AppOnboarding", new AppOnboardingModel(selectedAppConfiguration, stores)); + return View("AppOnboarding", new AppOnboardingModel(referenceId, selectedAppConfiguration, stores)); } private List ListStoresForUserAgent(ConsumerApiConfiguration.AppOnboardingConfiguration.App appConfiguration) @@ -110,8 +110,9 @@ public App(ConsumerApiConfiguration.AppOnboardingConfiguration.App app) public class AppOnboardingModel { - public AppOnboardingModel(ConsumerApiConfiguration.AppOnboardingConfiguration.App config, List links) + public AppOnboardingModel(string? referenceId, ConsumerApiConfiguration.AppOnboardingConfiguration.App config, List links) { + ReferenceId = referenceId; AppId = config.Id; AppDisplayName = config.DisplayName; AppDescription = config.Description; @@ -121,6 +122,7 @@ public AppOnboardingModel(ConsumerApiConfiguration.AppOnboardingConfiguration.Ap AppIconUrl = config.IconUrl; } + public string? ReferenceId { get; } public string AppId { get; } public string AppDisplayName { get; } public string AppDescription { get; set; } diff --git a/Applications/ConsumerApi/src/Dockerfile b/Applications/ConsumerApi/src/Dockerfile index c07e6ce24b..d733e49332 100644 --- a/Applications/ConsumerApi/src/Dockerfile +++ b/Applications/ConsumerApi/src/Dockerfile @@ -1,3 +1,13 @@ +FROM node:24-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS openid4vp-verifier-build-env + +WORKDIR /src/Applications/ConsumerApi/src/OpenId4VpVerifierSite + +COPY ["Applications/ConsumerApi/src/OpenId4VpVerifierSite/package.json", "Applications/ConsumerApi/src/OpenId4VpVerifierSite/package-lock.json", "./"] +RUN npm ci + +COPY ["Applications/ConsumerApi/src/OpenId4VpVerifierSite/", "./"] +RUN npm run build + FROM dhi.io/dotnet:10.0.302-sdk@sha256:e18fceb745383b13f9c334a22c4c17943770129a5601033d3dfc1beff2cec17e AS build-env ARG VERSION @@ -5,10 +15,11 @@ ARG VERSION WORKDIR /src COPY . . +COPY --from=openid4vp-verifier-build-env /src/Applications/ConsumerApi/src/OpenId4VpVerifierSite/dist/openid4vp-verifier ./Applications/ConsumerApi/src/wwwroot/openid4vp-verifier RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/ConsumerApi/src/ConsumerApi.csproj" RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/HealthCheck/src/HealthCheck.csproj" -RUN dotnet publish /p:ContinuousIntegrationBuild=true --configuration Release --output /app/publish --no-restore "Applications/ConsumerApi/src/ConsumerApi.csproj" +RUN dotnet publish /p:ContinuousIntegrationBuild=true /p:SkipOpenId4VpVerifierBuild=true --configuration Release --output /app/publish --no-restore "Applications/ConsumerApi/src/ConsumerApi.csproj" RUN dotnet publish /p:ContinuousIntegrationBuild=true --configuration Release --output /app/publish/health --no-restore "Applications/HealthCheck/src/HealthCheck.csproj" FROM dhi.io/aspnetcore:10.0.10-debian13@sha256:36f56fe5ec5c2ac63d1451b43eb6ef00f981c9454159e3a45087a2b9071d405d diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/AGENTS.md b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/AGENTS.md new file mode 100644 index 0000000000..39d3f1db75 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/AGENTS.md @@ -0,0 +1,96 @@ +# OpenID4VP Verifier – Instructions for AI Agents + +This file applies to the entire `OpenId4VpVerifierSite` directory. Keep it up to date with every change to the feature. If the architecture, validation rules, supported formats, integration points, build steps, tests, or design requirements change, update this file as part of the same change. + +## Purpose and Flow + +This Vite/TypeScript bundle adds browser-side verification of presented credentials to the Consumer API page `/r/{referenceId}`. + +1. `referenceContent.ts` reads the key from the URL fragment. The fragment is base64url-encoded and contains `algorithm|key|forIdentity|passwordProtection`; currently, only algorithm `3` (`XCHACHA20_POLY1305`) is supported. +2. The encrypted content is loaded through the Consumer API's token or RelationshipTemplate endpoint and decrypted in the browser. +3. Only content with `@type: "TokenContentVerifiablePresentation"` is treated as a presentation. Otherwise, the regular onboarding page remains visible. +4. `verification.ts` validates `value`. The reference ID is used as the expected nonce, while the audience is currently hard-coded to `defaultPresentationAudience`. +5. `main.ts` connects the loading and validation logic to the existing DOM and displays the status and credential contents. + +The URL fragment is not sent to the server by the browser. Do not move the decryption key it contains into query parameters or server-side requests. + +## File Responsibilities + +- `src/referenceContent.ts`: Parse the reference fragment, select API endpoints, and load and decrypt the encrypted content. Errors result in no VP content being returned. +- `src/verification.ts`: Validate and extract the credential data to be displayed. The public entry point is `validatePresentedCredential(presentation, options)`; input in, `VerificationOutcome` out, with no DOM dependency. +- `src/main.ts`: UI orchestration, DOM access, German text, mapping `PresentationValidationErrorCode` values to user-friendly messages, and merging optional `displayInformation`. +- `src/verificationKeyManagement.ts`: Browser KMS exclusively for signature verification with public JWKs. No key generation, private-key import, or signing. +- `src/verificationStorage.ts`: Ephemeral storage/filesystem adapters required to initialize Credo in the browser. No verifier data is stored persistently. +- `src/styles.css`: Verifier styles only; scope selectors beneath `.openid4vp-verifier` or `#openid4vp-verifier-root`. +- `test/verification.test.ts`: Real cryptographic unit tests of the validation logic without mocks. + +## Validation Rules + +Compact SD-JWT VCs with Key Binding as well as JWT- and JSON-LD-based VCs/VPs are currently supported to the extent that Credo can verify them. A successful status requires: + +- at least one presentation or credential; +- the expected nonce and audience to be present; +- valid cryptographic signatures; +- JWT and JSON-LD credentials to be embedded in a request-bound Verifiable Presentation rather than supplied as standalone credentials; +- for SD-JWT, a valid issuer signature, valid disclosures, a valid holder key-binding signature, and a matching `sd_hash`; +- the nonce and audience to match the current verification process; +- a supported SD-JWT type and the required `vct` field; +- a validity period that has already started and has not yet expired; +- at least one embedded credential in a VP; +- every individual item to be valid when multiple presentations or credentials are supplied. + +Signer keys are resolved from embedded JWKs, `x5c`, or supported DID URLs. The Credo configuration includes resolvers for `did:key`, `did:jwk`, and `did:web`. + +Important intentional limitations: + +- Credential status or revocation is not currently checked (`verifyCredentialStatus: false`). +- With `x5c`, the certificate's public key is used to verify the signature; neither the certificate chain nor trust in the issuer is validated. +- A valid signature alone therefore does not mean that the issuer is trusted from a business perspective. + +Do not change these limitations casually. Security-relevant extensions require appropriate positive and negative tests. + +## Error Handling + +Validation results do not contain a free-text error message, but a value from `PresentationValidationErrorCode`. For new error cases, add: + +1. a distinct enum value in `verification.ts`; +2. the correct mapping in the validation path; +3. a short, non-technical German UI message in `validationErrorMessages` in `main.ts`; +4. at least one unit test that asserts the exact error code. + +Unknown errors must not be treated as valid and are mapped to `VerificationFailed`. Internal technical details, library errors, key material, and complete presentations must not appear in visible error messages. + +## UI and Design Requirements + +The markup is not located in this Vite project, but in `../Views/AppOnboarding/AppOnboarding.cshtml`. `main.ts` expects the `data-*` elements defined there. Change the markup, TypeScript queries, and CSS together when modifying this DOM contract. Do not create missing elements dynamically as a fallback; the verifier must not initialize when the DOM is incomplete. + +All visible text, including error and accessibility text, must be in German. Error messages should be understandable and use minimal technical language. Use `textContent`, not `innerHTML`, for data from presentations. + +All presented domain claims should be displayed. Technical metadata listed in `technicalClaimNames` and image fields listed in `imageClaimNames` are intentionally excluded or displayed separately. Hide missing values; do not add invented sample data or display fallbacks. The issuer also controls the “verifiziert durch” section. Optional `displayInformation` from the token can specify the title, logo, and colors. + +Design source: [Frosch Wallet App in Figma](https://www.figma.com/design/D15DcZItr1P4lCa61vfOWN/Frosch-Wallet-App?node-id=73604-148644&m=dev). For design changes, use the Figma plugin and consider the desktop screens directly below the linked element; the first two screens there are only for the mobile app. The blue outer frame in Figma represents a smartphone and is not part of the web UI. + +## Embedding and Build + +- `vite.config.ts` generates fixed filenames under `dist/openid4vp-verifier/assets/verifier.{js,css}` with the base path `/openid4vp-verifier/`. +- `../ConsumerApi.csproj` runs `npm ci` and `npm run build` during regular builds and copies the result to `../wwwroot/openid4vp-verifier`. +- `../Dockerfile` builds the bundle in a dedicated Node stage and copies it into the Consumer API image. +- `../Views/AppOnboarding/AppOnboarding.cshtml` includes CSS and preloads and dynamically imports JavaScript through `IFileVersionProvider` with cache busting. The server-rendered verifier remains visible during normal loading, while import or initialization failures restore the onboarding page. +- `../../../../.github/workflows/test.yml` installs the dependencies and runs `npm test` in the unit-test job. + +`node_modules/`, `dist/`, and `../wwwroot/openid4vp-verifier/` are generated or copied artifacts. Do not edit or commit them directly. Changes belong in `src/`, the Razor markup, or the build configuration. When dependencies change, `package-lock.json` must be updated together with `package.json`. + +## Tests and Local Verification + +Run at least the following commands in the verifier directory: + +```sh +npm ci +npm run typecheck +npm test +npm run build +``` + +Tests for `validatePresentedCredential` should remain pure input/output tests without mocks. Generate signed test presentations with real test keys and inject a fixed time through `options.now` so that time checks are deterministic. When making changes, cover the success case as well as tampering, incorrect binding values, time boundaries, missing required data, and arrays containing partially invalid presentations. + +When changing the markup or Consumer API embedding, also run the affected .NET integration tests or at least build `../ConsumerApi.csproj`. Do not confuse known warnings from transitive cryptography dependencies during the Vite build with errors, but investigate and document new warnings. diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/README.md b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/README.md new file mode 100644 index 0000000000..86e8aead8a --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/README.md @@ -0,0 +1,29 @@ +# OpenID4VP Verifier Site + +This Vite application builds the static OpenID4VP verifier bundle that is embedded into the Consumer API `/r/{referenceId}` onboarding page. + +## Build + +```sh +npm install +npm run build +``` + +The Vite build output is written to `dist/openid4vp-verifier` and exposes fixed asset names under `/openid4vp-verifier/assets/`. + +During the Consumer API build, MSBuild runs the Vite build and copies the generated files into `../wwwroot/openid4vp-verifier`. The copied `wwwroot` files are build artifacts and are not committed. + +## Input + +The bundle reads the NMSHD reference fragment from the current `/r/{referenceId}` URL. The fragment is base64url encoded and contains `algorithm|key|forIdentity|passwordProtection`. For this first version, only algorithm `3` (`XCHACHA20_POLY1305`) is supported. + +The referenced Token or RelationshipTemplate content is fetched from the Consumer API. If the decrypted JSON has `@type: "TokenContentVerifiablePresentation"`, its `value` is verified as a presented credential using the reference id as expected nonce and `defaultPresentationAudience` as expected audience. Otherwise, the original onboarding page is shown. + +Credential display values are extracted from the Verifiable Presentation. Missing values are omitted instead of being replaced with sample/default credential data. + +Optional validation parameters: + +- `nonce` or `expected_nonce`: expected presentation challenge. +- `audience` or `client_id`: expected presentation audience. If omitted, the current origin is used. + +The implementation performs a minimal browser-side validation using `@credo-ts/core` and `@credo-ts/openid4vc` types. NMSHD token content is decrypted with `@nmshd/crypto`. Credential status checks are disabled for this first version. diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package-lock.json b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package-lock.json new file mode 100644 index 0000000000..7e153a47d8 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package-lock.json @@ -0,0 +1,2798 @@ +{ + "name": "@nmshd/backbone-consumerapi-openid4vp-verifier", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nmshd/backbone-consumerapi-openid4vp-verifier", + "dependencies": { + "@credo-ts/core": "0.7.0", + "@fontsource/work-sans": "5.2.8", + "@js-soft/ts-serval": "2.0.17", + "@nmshd/crypto": "3.0.6", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@sd-jwt/core": "0.19.0", + "events": "3.3.0", + "reflect-metadata": "0.2.2" + }, + "devDependencies": { + "typescript": "5.9.3", + "vite": "8.2.2", + "vitest": "5.0.0" + } + }, + "node_modules/@animo-id/mdoc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@animo-id/mdoc/-/mdoc-0.5.2.tgz", + "integrity": "sha512-EQVsNOOeXFfBaEHkiKoh24jbSEQ1MORB/kUu0rnNrAEETpY5GK/H9iWevYFdmNDIqQTIEJlkU7S+sIj3pe66eA==", + "license": "Apache-2.0", + "dependencies": { + "compare-versions": "^6.1.1" + } + }, + "node_modules/@animo-id/pex": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@animo-id/pex/-/pex-6.1.1.tgz", + "integrity": "sha512-my3g9Divea1sseZRzgD2tnrv0ett9fTlyoZp1x9nSjyRwtai/BBnFQigknMFscJuG6vnwYNcaj6TFQprw+v5xw==", + "license": "Apache-2.0", + "dependencies": { + "@animo-id/mdoc": "^0.5.2", + "@astronautlabs/jsonpath": "^1.1.2", + "@sd-jwt/decode": "^0.7.2", + "@sd-jwt/present": "^0.7.2", + "@sd-jwt/types": "^0.7.2", + "@sphereon/pex-models": "^2.3.2", + "@sphereon/ssi-types": "0.33.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "jwt-decode": "^3.1.2", + "nanoid": "^3.3.7", + "uint8arrays": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@animo-id/pex/node_modules/@sd-jwt/decode": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.7.2.tgz", + "integrity": "sha512-dan2LSvK63SKwb62031G4r7TE4TaiI0EK1KbPXqS+LCXNkNDUHqhtYp9uOpj+grXceCsMtMa2f8VnUfsjmwHHg==", + "deprecated": "Merged into @sd-jwt/core (>= 0.20.0). Security: GHSA-f9j6-8p6x-r9j6.", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.7.2", + "@sd-jwt/utils": "0.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@animo-id/pex/node_modules/@sd-jwt/present": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/present/-/present-0.7.2.tgz", + "integrity": "sha512-mQV85u2+mLLy2VZ9Wx2zpaB6yTDnbhCfWkP7eeCrzJQHBKAAHko8GrylEFmLKewFIcajS/r4lT/zHOsCkp5pZw==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/decode": "0.7.2", + "@sd-jwt/types": "0.7.2", + "@sd-jwt/utils": "0.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@animo-id/pex/node_modules/@sd-jwt/types": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.7.2.tgz", + "integrity": "sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@animo-id/pex/node_modules/@sd-jwt/utils": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.7.2.tgz", + "integrity": "sha512-aMPY7uHRMgyI5PlDvEiIc+eBFGC1EM8OCQRiEjJ8HGN0pajWMYj0qwSw7pS90A49/DsYU1a5Zpvb7nyjgGH0Yg==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.7.2", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@astronautlabs/jsonpath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@astronautlabs/jsonpath/-/jsonpath-1.1.2.tgz", + "integrity": "sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A==", + "license": "MIT", + "dependencies": { + "static-eval": "2.0.2" + } + }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.2.tgz", + "integrity": "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.2.tgz", + "integrity": "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.2.tgz", + "integrity": "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.2.tgz", + "integrity": "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.2.tgz", + "integrity": "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.2.tgz", + "integrity": "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@credo-ts/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@credo-ts/core/-/core-0.7.0.tgz", + "integrity": "sha512-J30MqquFaV2NzB1nxl5Nk43nueAsvSt4l+SHQI5fLQ+OwuY42iDmuJZvDmBXO7anoYBCp4ABsF78a1I7AEuL5Q==", + "license": "Apache-2.0", + "dependencies": { + "@animo-id/pex": "^6.1.1", + "@astronautlabs/jsonpath": "^1.1.2", + "@digitalcredentials/jsonld": "^9.0.0", + "@digitalcredentials/jsonld-signatures": "^12.0.1", + "@digitalcredentials/vc": "^10.0.2", + "@multiformats/base-x": "^4.0.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", + "@owf/mdoc": "^0.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/x509": "^1.14.3", + "@scure/base": "^2.0.0", + "@sd-jwt/core": "^0.19.0", + "@sd-jwt/decode": "^0.19.0", + "@sd-jwt/jwt-status-list": "^0.19.0", + "@sd-jwt/present": "^0.19.0", + "@sd-jwt/sd-jwt-vc": "^0.19.0", + "@sd-jwt/types": "^0.19.0", + "@sd-jwt/utils": "^0.19.0", + "@sphereon/pex-models": "^2.3.2", + "@sphereon/ssi-types": "0.33.0", + "@stablelib/ed25519": "^2.0.2", + "@types/ws": "^8.18.1", + "@verifiables/request-converter": "^0.2.0", + "class-transformer": "0.5.1", + "class-validator": "^0.14.3", + "dcql": "^3.0.0", + "did-resolver": "^4.1.0", + "ec-compression": "0.0.1-alpha.12", + "lru_map": "^0.4.1", + "make-error": "^1.3.6", + "object-inspect": "^1.13.4", + "reflect-metadata": "0.2.2", + "rxjs": "^7.8.2", + "tsyringe": "^4.10.0", + "uuid": "^13.0.0", + "varint": "^6.0.0", + "web-did-resolver": "^2.0.32", + "webcrypto-core": "^1.8.1", + "zod": "^4.3.6" + } + }, + "node_modules/@digitalbazaar/security-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/security-context/-/security-context-1.0.1.tgz", + "integrity": "sha512-0WZa6tPiTZZF8leBtQgYAfXQePFQp2z5ivpCEN/iZguYYZ0TB9qRmWtan5XH6mNFuusHtMcyIzAcReyE6rZPhA==", + "license": "BSD-3-Clause" + }, + "node_modules/@digitalcredentials/credentials-v2-context": { + "version": "0.0.1-beta.0", + "resolved": "https://registry.npmjs.org/@digitalcredentials/credentials-v2-context/-/credentials-v2-context-0.0.1-beta.0.tgz", + "integrity": "sha512-i0AQXFnMeOqf2uKNBUcnN78mO8L9H91QKMdpDqsgDYzTIKGLnNCOOxbRbrJOimhR+soYO64xn54U8/R7Qx0nyA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalcredentials/http-client": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@digitalcredentials/http-client/-/http-client-5.0.4.tgz", + "integrity": "sha512-TbbfFauhd5fUAcZ/6TDenTuYqkzqdEhxYk1Rqicxcz33B9rzvJcmT+/81KF9DrUXmu6gpoqCranT2UkZftsxZA==", + "license": "BSD-3-Clause", + "dependencies": { + "ky": "^1.0.1", + "undici": "^6.6.2" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@digitalcredentials/jsonld": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@digitalcredentials/jsonld/-/jsonld-9.0.0.tgz", + "integrity": "sha512-lWVpg65xQbi4lvXWs3BezCwPrGDAllFjHwTBUWJCoKsfGzxtxTwbRNXpfhii5psQ2vvpjPXSI+y0uyRNOzr4cw==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalcredentials/http-client": "^5.0.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@digitalcredentials/jsonld-signatures": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-12.0.1.tgz", + "integrity": "sha512-KdyE7Bex+boqKRbvxofRyuI09BD6/8hQGSC5qbrXEmHd0oSLqePqKBM2pu9UZnzjxMZrN9sB3RqHxpijYEFh/w==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/security-context": "^1.0.0", + "@digitalcredentials/jsonld": "^9.0.0", + "fast-text-encoding": "^1.0.3", + "serialize-error": "^8.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalcredentials/open-badges-context": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@digitalcredentials/open-badges-context/-/open-badges-context-2.1.0.tgz", + "integrity": "sha512-VK7X5u6OoBFxkyIFplNqUPVbo+8vFSAEoam8tSozpj05KPfcGw41Tp5p9fqMnY38oPfwtZR2yDNSctj/slrE0A==", + "license": "MIT" + }, + "node_modules/@digitalcredentials/vc": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@digitalcredentials/vc/-/vc-10.0.2.tgz", + "integrity": "sha512-Mmts8WtAQmgdrSurQv+SFZNozNgvPzsruWQNIBlmfrlJ7QSyCoO7jybSnq43EuLm3UcqyqSb2mLHwAza310mhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalcredentials/credentials-v2-context": "^0.0.1-beta.0", + "@digitalcredentials/jsonld": "^9.0.0", + "@digitalcredentials/jsonld-signatures": "^12.0.1", + "@digitalcredentials/open-badges-context": "^2.1.0", + "credentials-context": "^2.0.0", + "ed25519-signature-2018-context": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/work-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/work-sans/-/work-sans-5.2.8.tgz", + "integrity": "sha512-6LaHjVVgts+rnrcqvEkP2+iUB/jw1oDSYsGO0+TltAhnWki9Hnf/UGpgMQh2jcm0GEH8VqCPnq4PpmHLFzxXtQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-joda/core": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz", + "integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA==", + "license": "BSD-3-Clause" + }, + "node_modules/@js-joda/timezone": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@js-joda/timezone/-/timezone-2.3.0.tgz", + "integrity": "sha512-DHXdNs0SydSqC5f0oRJPpTcNfnpRojgBqMCFupQFv6WgeZAjU3DBx+A7JtaGPP3dHrP2Odi2N8Vf+uAm/8ynCQ==", + "license": "BSD-3-Clause", + "peerDependencies": { + "@js-joda/core": ">=1.11.0" + } + }, + "node_modules/@js-soft/ts-serval": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@js-soft/ts-serval/-/ts-serval-2.0.17.tgz", + "integrity": "sha512-wHfwrDa7cOtdM1QQ0FCuLaqBqJh+emN187NTIQL8wAHh5sQEk3EDV6teOBLA5bDUXajInmczG84xuznaGY9/TQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "reflect-metadata": "^0.2.2" + } + }, + "node_modules/@multiformats/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==", + "license": "MIT" + }, + "node_modules/@nmshd/crypto": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@nmshd/crypto/-/crypto-3.0.6.tgz", + "integrity": "sha512-Ge36ZkanzmUMXuSSvJJdmNhhTCw7z7JCGmGY1TdWyHG7Az4ImPWkw+kuL8N5UaC29hbAZx6kzw75dLFhQJx4Aw==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "libsodium-wrappers-sumo": "0.8.4" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@owf/mdoc": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@owf/mdoc/-/mdoc-0.6.0.tgz", + "integrity": "sha512-vJL62hzjcSweBjDD9eRUH1RIMSzZUtHKjVFMMo50oHPLO9ZVZbQABFe9IFoa5AgXkwfv27HjnFv/eRbkOrlsUA==", + "license": "Apache-2.0", + "dependencies": { + "cbor-x": "^1.6.4", + "zod": "^4.3.6", + "zod-validation-error": "^5.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", + "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", + "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", + "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", + "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", + "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", + "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", + "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", + "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", + "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", + "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", + "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sd-jwt/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/core/-/core-0.19.0.tgz", + "integrity": "sha512-/FeuQjzQFtnxDsHF64Bw+3uixVogNkXlMGBo1CKWrxB/OLNqtGXQZplKBRZMJWgnOJEqDwD6750wrOxsVly6mg==", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/decode": "0.19.0", + "@sd-jwt/present": "0.19.0", + "@sd-jwt/types": "0.19.0", + "@sd-jwt/utils": "0.19.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/decode": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.19.0.tgz", + "integrity": "sha512-gRfpJseFRy3bFMdlmJjVjuEcLNuekpiZJD2F2luJIKVlk26AEjZSJf6377vwNySa8hb+i4MZDwdy14lcTTmqtA==", + "deprecated": "Merged into @sd-jwt/core (>= 0.20.0). Security: GHSA-f9j6-8p6x-r9j6.", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.19.0", + "@sd-jwt/utils": "0.19.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/jwt-status-list": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/jwt-status-list/-/jwt-status-list-0.19.0.tgz", + "integrity": "sha512-Xhh0n0DKe3paEtNNQSScy4dtA0ZVKF2OrmDcIB2jm77Doh4xsSs1pOseGEWBs9fmAe5Y0m082wgkmQnWEV53IQ==", + "deprecated": "Package moved to @owf/token-status-list, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.19.0", + "@sd-jwt/utils": "0.19.0", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/present": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/present/-/present-0.19.0.tgz", + "integrity": "sha512-WXDwqwUXmtyj7YZ5c+wb26ZBeVOsKXCbCI7s1pRH9ngIjFNDGgAZoVCOmLq8pPgWSJzOTgJe3ErO2k63ZwhyeQ==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/decode": "0.19.0", + "@sd-jwt/types": "0.19.0", + "@sd-jwt/utils": "0.19.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.19.0.tgz", + "integrity": "sha512-sjX9/E32X0HNpOM5b9np2fHSS9CJu72JDkGEuaHTfZTLTmgh1Cj6KhP7kwI1Bxhw0CCt7tjhrAmMvEEIbgIwVg==", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/core": "0.19.0", + "@sd-jwt/jwt-status-list": "0.19.0", + "@sd-jwt/utils": "0.19.0", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/types": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.19.0.tgz", + "integrity": "sha512-nfuC9zRLKe7o4HSvc+N4ojWRAxo4JGfgcNWpR7bJloLUlnE9eQuu9h9pEaJZht7KRwMpGorNTIdYpoi1btuiew==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@sd-jwt/utils": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.19.0.tgz", + "integrity": "sha512-bDwDRjfxMBNOsAXY8q8hnxQq7jdOWxrdqTK926Mxt8DN+ttXbXbZIPLwSh84M90WP0V7+WdkXlZD31iISzUR3w==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.19.0", + "js-base64": "^3.7.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sphereon/kmp-mdoc-core": { + "version": "0.2.0-SNAPSHOT.26", + "resolved": "https://registry.npmjs.org/@sphereon/kmp-mdoc-core/-/kmp-mdoc-core-0.2.0-SNAPSHOT.26.tgz", + "integrity": "sha512-QXJ6R8ENiZV2rPMbn06cw5JKwqUYN1kzVRbYfONqE1PEXx1noQ4md7uxr2zSczi0ubKkNcbyYDNtIMTZIhGzmQ==", + "dependencies": { + "@js-joda/core": "5.6.3", + "@js-joda/timezone": "2.3.0", + "format-util": "^1.0.5" + } + }, + "node_modules/@sphereon/pex-models": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.3.2.tgz", + "integrity": "sha512-foFxfLkRwcn/MOp/eht46Q7wsvpQGlO7aowowIIb5Tz9u97kYZ2kz6K2h2ODxWuv5CRA7Q0MY8XUBGE2lfOhOQ==", + "license": "Apache-2.0" + }, + "node_modules/@sphereon/ssi-types": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@sphereon/ssi-types/-/ssi-types-0.33.0.tgz", + "integrity": "sha512-OQnWLKZQU6lHlMw3JS5308q5Kctv1eT/NNQthGNXETarsgJFiD711pWMOJvi0p5kLpU8N1e7/okaH3mXsVAW0A==", + "license": "Apache-2.0", + "dependencies": { + "@noble/hashes": "1.6.1", + "@sd-jwt/decode": "^0.9.2", + "@sphereon/kmp-mdoc-core": "0.2.0-SNAPSHOT.26", + "debug": "^4.3.5", + "events": "^3.3.0", + "jwt-decode": "^4.0.0", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@sphereon/ssi-types/node_modules/@noble/hashes": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sphereon/ssi-types/node_modules/@sd-jwt/decode": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.9.2.tgz", + "integrity": "sha512-jHY7hqk7EMkp6E2cB13QmXvRZN7njvAveVh+zKKy0kxpAM7DmcR4TqcDA4mc5y4lP8zWFUgbk7oGLCx2wiBq+w==", + "deprecated": "Merged into @sd-jwt/core (>= 0.20.0). Security: GHSA-f9j6-8p6x-r9j6.", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.9.2", + "@sd-jwt/utils": "0.9.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sphereon/ssi-types/node_modules/@sd-jwt/types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.9.2.tgz", + "integrity": "sha512-eV/MY80ekeTrqaohzPpkrgwPki6Igx69D6RniduV1Ehv6o/zaJQ2F0hY/RqBAkJhQtBQoOzouwKYHme40k1Dlw==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sphereon/ssi-types/node_modules/@sd-jwt/utils": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.9.2.tgz", + "integrity": "sha512-GpTD0isav2f+JyMyzeyf6wV3nYcD5e3oL+sVVr/61Y3oaIBGMWLtGGGhBRMCimSnd8kbb0P9jLxvp7ioASk6vw==", + "deprecated": "Package got merged into @sd-jwt/core, please use this instead", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.9.2", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sphereon/ssi-types/node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@stablelib/binary": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-2.0.1.tgz", + "integrity": "sha512-U9iAO8lXgEDONsA0zPPSgcf3HUBNAqHiJmSHgZz62OvC3Hi2Bhc5kTnQ3S1/L+sthDTHtCMhcEiklmIly6uQ3w==", + "license": "MIT", + "dependencies": { + "@stablelib/int": "^2.0.1" + } + }, + "node_modules/@stablelib/ed25519": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stablelib/ed25519/-/ed25519-2.1.0.tgz", + "integrity": "sha512-8GLWoJur9nJiErABKHs5MjceSiYJJ2n8QP9k8Q5x6GWU2y5oAQ6qzPh8kCgQy5aHlUxcmRA1frQ5Gu2bHoIDPw==", + "license": "MIT", + "dependencies": { + "@stablelib/random": "^2.0.1", + "@stablelib/sha512": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stablelib/hash/-/hash-2.0.0.tgz", + "integrity": "sha512-u3WPSqGido8lwJuMcrBgM5K54LrPGhkWAdtsyccf7dGsLixAZUds77zOAbu7bvKPwQlmoByH0txBi5rTmEKuHg==", + "license": "MIT" + }, + "node_modules/@stablelib/int": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-2.0.1.tgz", + "integrity": "sha512-Ht63fQp3wz/F8U4AlXEPb7hfJOIILs8Lq55jgtD7KueWtyjhVuzcsGLSTAWtZs3XJDZYdF1WcSKn+kBtbzupww==", + "license": "MIT" + }, + "node_modules/@stablelib/random": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-2.0.1.tgz", + "integrity": "sha512-W6GAtXEEs7r+dSbuBsvoFmlyL3gLxle41tQkjKu17dDWtDdjhVUbtRfRCQcCUeczwkgjQxMPopgwYEvxXtHXGw==", + "license": "MIT", + "dependencies": { + "@stablelib/binary": "^2.0.1", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/sha512": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/sha512/-/sha512-2.0.1.tgz", + "integrity": "sha512-DUNe5cbnoH3sSIN+MG04RvTCLXtkbyy/SnQxiNO+GgF/KSXkkUSlF6mUVvCUdZBZ2X3NgogR+tAvaRSn8wxnLw==", + "license": "MIT", + "dependencies": { + "@stablelib/binary": "^2.0.1", + "@stablelib/hash": "^2.0.0", + "@stablelib/wipe": "^2.0.1" + } + }, + "node_modules/@stablelib/wipe": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-2.0.1.tgz", + "integrity": "sha512-1eU2K9EgOcV4qc9jcP6G72xxZxEm5PfeI5H55l08W95b4oRJaqhmlWRc4xZAm6IVSKhVNxMi66V67hCzzuMTAg==", + "license": "MIT" + }, + "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/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/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@verifiables/request-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@verifiables/request-converter/-/request-converter-0.2.0.tgz", + "integrity": "sha512-afhFYBSYf0zqLkHu1xlLJ8DUAV/ved5yD9MKnPPaG9tH68winV10eBBH4/2eoi2qK6hvAtWPjoZ+1pRF6jLUJw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "dcql": ">=2.0.0" + }, + "peerDependenciesMeta": { + "dcql": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/canonicalize": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", + "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==", + "license": "Apache-2.0" + }, + "node_modules/cbor-extract": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.2.tgz", + "integrity": "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", + "@cbor-extract/cbor-extract-linux-arm": "2.2.2", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", + "@cbor-extract/cbor-extract-linux-x64": "2.2.2", + "@cbor-extract/cbor-extract-win32-x64": "2.2.2" + } + }, + "node_modules/cbor-x": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.6.tgz", + "integrity": "sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.2" + } + }, + "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/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "license": "MIT" + }, + "node_modules/credentials-context": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/credentials-context/-/credentials-context-2.0.0.tgz", + "integrity": "sha512-/mFKax6FK26KjgV2KW2D4YqKgoJ5DVJpNt87X2Jc9IxT2HBMy7nEIlc+n7pEi+YFFe721XqrvZPd+jbyyBjsvQ==", + "license": "SEE LICENSE IN LICENSE.md" + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/dcql": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dcql/-/dcql-3.0.0.tgz", + "integrity": "sha512-Z1Iq+tDQqPCqWzQVevUVOaYa+NfoplwrwSPvV0JINZpniiN5jq32C4CXsrmELAgSa5Ja7hBKiuYkpZjQFkeHyw==", + "license": "Apache-2.0", + "dependencies": { + "valibot": "1.2.0" + } + }, + "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/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==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/did-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz", + "integrity": "sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==", + "license": "Apache-2.0" + }, + "node_modules/ec-compression": { + "version": "0.0.1-alpha.12", + "resolved": "https://registry.npmjs.org/ec-compression/-/ec-compression-0.0.1-alpha.12.tgz", + "integrity": "sha512-rfsgHPnS/q8SiiJyaJ5w+qpkLtvtvEFOXoh40VmjH+Xs1pjzCCKwhd9Un3BQV+1+Qg0es5VQnzwZVJ7fjzfN+A==" + }, + "node_modules/ed25519-signature-2018-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ed25519-signature-2018-context/-/ed25519-signature-2018-context-1.1.0.tgz", + "integrity": "sha512-ppDWYMNwwp9bploq0fS4l048vHIq41nWsAbPq6H4mNVx9G/GxW3fwg4Ln0mqctP13MoEpREK7Biz8TbVVdYXqA==", + "license": "BSD-3-Clause" + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==", + "license": "Apache-2.0" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/format-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", + "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-base64": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.3.tgz", + "integrity": "sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT" + }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==", + "license": "MIT" + }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.12", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.12.tgz", + "integrity": "sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==", + "license": "MIT" + }, + "node_modules/libsodium-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.4.tgz", + "integrity": "sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.4.tgz", + "integrity": "sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lru_map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", + "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "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/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "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/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", + "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.148.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.7", + "@rolldown/binding-android-arm64": "1.2.7", + "@rolldown/binding-darwin-arm64": "1.2.7", + "@rolldown/binding-darwin-x64": "1.2.7", + "@rolldown/binding-freebsd-x64": "1.2.7", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", + "@rolldown/binding-linux-arm64-gnu": "1.2.7", + "@rolldown/binding-linux-arm64-musl": "1.2.7", + "@rolldown/binding-linux-ppc64-gnu": "1.2.7", + "@rolldown/binding-linux-s390x-gnu": "1.2.7", + "@rolldown/binding-linux-x64-gnu": "1.2.7", + "@rolldown/binding-linux-x64-musl": "1.2.7", + "@rolldown/binding-openharmony-arm64": "1.2.7", + "@rolldown/binding-win32-arm64-msvc": "1.2.7", + "@rolldown/binding-win32-x64-msvc": "1.2.7" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/web-did-resolver": { + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/web-did-resolver/-/web-did-resolver-2.0.32.tgz", + "integrity": "sha512-L91/ApTmDjgzS0UDstTKn3kN/1hlQBnVcUN8K29e3xhVBpPktHYC6uvVAQ8ohbIg9D6wrrbaBQvfRArDxgJG2g==", + "license": "Apache-2.0", + "dependencies": { + "cross-fetch": "^4.1.0", + "did-resolver": "^4.1.0" + } + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-5.0.0.tgz", + "integrity": "sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package.json b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package.json new file mode 100644 index 0000000000..9338f40f38 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/package.json @@ -0,0 +1,28 @@ +{ + "name": "@nmshd/backbone-consumerapi-openid4vp-verifier", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite --host 127.0.0.1", + "preview": "vite preview --host 127.0.0.1", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@credo-ts/core": "0.7.0", + "@fontsource/work-sans": "5.2.8", + "@js-soft/ts-serval": "2.0.17", + "@nmshd/crypto": "3.0.6", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@sd-jwt/core": "0.19.0", + "events": "3.3.0", + "reflect-metadata": "0.2.2" + }, + "devDependencies": { + "typescript": "5.9.3", + "vite": "8.2.2", + "vitest": "5.0.0" + } +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/main.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/main.ts new file mode 100644 index 0000000000..f8673973dd --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/main.ts @@ -0,0 +1,323 @@ +import "@fontsource/work-sans/latin-400.css"; +import "@fontsource/work-sans/latin-500.css"; +import "@fontsource/work-sans/latin-600.css"; +import "reflect-metadata"; +import { tryLoadVerifiablePresentationTokenContent } from "./referenceContent"; +import "./styles.css"; +import { PresentationValidationErrorCode, type VerificationDisplay, validatePresentedCredential } from "./verification"; + +type CredentialDisplay = VerificationDisplay & { + backgroundColor?: string; + logo?: string; + textColor?: string; +}; + +type VerificationStatus = "loading" | "valid" | "invalid"; + +type VerifierElements = { + closeButton: HTMLButtonElement; + closedView: HTMLElement; + content: HTMLElement; + credentialCard: HTMLElement; + credentialLogo: HTMLImageElement; + additionalClaims: HTMLElement; + details: HTMLElement; + detailRows: Record; + detailValues: Record; + error: HTMLElement; + invalidMark: HTMLElement; + loadingMark: HTMLElement; + portrait: HTMLImageElement; + resultText: HTMLElement; + root: HTMLElement; + title: HTMLElement; + validMark: HTMLElement; + verifiedBy: HTMLElement; + verifiedByIssuer: HTMLElement; +}; + +const credentialDisplayFields = ["backgroundColor", "createdAt", "expiresAt", "issuer", "logo", "portrait", "publicKey", "textColor", "title"] as const; +const detailFields = ["createdAt", "expiresAt", "issuer", "publicKey"] as const; +type DetailField = (typeof detailFields)[number]; +const validationErrorMessages: Record = { + [PresentationValidationErrorCode.AudienceMismatch]: "Dieser Nachweis wurde für eine andere Prüfung erstellt. (Fehlercode: AudienceMismatch)", + [PresentationValidationErrorCode.CredentialExpired]: "Der präsentierte Nachweis ist abgelaufen. (Fehlercode: CredentialExpired)", + [PresentationValidationErrorCode.CredentialNotYetValid]: "Der präsentierte Nachweis ist noch nicht gültig. (Fehlercode: CredentialNotYetValid)", + [PresentationValidationErrorCode.InvalidDisclosure]: "Die Angaben im Nachweis konnten nicht bestätigt werden. (Fehlercode: InvalidDisclosure)", + [PresentationValidationErrorCode.InvalidPresentationBinding]: "Der Nachweis gehört nicht zu dieser Präsentation. (Fehlercode: InvalidPresentationBinding)", + [PresentationValidationErrorCode.InvalidSignature]: "Die Echtheit des Nachweises konnte nicht bestätigt werden. (Fehlercode: InvalidSignature)", + [PresentationValidationErrorCode.MissingAudience]: "Die Prüfung konnte nicht gestartet werden. Bitte öffnen Sie den Link erneut. (Fehlercode: MissingAudience)", + [PresentationValidationErrorCode.MissingChallenge]: "Die Prüfung konnte nicht gestartet werden. Bitte öffnen Sie den Link erneut. (Fehlercode: MissingChallenge)", + [PresentationValidationErrorCode.MissingCredential]: "Es wurde kein Nachweis übermittelt. (Fehlercode: MissingCredential)", + [PresentationValidationErrorCode.MissingCredentialType]: "Die Art des Nachweises konnte nicht erkannt werden. (Fehlercode: MissingCredentialType)", + [PresentationValidationErrorCode.MissingHolderVerificationKey]: "Die Echtheit des Nachweises konnte nicht bestätigt werden. (Fehlercode: MissingHolderVerificationKey)", + [PresentationValidationErrorCode.MissingIssuerVerificationKey]: "Die Echtheit des Nachweises konnte nicht bestätigt werden. (Fehlercode: MissingIssuerVerificationKey)", + [PresentationValidationErrorCode.MissingKeyBinding]: "Der Nachweis konnte dieser Präsentation nicht eindeutig zugeordnet werden. (Fehlercode: MissingKeyBinding)", + [PresentationValidationErrorCode.MissingPresentation]: "Es wurde kein Nachweis übermittelt. (Fehlercode: MissingPresentation)", + [PresentationValidationErrorCode.NonceMismatch]: "Diese Präsentation wurde für eine andere Anfrage erstellt. (Fehlercode: NonceMismatch)", + [PresentationValidationErrorCode.UnsupportedHolderDidUrl]: "Dieser Nachweis kann hier nicht geprüft werden. (Fehlercode: UnsupportedHolderDidUrl)", + [PresentationValidationErrorCode.UnsupportedIssuerDidUrl]: "Dieser Nachweis kann hier nicht geprüft werden. (Fehlercode: UnsupportedIssuerDidUrl)", + [PresentationValidationErrorCode.UnsupportedPresentationFormat]: "Dieser Nachweis kann hier nicht geprüft werden. (Fehlercode: UnsupportedPresentationFormat)", + [PresentationValidationErrorCode.UnsupportedSdJwtType]: "Dieser Nachweis kann hier nicht geprüft werden. (Fehlercode: UnsupportedSdJwtType)", + [PresentationValidationErrorCode.UnsupportedSignatureAlgorithm]: "Dieser Nachweis kann hier nicht geprüft werden. (Fehlercode: UnsupportedSignatureAlgorithm)", + [PresentationValidationErrorCode.VerificationFailed]: "Bei der Prüfung ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. (Fehlercode: VerificationFailed)" +}; +const rootElement = document.querySelector("#openid4vp-verifier-root"); +const verifierElements = rootElement ? getVerifierElements(rootElement) : undefined; + +if (rootElement && verifierElements) { + document.body.classList.add("openid4vp-verifier-visible"); + setStatus(verifierElements, "loading"); + + void initialize(rootElement, verifierElements).catch((error) => { + console.error("Der Nachweisprüfer konnte nicht gestartet werden.", error); + showOnboarding(); + }); +} else { + showOnboarding(); +} + +async function initialize(appElement: HTMLElement, elements: VerifierElements) { + const tokenContent = await tryLoadVerifiablePresentationTokenContent({ + referenceId: appElement.dataset.referenceId, + relationshipTemplateEndpointTemplate: appElement.dataset.relationshipTemplateEndpointTemplate, + tokenEndpointTemplate: appElement.dataset.tokenEndpointTemplate + }); + + if (!tokenContent) { + showOnboarding(); + return; + } + + const result = await validatePresentedCredential(tokenContent.value, { + expectedAudience: "defaultPresentationAudience", + expectedNonce: appElement.dataset.referenceId ?? "" + }); + const credential = mergeCredentialDisplay(result.credential, credentialDisplayFromTokenContent(tokenContent.displayInformation)); + + setCredential(elements, credential); + setStatus(elements, result.isValid ? "valid" : "invalid", result.errorCode); + + elements.closeButton.addEventListener("click", () => { + window.close(); + + window.setTimeout(() => { + showClosedView(elements); + }, 120); + }); +} + +function showOnboarding() { + document.body.classList.remove("openid4vp-verifier-visible"); + rootElement?.replaceChildren(); +} + +function showClosedView(elements: VerifierElements) { + elements.content.hidden = true; + elements.closedView.hidden = false; +} + +function setStatus(elements: VerifierElements, status: VerificationStatus, errorCode?: PresentationValidationErrorCode) { + elements.root.classList.toggle("is-loading", status === "loading"); + elements.root.classList.toggle("is-valid", status === "valid"); + elements.root.classList.toggle("is-invalid", status === "invalid"); + + elements.resultText.textContent = status === "loading" ? "Der Nachweis\nwird geprüft." : `Der präsentierte Nachweis\n${status === "valid" ? "ist gültig." : "ist ungültig."}`; + elements.loadingMark.hidden = status !== "loading"; + elements.validMark.hidden = status !== "valid"; + elements.invalidMark.hidden = status !== "invalid"; + elements.closeButton.hidden = status === "loading"; + + if (errorCode) { + elements.error.textContent = `Hinweis: ${validationErrorMessages[errorCode]}`; + elements.error.hidden = false; + } else { + elements.error.textContent = ""; + elements.error.hidden = true; + } +} + +function setCredential(elements: VerifierElements, credential: CredentialDisplay) { + setText(elements.title, credential.title); + elements.credentialCard.hidden = !credential.title && !credential.portrait && !credential.logo; + elements.credentialCard.style.backgroundColor = credential.backgroundColor ?? ""; + elements.credentialCard.style.color = credential.textColor ?? ""; + + setImage(elements.credentialLogo, credential.logo); + setImage(elements.portrait, credential.portrait); + + let hasDetailRow = false; + for (const field of detailFields) { + const row = elements.detailRows[field]; + const value = elements.detailValues[field]; + const fieldValue = credential[field]; + + if (!row || !value) { + continue; + } + + const hasValue = setText(value, fieldValue); + row.hidden = !hasValue; + hasDetailRow ||= hasValue; + } + + elements.additionalClaims.replaceChildren(); + for (const claim of credential.claims ?? []) { + const row = document.createElement("div"); + const label = document.createElement("dt"); + const value = document.createElement("dd"); + label.textContent = claim.label; + value.textContent = claim.value; + row.append(label, value); + elements.additionalClaims.append(row); + hasDetailRow = true; + } + + if (credential.issuer) { + elements.verifiedByIssuer.textContent = credential.issuer; + elements.verifiedBy.hidden = false; + } else { + elements.verifiedByIssuer.textContent = ""; + elements.verifiedBy.hidden = true; + } + + elements.details.hidden = !hasDetailRow; +} + +function mergeCredentialDisplay(...sources: Array>): CredentialDisplay { + const merged: Partial = {}; + + for (const source of sources) { + for (const field of credentialDisplayFields) { + const value = source[field]; + + if (typeof value === "string" && value.trim()) { + merged[field] = value; + } + } + + if (source.claims?.length) { + merged.claims = source.claims; + } + } + + return merged; +} + +function credentialDisplayFromTokenContent(displayInformation?: Array>): CredentialDisplay { + const display = selectDisplayInformation(displayInformation); + if (!display) { + return {}; + } + + const logo = display.logo; + return { + backgroundColor: stringValue(display.background_color), + logo: typeof logo === "string" ? logo : isRecord(logo) ? stringValue(logo.uri) : undefined, + textColor: stringValue(display.text_color), + title: stringValue(display.name) + }; +} + +function selectDisplayInformation(displayInformation?: Array>) { + if (!displayInformation?.length) { + return undefined; + } + + const languages = navigator.languages.map((language) => language.toLowerCase()); + return ( + displayInformation.find((entry) => { + const locale = stringValue(entry.locale)?.toLowerCase(); + return locale ? languages.includes(locale) : false; + }) ?? + displayInformation.find((entry) => { + const language = stringValue(entry.locale)?.split("-")[0].toLowerCase(); + return language ? languages.some((candidate) => candidate.split("-")[0] === language) : false; + }) ?? + displayInformation[0] + ); +} + +function stringValue(value: unknown) { + return typeof value === "string" && value.trim() ? value : undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function setImage(element: HTMLImageElement, source?: string) { + if (source) { + element.onload = () => { + element.hidden = false; + }; + element.onerror = () => { + element.hidden = true; + }; + element.src = source; + element.hidden = false; + } else { + element.removeAttribute("src"); + element.hidden = true; + } +} + +function setText(element: HTMLElement, value?: string) { + const hasValue = typeof value === "string" && value.trim().length > 0; + element.textContent = hasValue ? value : ""; + element.hidden = !hasValue; + + return hasValue; +} + +function getVerifierElements(root: HTMLElement): VerifierElements | undefined { + const credentialCard = query(root, "[data-credential-card]"); + const credentialLogo = query(root, "[data-credential-logo]"); + const additionalClaims = query(root, "[data-additional-claims]"); + const details = query(root, "[data-details]"); + const closeButton = query(root, "[data-close]"); + if (!credentialCard || !credentialLogo || !additionalClaims || !details || !closeButton) { + return undefined; + } + + const detailRows = { + createdAt: query(root, '[data-detail-row="createdAt"]'), + expiresAt: query(root, '[data-detail-row="expiresAt"]'), + issuer: query(root, '[data-detail-row="issuer"]'), + publicKey: query(root, '[data-detail-row="publicKey"]') + }; + const detailValues = { + createdAt: query(root, '[data-detail-value="createdAt"]'), + expiresAt: query(root, '[data-detail-value="expiresAt"]'), + issuer: query(root, '[data-detail-value="issuer"]'), + publicKey: query(root, '[data-detail-value="publicKey"]') + }; + + const elements = { + additionalClaims, + closeButton, + closedView: query(root, "[data-closed-view]"), + content: query(root, "[data-verifier-content]"), + credentialCard, + credentialLogo, + details, + detailRows, + detailValues, + error: query(root, "[data-error]"), + invalidMark: query(root, "[data-invalid-mark]"), + loadingMark: query(root, "[data-loading-mark]"), + portrait: query(root, "[data-credential-portrait]"), + resultText: query(root, "[data-result-text]"), + root: query(root, "[data-verifier]"), + title: query(root, "[data-credential-title]"), + validMark: query(root, "[data-valid-mark]"), + verifiedBy: query(root, "[data-verified-by]"), + verifiedByIssuer: query(root, "[data-verified-by-issuer]") + }; + + return Object.values(elements).every(Boolean) ? (elements as VerifierElements) : undefined; +} + +function query(root: ParentNode, selector: string) { + return root.querySelector(selector) ?? undefined; +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/referenceContent.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/referenceContent.ts new file mode 100644 index 0000000000..202bcab8bf --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/referenceContent.ts @@ -0,0 +1,182 @@ +import { CoreBuffer, CryptoCipher, CryptoEncryption, SodiumWrapper } from "@nmshd/crypto"; + +type ApiEnvelope = { + result?: unknown; +}; + +export type TokenContentVerifiablePresentation = { + "@type": "TokenContentVerifiablePresentation"; + displayInformation?: Array>; + type?: string; + value: unknown; +}; + +type ReferenceContentConfig = { + referenceId?: string; + relationshipTemplateEndpointTemplate?: string; + tokenEndpointTemplate?: string; +}; + +type ParsedReferenceHash = { + algorithm: number; + key: string; +}; + +const XCHACHA20_POLY1305_ALGORITHM = 3; + +export async function tryLoadVerifiablePresentationTokenContent( + config: ReferenceContentConfig +): Promise { + try { + await SodiumWrapper.ready(); + + const referenceId = config.referenceId?.trim(); + const parsedReference = parseReferenceHash(window.location.hash); + + if (!referenceId || !parsedReference || parsedReference.algorithm !== XCHACHA20_POLY1305_ALGORITHM) { + return undefined; + } + + const encryptedContent = await tryFetchEncryptedContent(referenceId, config); + if (!encryptedContent) { + return undefined; + } + + const decryptedContent = await decryptTokenContent(encryptedContent, parsedReference); + return isTokenContentVerifiablePresentation(decryptedContent) ? decryptedContent : undefined; + } catch (error) { + console.error("Der Inhalt der Nachweispräsentation konnte nicht geladen werden.", error); + return undefined; + } +} + +function parseReferenceHash(hash: string): ParsedReferenceHash | undefined { + const hashValue = hash.startsWith("#") ? hash.slice(1) : hash; + const candidates = [hashValue, hashValue.endsWith(".") ? hashValue.slice(0, -1) : undefined].filter(Boolean) as string[]; + + for (const candidate of candidates) { + try { + const [algorithm, key] = CoreBuffer.fromBase64URL(candidate).toUtf8().split("|"); + const parsedAlgorithm = Number.parseInt(algorithm, 10); + + if (Number.isNaN(parsedAlgorithm) || !key) { + continue; + } + + return { + algorithm: parsedAlgorithm, + key + }; + } catch { + continue; + } + } + + return undefined; +} + +async function tryFetchEncryptedContent(referenceId: string, config: ReferenceContentConfig) { + for (const endpoint of endpointsForReferenceId(referenceId, config)) { + try { + const response = await fetch(endpoint, { + credentials: "same-origin", + headers: { + Accept: "application/json" + } + }); + + if (!response.ok) { + continue; + } + + const body = (await response.json()) as ApiEnvelope; + const content = extractEncryptedContent(body); + + if (content) { + return content; + } + } catch { + continue; + } + } + + return undefined; +} + +function endpointsForReferenceId(referenceId: string, config: ReferenceContentConfig) { + const tokenEndpoint = applyEndpointTemplate(config.tokenEndpointTemplate, referenceId); + const relationshipTemplateEndpoint = applyEndpointTemplate(config.relationshipTemplateEndpointTemplate, referenceId); + + if (referenceId.startsWith("RLT")) { + return [relationshipTemplateEndpoint, tokenEndpoint].filter(Boolean) as string[]; + } + + return [tokenEndpoint, relationshipTemplateEndpoint].filter(Boolean) as string[]; +} + +function applyEndpointTemplate(template: string | undefined, referenceId: string) { + return template?.replace("{id}", encodeURIComponent(referenceId)); +} + +function extractEncryptedContent(body: ApiEnvelope): string | undefined { + const candidates = [body.result, getPath(body.result, ["value"]), body].filter(isRecord); + + for (const candidate of candidates) { + const content = candidate.content; + if (typeof content === "string" && content.trim()) { + return content; + } + } + + return undefined; +} + +async function decryptTokenContent(encryptedContent: string, reference: ParsedReferenceHash) { + const secretKey = CoreBuffer.fromBase64URL(reference.key); + const cipher = deserializeCryptoCipher(encryptedContent); + const plaintext = await CryptoEncryption.decrypt(cipher, secretKey, undefined, XCHACHA20_POLY1305_ALGORITHM); + + return JSON.parse(plaintext.toUtf8()) as unknown; +} + +function deserializeCryptoCipher(encryptedContent: string) { + const serializedCipher = JSON.parse(CoreBuffer.fromBase64URL(encryptedContent).toUtf8()) as unknown; + + if ( + !isRecord(serializedCipher) || + serializedCipher.alg !== XCHACHA20_POLY1305_ALGORITHM || + typeof serializedCipher.cph !== "string" || + typeof serializedCipher.nnc !== "string" + ) { + throw new Error("Der Token-Inhalt enthält keine unterstützte Verschlüsselung."); + } + + const cipher = new CryptoCipher(); + cipher.algorithm = XCHACHA20_POLY1305_ALGORITHM; + cipher.cipher = CoreBuffer.fromBase64URL(serializedCipher.cph); + cipher.nonce = CoreBuffer.fromBase64URL(serializedCipher.nnc); + + return cipher; +} + +function isTokenContentVerifiablePresentation(value: unknown): value is TokenContentVerifiablePresentation { + return isRecord(value) && value["@type"] === "TokenContentVerifiablePresentation" && "value" in value; +} + +function getPath(object: unknown, path: string[]) { + let current = object; + + for (const segment of path) { + if (!isRecord(current)) { + return undefined; + } + + current = current[segment]; + } + + return current; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/styles.css b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/styles.css new file mode 100644 index 0000000000..c97b8c464b --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/styles.css @@ -0,0 +1,284 @@ +body.openid4vp-verifier-visible { + min-height: 100%; + margin: 0; + padding: 0; + background: #f7f9ff; + color: #181c20; + font-family: "Work Sans", sans-serif; + font-weight: 400; + letter-spacing: 0.25px; +} + +body.openid4vp-verifier-visible #app-onboarding-content { + display: none; +} + +#openid4vp-verifier-root { + display: none; +} + +body.openid4vp-verifier-visible #openid4vp-verifier-root { + display: block; +} + +.openid4vp-verifier, +.openid4vp-verifier * { + box-sizing: border-box; +} + +.openid4vp-verifier button { + font: inherit; +} + +.openid4vp-verifier [hidden] { + display: none !important; +} + +.openid4vp-verifier.verifier { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + min-height: max(100svh, 812px); + max-width: 375px; + margin: 0 auto; + overflow: hidden; + background: #f7f9ff; +} + +.openid4vp-verifier .verifier-content { + display: flex; + flex: 1; + flex-direction: column; +} + +.openid4vp-verifier .intro { + width: 100%; + min-height: 80px; + margin: 0; + padding: 16px 24px; + text-align: center; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.5px; +} + +.openid4vp-verifier .credential-section { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + padding: 16px; +} + +.openid4vp-verifier .pass-card { + position: relative; + width: 100%; + height: 216px; + flex: 0 0 216px; + overflow: hidden; + color: #fff; + background: #971d50; + border-radius: 12px; +} + +.openid4vp-verifier .pass-card h1 { + position: absolute; + right: 15px; + bottom: 13px; + left: 15px; + margin: 0; + font-size: 30px; + font-weight: 600; + line-height: 36px; + letter-spacing: 0; +} + +.openid4vp-verifier .pass-logo { + position: absolute; + top: 16px; + left: 16px; + width: 49px; + height: 49px; + object-fit: contain; + object-position: left top; +} + +.openid4vp-verifier .pass-portrait { + position: absolute; + top: 16px; + right: 16px; + width: 84px; + height: 108px; + object-fit: cover; + background: #e6e8ee; + border-radius: 6px; +} + +.openid4vp-verifier .details { + width: 100%; + overflow: hidden; + color: #181c20; + background: #F5E5DB; + border-radius: 8px; +} + +.openid4vp-verifier .verified-by { + display: flex; + align-items: center; + gap: 4px; + min-height: 32px; + padding: 4px 12px 4px 4px; + background: #e0e2e8; + border-radius: 4px 4px 0 0; + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; +} + +.openid4vp-verifier .verified-by svg { + width: 24px; + height: 24px; + flex: 0 0 24px; +} + +.openid4vp-verifier .details dl { + display: grid; + gap: 8px; + margin: 0; + padding: 6px 8px; + background: #f5e5db; + border-radius: 0 0 4px 4px; +} + +.openid4vp-verifier .details dt { + margin: 0; + color: #42474e; + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; +} + +.openid4vp-verifier .details dd { + margin: 0; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + overflow-wrap: anywhere; +} + +.openid4vp-verifier .additional-claims { + display: contents; +} + +.openid4vp-verifier .result { + display: flex; + min-height: 283px; + flex-direction: column; + align-items: center; + gap: 24px; + margin: 0; + padding: 24px; + color: #42474e; + background: #f7f9ff; +} + +.openid4vp-verifier .result p, +.openid4vp-verifier .closed-view p { + width: 100%; + margin: 0; + text-align: center; + font-size: 24px; + font-weight: 400; + line-height: 32px; + letter-spacing: 0; + white-space: pre-line; +} + +.openid4vp-verifier .status-mark { + display: grid; + place-items: center; + width: 147px; + height: 147px; + flex: 0 0 147px; + border: 7px solid currentColor; + border-radius: 50%; +} + +.openid4vp-verifier .status-mark svg { + width: 64px; + height: 64px; +} + +.openid4vp-verifier.is-valid .status-mark { + color: #428c17; +} + +.openid4vp-verifier.is-invalid .status-mark { + color: #ff5449; +} + +.openid4vp-verifier.is-loading .status-mark { + color: #2f628c; +} + +.openid4vp-verifier .loading-mark { + border-right-color: transparent; + animation: openid4vp-spin 1s linear infinite; +} + +.openid4vp-verifier .button-area { + display: flex; + width: 100%; + min-height: 88px; + align-items: center; + padding: 24px; + margin-top: auto; + background: #fff8f5; +} + +.openid4vp-verifier .close-button { + width: 100%; + min-height: 40px; + padding: 10px 24px; + color: #fff; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + background: #2f628c; + border: 0; + border-radius: 100px; + cursor: pointer; +} + +.openid4vp-verifier .close-button:active { + transform: translateY(1px); +} + +.openid4vp-verifier .closed-view { + display: grid; + flex: 1; + place-items: center; + padding: 24px; + color: #42474e; +} + +.openid4vp-verifier .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + white-space: nowrap; + clip: rect(0, 0, 0, 0); + border: 0; +} + +@keyframes openid4vp-spin { + to { + transform: rotate(1turn); + } +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verification.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verification.ts new file mode 100644 index 0000000000..a1b9cd7916 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verification.ts @@ -0,0 +1,955 @@ +import { + Agent, + ClaimFormat, + ConsoleLogger, + DependencyManager, + DidsModule, + InjectionSymbols, + JwkDidResolver, + KeyDidResolver, + Kms, + LogLevel, + TypedArrayEncoder, + WebDidResolver, + W3cJsonLdVerifiableCredential, + W3cJsonLdVerifiablePresentation, + X509Certificate, + getPublicJwkFromVerificationMethod, + sdJwtVcHasher +} from "@credo-ts/core"; +import { SDJwtInstance } from "@sd-jwt/core"; +import { EventEmitter } from "events"; +import { BrowserVerificationKeyManagementService } from "./verificationKeyManagement"; +import { BrowserFileSystem, InMemoryStorageService } from "./verificationStorage"; + +type JsonObject = Record; + +export enum PresentationValidationErrorCode { + AudienceMismatch = "audienceMismatch", + CredentialExpired = "credentialExpired", + CredentialNotYetValid = "credentialNotYetValid", + InvalidDisclosure = "invalidDisclosure", + InvalidPresentationBinding = "invalidPresentationBinding", + InvalidSignature = "invalidSignature", + MissingAudience = "missingAudience", + MissingChallenge = "missingChallenge", + MissingCredential = "missingCredential", + MissingCredentialType = "missingCredentialType", + MissingHolderVerificationKey = "missingHolderVerificationKey", + MissingIssuerVerificationKey = "missingIssuerVerificationKey", + MissingKeyBinding = "missingKeyBinding", + MissingPresentation = "missingPresentation", + NonceMismatch = "nonceMismatch", + UnsupportedHolderDidUrl = "unsupportedHolderDidUrl", + UnsupportedIssuerDidUrl = "unsupportedIssuerDidUrl", + UnsupportedPresentationFormat = "unsupportedPresentationFormat", + UnsupportedSdJwtType = "unsupportedSdJwtType", + UnsupportedSignatureAlgorithm = "unsupportedSignatureAlgorithm", + VerificationFailed = "verificationFailed" +} + +class PresentationValidationError extends Error { + public constructor(public readonly code: PresentationValidationErrorCode) { + super(code); + } +} + +export type VerificationDisplay = { + claims?: VerificationDisplayClaim[]; + createdAt?: string; + expiresAt?: string; + issuer?: string; + portrait?: string; + publicKey?: string; + title?: string; +}; + +export type VerificationDisplayClaim = { + label: string; + value: string; +}; + +export type VerificationOutcome = { + credential: VerificationDisplay; + errorCode?: PresentationValidationErrorCode; + isValid: boolean; +}; + +type ArtifactVerificationOutcome = { + display?: VerificationDisplay; + errorCode?: PresentationValidationErrorCode; + isValid: boolean; +}; + +export type PresentationValidationOptions = { + expectedAudience: string; + expectedNonce: string; + now?: Date; +}; + +type VerificationContext = { + expectedAudience: string; + expectedNonce: string; + now: Date; +}; + +type VerifierAgent = Agent<{ + dids: DidsModule; + kms: Kms.KeyManagementModule; +}>; + +let agentPromise: Promise | undefined; + +export async function validatePresentedCredential( + presentation: unknown, + options: PresentationValidationOptions +): Promise { + try { + if (!options.expectedNonce) { + return invalid(PresentationValidationErrorCode.MissingChallenge); + } + + if (!options.expectedAudience) { + return invalid(PresentationValidationErrorCode.MissingAudience); + } + + const tokens = normalizeToken(presentation); + + if (tokens.length === 0) { + return invalid(PresentationValidationErrorCode.MissingPresentation); + } + + const agent = await getVerifierAgent(); + const verifiedArtifacts = []; + const now = options.now ?? new Date(); + + for (const token of tokens) { + verifiedArtifacts.push( + await verifyToken(agent, token, { + expectedAudience: options.expectedAudience, + expectedNonce: options.expectedNonce, + now + }) + ); + } + + const isValid = verifiedArtifacts.length > 0 && verifiedArtifacts.every((artifact) => artifact.isValid); + const firstCredential = verifiedArtifacts.map((artifact) => artifact.display).find(Boolean) ?? {}; + const firstErrorCode = verifiedArtifacts.find((artifact) => !artifact.isValid)?.errorCode; + + return { + credential: firstCredential, + errorCode: firstErrorCode, + isValid + }; + } catch (error) { + return invalid(error instanceof PresentationValidationError ? error.code : PresentationValidationErrorCode.VerificationFailed); + } +} + +function normalizeToken(value: unknown): Array { + if (!value) { + return []; + } + + if (Array.isArray(value)) { + return value.flatMap(normalizeToken); + } + + if (typeof value === "string") { + const parsed = tryParseJson(value); + if (parsed !== value) { + return normalizeToken(parsed); + } + + return [value]; + } + + if (typeof value === "object") { + return [value as JsonObject]; + } + + return []; +} + +async function verifyToken( + agent: VerifierAgent, + token: string | JsonObject, + context: VerificationContext, + isEmbeddedInVerifiedPresentation = false +): Promise { + if (typeof token === "string") { + if (isSdJwt(token)) { + return verifySdJwtCredential(agent, token, context); + } + + if (isJwt(token)) { + return verifyJwtArtifact(agent, token, context, isEmbeddedInVerifiedPresentation); + } + + return { errorCode: PresentationValidationErrorCode.UnsupportedPresentationFormat, isValid: false }; + } + + return verifyJsonLdArtifact(agent, token, context, isEmbeddedInVerifiedPresentation); +} + +async function verifySdJwtCredential( + agent: VerifierAgent, + token: string, + context: VerificationContext +): Promise { + const sdJwtVc = agent.sdJwtVc.fromCompact(token); + const display = extractDisplayFromObject(sdJwtVc.prettyClaims); + + if (!context.expectedNonce) { + return { + display, + errorCode: PresentationValidationErrorCode.MissingChallenge, + isValid: false + }; + } + + try { + if (sdJwtVc.header.typ !== "dc+sd-jwt" && sdJwtVc.header.typ !== "vc+sd-jwt") { + throw new PresentationValidationError(PresentationValidationErrorCode.UnsupportedSdJwtType); + } + + if (!sdJwtVc.kbJwt) { + throw new PresentationValidationError(PresentationValidationErrorCode.MissingKeyBinding); + } + + const issuerJwk = await resolveSdJwtIssuerJwk(agent, sdJwtVc.header, sdJwtVc.payload); + const holderJwk = await resolveSdJwtHolderJwk(agent, sdJwtVc.payload); + const issuerAlgorithm = signingAlgorithm(sdJwtVc.header); + const holderAlgorithm = signingAlgorithm(sdJwtVc.kbJwt.header); + const verifier = new SDJwtInstance({ + hasher: sdJwtVcHasher, + kbVerifier: createJwtVerifier(agent, holderJwk, holderAlgorithm), + verifier: createJwtVerifier(agent, issuerJwk, issuerAlgorithm) + }); + + await verifier.verify(token, { + currentDate: Math.floor(context.now.getTime() / 1000), + keyBindingNonce: context.expectedNonce, + requiredClaimKeys: ["vct"] + }); + + const validationError = validateSdJwtPresentation(sdJwtVc.payload, sdJwtVc.kbJwt?.payload, context); + + return { + display, + errorCode: validationError, + isValid: validationError === undefined + }; + } catch (error) { + return { + display, + errorCode: classifySdJwtError(error), + isValid: false + }; + } +} + +function createJwtVerifier(agent: VerifierAgent, publicJwk: Kms.KmsJwkPublicAsymmetric, algorithm: Kms.KnownJwaSignatureAlgorithm) { + return async (data: string, signature: string) => { + const result = await agent.kms.verify({ + algorithm, + data: TypedArrayEncoder.fromUtf8String(data), + key: { publicJwk }, + signature: TypedArrayEncoder.fromBase64Url(signature) + }); + + return result.verified; + }; +} + +function signingAlgorithm(header?: JsonObject): Kms.KnownJwaSignatureAlgorithm { + const algorithm = header?.alg; + + if ( + typeof algorithm !== "string" || + !Object.values(Kms.KnownJwaSignatureAlgorithms).includes(algorithm as Kms.KnownJwaSignatureAlgorithm) || + algorithm.startsWith("HS") + ) { + throw new PresentationValidationError(PresentationValidationErrorCode.UnsupportedSignatureAlgorithm); + } + + return algorithm as Kms.KnownJwaSignatureAlgorithm; +} + +async function resolveSdJwtIssuerJwk(agent: VerifierAgent, header: JsonObject, payload: JsonObject): Promise { + const x5c = header.x5c; + if (Array.isArray(x5c) && typeof x5c[0] === "string") { + return X509Certificate.fromEncodedCertificate(x5c[0]).publicJwk.toJson(); + } + + return resolveDidJwk(agent, header.kid, payload.iss, "issuer"); +} + +async function resolveSdJwtHolderJwk(agent: VerifierAgent, payload: JsonObject): Promise { + const confirmation = isObject(payload.cnf) ? payload.cnf : undefined; + + if (confirmation?.jwk) { + return Kms.PublicJwk.fromUnknown(confirmation.jwk).toJson(); + } + + return resolveDidJwk(agent, confirmation?.kid, undefined, "holder"); +} + +async function resolveDidJwk(agent: VerifierAgent, keyId: unknown, controller: unknown, role: "holder" | "issuer") { + if (typeof keyId !== "string") { + throw new PresentationValidationError( + role === "issuer" ? PresentationValidationErrorCode.MissingIssuerVerificationKey : PresentationValidationErrorCode.MissingHolderVerificationKey + ); + } + + const didUrl = keyId.startsWith("#") && typeof controller === "string" ? `${controller}${keyId}` : keyId; + if (!didUrl.startsWith("did:")) { + throw new PresentationValidationError( + role === "issuer" ? PresentationValidationErrorCode.UnsupportedIssuerDidUrl : PresentationValidationErrorCode.UnsupportedHolderDidUrl + ); + } + + const didDocument = await agent.dids.resolveDidDocument(didUrl); + const verificationMethod = didDocument.dereferenceKey(didUrl, role === "issuer" ? ["assertionMethod", "verificationMethod"] : ["authentication", "verificationMethod"]); + return getPublicJwkFromVerificationMethod(verificationMethod).toJson(); +} + +function validateSdJwtPresentation( + credentialPayload: JsonObject, + keyBindingPayload: JsonObject | undefined, + context: VerificationContext +) { + if (context.expectedNonce) { + if (!keyBindingPayload) { + return PresentationValidationErrorCode.MissingKeyBinding; + } + + if (keyBindingPayload.nonce !== context.expectedNonce) { + return PresentationValidationErrorCode.NonceMismatch; + } + + const audience = keyBindingPayload.aud; + const hasExpectedAudience = + audience === context.expectedAudience || (Array.isArray(audience) && audience.includes(context.expectedAudience)); + + if (!hasExpectedAudience) { + return PresentationValidationErrorCode.AudienceMismatch; + } + } + + const now = context.now.getTime() / 1000; + if (typeof credentialPayload.nbf === "number" && credentialPayload.nbf > now) { + return PresentationValidationErrorCode.CredentialNotYetValid; + } + + if (typeof credentialPayload.exp === "number" && credentialPayload.exp <= now) { + return PresentationValidationErrorCode.CredentialExpired; + } + + return undefined; +} + +async function verifyJwtArtifact( + agent: VerifierAgent, + token: string, + context: VerificationContext, + isEmbeddedInVerifiedPresentation: boolean +): Promise { + const payload = decodeJwtPayload(token); + const embeddedCredentials = extractEmbeddedCredentials(payload); + + if (looksLikePresentation(payload)) { + const challenge = context.expectedNonce; + if (!challenge) { + return { + display: extractDisplayFromObject(payload), + errorCode: PresentationValidationErrorCode.MissingChallenge, + isValid: false + }; + } + + const presentationResult = await tryVerify(() => + agent.w3cCredentials.verifyPresentation({ + challenge, + domain: context.expectedAudience, + presentation: token, + verifyCredentialStatus: false + }) + ); + + if (!presentationResult.isValid) { + return { + display: extractDisplayFromObject(payload), + errorCode: presentationResult.errorCode, + isValid: false + }; + } + + if (embeddedCredentials.length === 0) { + return { + display: extractDisplayFromObject(payload), + errorCode: PresentationValidationErrorCode.MissingCredential, + isValid: false + }; + } + + const credentialResults = await Promise.all(embeddedCredentials.map((credential) => verifyToken(agent, credential, context, true))); + + return { + display: credentialResults.map((result) => result.display).find(Boolean) ?? extractDisplayFromObject(payload), + errorCode: credentialResults.find((result) => !result.isValid)?.errorCode, + isValid: credentialResults.every((result) => result.isValid) + }; + } + + if (!isEmbeddedInVerifiedPresentation) { + return { + display: extractDisplayFromObject(payload), + errorCode: PresentationValidationErrorCode.MissingKeyBinding, + isValid: false + }; + } + + const credentialResult = await tryVerify(() => + agent.w3cCredentials.verifyCredential({ + credential: token, + verifyCredentialStatus: false + }) + ); + + if (credentialResult.isValid) { + return { + display: extractDisplayFromObject(payload), + isValid: true + }; + } + + const v2CredentialResult = await tryVerify(() => + agent.w3cV2Credentials.verifyCredential({ + credential: token + }) + ); + + return { + display: extractDisplayFromObject(payload), + errorCode: v2CredentialResult.isValid ? undefined : credentialResult.errorCode ?? v2CredentialResult.errorCode, + isValid: v2CredentialResult.isValid + }; +} + +async function verifyJsonLdArtifact( + agent: VerifierAgent, + token: JsonObject, + context: VerificationContext, + isEmbeddedInVerifiedPresentation: boolean +): Promise { + if (looksLikePresentation(token)) { + if (!context.expectedNonce) { + return { + display: extractDisplayFromObject(token), + errorCode: PresentationValidationErrorCode.MissingChallenge, + isValid: false + }; + } + + const challenge = context.expectedNonce; + const result = await tryVerify(() => + agent.w3cCredentials.verifyPresentation({ + challenge, + domain: context.expectedAudience, + presentation: new W3cJsonLdVerifiablePresentation(token as never), + verifyCredentialStatus: false + }) + ); + + if (!result.isValid) { + return { + display: extractDisplayFromObject(token), + errorCode: result.errorCode, + isValid: false + }; + } + + const embeddedCredentials = extractEmbeddedCredentials(token); + if (embeddedCredentials.length === 0) { + return { + display: extractDisplayFromObject(token), + errorCode: PresentationValidationErrorCode.MissingCredential, + isValid: false + }; + } + + const credentialResults = await Promise.all(embeddedCredentials.map((credential) => verifyToken(agent, credential, context, true))); + + return { + display: credentialResults.map((credentialResult) => credentialResult.display).find(Boolean) ?? extractDisplayFromObject(token), + errorCode: credentialResults.find((credentialResult) => !credentialResult.isValid)?.errorCode, + isValid: credentialResults.every((credentialResult) => credentialResult.isValid) + }; + } + + if (!isEmbeddedInVerifiedPresentation) { + return { + display: extractDisplayFromObject(token), + errorCode: PresentationValidationErrorCode.MissingKeyBinding, + isValid: false + }; + } + + const result = await tryVerify(() => + agent.w3cCredentials.verifyCredential({ + credential: W3cJsonLdVerifiableCredential.fromJson(token), + verifyCredentialStatus: false + }) + ); + + return { + display: extractDisplayFromObject(token), + errorCode: result.isValid ? undefined : result.errorCode, + isValid: result.isValid + }; +} + +function classifySdJwtError(error: unknown) { + if (error instanceof PresentationValidationError) { + return error.code; + } + + const message = error instanceof Error ? error.message : ""; + + if (/invalid jwt signature/i.test(message)) { + return PresentationValidationErrorCode.InvalidSignature; + } + + if (/invalid sd_hash/i.test(message)) { + return PresentationValidationErrorCode.InvalidPresentationBinding; + } + + if (/missing required claim keys.*vct/i.test(message)) { + return PresentationValidationErrorCode.MissingCredentialType; + } + + if (/jwt is expired/i.test(message)) { + return PresentationValidationErrorCode.CredentialExpired; + } + + if (/jwt is not yet valid/i.test(message)) { + return PresentationValidationErrorCode.CredentialNotYetValid; + } + + if (/invalid nonce/i.test(message)) { + return PresentationValidationErrorCode.NonceMismatch; + } + + if (/key binding jwt not exist/i.test(message)) { + return PresentationValidationErrorCode.MissingKeyBinding; + } + + if (/disclosure/i.test(message)) { + return PresentationValidationErrorCode.InvalidDisclosure; + } + + return PresentationValidationErrorCode.VerificationFailed; +} + +async function tryVerify(verify: () => Promise<{ isValid?: boolean; verified?: boolean }>) { + try { + const result = await verify(); + const isValid = result.isValid === true || result.verified === true; + return { + errorCode: isValid ? undefined : PresentationValidationErrorCode.InvalidSignature, + isValid + }; + } catch { + return { + errorCode: PresentationValidationErrorCode.InvalidSignature, + isValid: false + }; + } +} + +async function getVerifierAgent() { + agentPromise ??= createVerifierAgent(); + return agentPromise; +} + +async function createVerifierAgent() { + const dependencyManager = new DependencyManager(); + dependencyManager.registerInstance(InjectionSymbols.StorageService, new InMemoryStorageService()); + + const agent = new Agent({ + config: { + allowInsecureHttpUrls: globalThis.location?.protocol === "http:", + autoUpdateStorageOnStartup: true, + logger: new ConsoleLogger(LogLevel.Error) + }, + dependencies: { + EventEmitterClass: EventEmitter, + FileSystem: BrowserFileSystem, + WebSocketClass: globalThis.WebSocket as never, + fetch: globalThis.fetch.bind(globalThis) + }, + modules: { + dids: new DidsModule({ + resolvers: [new KeyDidResolver(), new JwkDidResolver(), new WebDidResolver()] + }), + kms: new Kms.KeyManagementModule({ + backends: [new BrowserVerificationKeyManagementService()] + }) + } + }, dependencyManager); + + await agent.initialize(); + return agent; +} + +function extractEmbeddedCredentials(input: unknown): Array { + const presentation = unwrapPresentation(input); + const rawCredentials = getPath(presentation, ["verifiableCredential"]) ?? getPath(presentation, ["vp", "verifiableCredential"]); + + return normalizeToken(rawCredentials); +} + +function unwrapPresentation(input: unknown) { + if (!isObject(input)) { + return input; + } + + return input.vp ?? input.presentation ?? input.verifiablePresentation ?? input; +} + +function looksLikePresentation(input: unknown) { + const presentation = unwrapPresentation(input); + + if (!isObject(presentation)) { + return false; + } + + const type = presentation.type; + return Boolean( + "verifiableCredential" in presentation || + (Array.isArray(type) && type.includes("VerifiablePresentation")) || + type === "VerifiablePresentation" || + "vp" in presentation + ); +} + +function extractDisplayFromObject(input: unknown): VerificationDisplay { + const credential = unwrapCredential(input); + const credentialSubject = getObjectPath(credential, ["credentialSubject"]) ?? getObjectPath(credential, ["vc", "credentialSubject"]); + const claims = [credentialSubject, getObjectPath(credential, ["vc"]), credential].filter(isObject); + + return { + claims: extractDisplayClaims(credential), + createdAt: formatDate(firstString(claims, ["issuanceDate", "validFrom", "nbf", "iat"])), + expiresAt: formatDate(firstString(claims, ["expirationDate", "validUntil", "exp"])), + issuer: issuerName(credential), + portrait: firstString(claims, ["portrait", "photo", "picture", "image"]), + publicKey: formatPublicKey(firstString([credential], ["kid", "iss", "id"])), + title: credentialTitle(credential) + }; +} + +const technicalClaimNames = new Set([ + "@context", + "_sd", + "_sd_alg", + "cnf", + "credentialSubject", + "exp", + "iat", + "iss", + "issuanceDate", + "nbf", + "proof", + "status", + "type", + "validFrom", + "validUntil", + "vc", + "vct", + "vp" +]); +const imageClaimNames = new Set(["image", "photo", "picture", "portrait"]); +const germanClaimLabels: Record = { + birthdate: "Geburtsdatum", + city: "Stadt", + country: "Land", + dateofbirth: "Geburtsdatum", + day: "Tag", + email: "E-Mail-Adresse", + familyname: "Nachname", + firstname: "Vorname", + givenname: "Vorname", + houseno: "Hausnummer", + lastname: "Nachname", + month: "Monat", + phonenumber: "Telefonnummer", + recipient: "Empfänger", + street: "Straße", + streetaddress: "Anschrift", + surname: "Nachname", + year: "Jahr", + zipcode: "Postleitzahl" +}; + +function extractDisplayClaims(credential: JsonObject): VerificationDisplayClaim[] { + const credentialSubject = getObjectPath(credential, ["credentialSubject"]) ?? getObjectPath(credential, ["vc", "credentialSubject"]); + const claimSource = credentialSubject ?? credential; + const claims: VerificationDisplayClaim[] = []; + + for (const [name, value] of Object.entries(claimSource)) { + if (technicalClaimNames.has(name) || imageClaimNames.has(name)) { + continue; + } + + appendDisplayClaims(claims, [name], value); + } + + return claims; +} + +function appendDisplayClaims(claims: VerificationDisplayClaim[], path: string[], value: unknown) { + if (isObject(value)) { + for (const [name, nestedValue] of Object.entries(value)) { + if (name === "@type") { + continue; + } + + appendDisplayClaims(claims, [...path, name], nestedValue); + } + return; + } + + if (Array.isArray(value) && value.some((entry) => isObject(entry) || Array.isArray(entry))) { + value.forEach((entry, index) => appendDisplayClaims(claims, [...path, String(index + 1)], entry)); + return; + } + + const formattedValue = formatClaimValue(value); + if (formattedValue === undefined) { + return; + } + + claims.push({ + label: path.map(localizeClaimName).join(" · "), + value: formattedValue + }); +} + +function localizeClaimName(value: string) { + const normalizedValue = value.toLowerCase().replace(/[^a-z0-9]/g, ""); + return germanClaimLabels[normalizedValue] ?? capitalize(splitCamelCase(value)); +} + +function formatClaimValue(value: unknown) { + if (typeof value === "string") { + return value.trim() || undefined; + } + + if (typeof value === "boolean") { + return value ? "Ja" : "Nein"; + } + + if (typeof value === "number") { + return String(value); + } + + if (Array.isArray(value)) { + return value.map((entry) => String(entry)).join(", "); + } + + return undefined; +} + +function unwrapCredential(input: unknown): JsonObject { + if (typeof input === "string" && isJwt(input)) { + return decodeJwtPayload(input); + } + + if (!isObject(input)) { + return {}; + } + + if (isObject(input.vc)) { + return input.vc; + } + + if (isObject(input.credential)) { + return input.credential; + } + + return input; +} + +function credentialTitle(credential: JsonObject) { + const name = getStringPath(credential, ["name"]) ?? getStringPath(credential, ["vc", "name"]); + if (name) { + return name; + } + + const type = credential.type ?? getPath(credential, ["vc", "type"]); + const lastType = Array.isArray(type) ? type.filter((entry) => entry !== "VerifiableCredential").at(-1) : type; + if (typeof lastType === "string") { + return splitCamelCase(lastType).replaceAll("_", " "); + } + + const vct = getStringPath(credential, ["vct"]); + if (vct) { + return splitCamelCase(vct.split(/[/:#]/).at(-1) ?? vct); + } + + return undefined; +} + +function issuerName(credential: JsonObject) { + const issuer = credential.issuer ?? getPath(credential, ["vc", "issuer"]) ?? credential.iss; + + if (typeof issuer === "string") { + return readableIssuer(issuer); + } + + if (isObject(issuer)) { + return getStringPath(issuer, ["name"]) ?? getStringPath(issuer, ["id"]) ?? undefined; + } + + return undefined; +} + +function firstString(objects: JsonObject[], keys: string[]) { + for (const object of objects) { + for (const key of keys) { + const value = object[key]; + if (typeof value === "string" && value.trim()) { + return value; + } + + if (typeof value === "number") { + return value.toString(); + } + } + } + + return undefined; +} + +function formatDate(value?: string) { + if (!value) { + return undefined; + } + + if (/^\d+$/.test(value)) { + const date = new Date(Number(value) * 1000); + return Number.isNaN(date.getTime()) ? undefined : new Intl.DateTimeFormat("de-DE").format(date); + } + + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat("de-DE").format(date); +} + +function formatPublicKey(value?: string) { + if (!value) { + return undefined; + } + + const strippedValue = value.split("#").at(-1)?.replace(/[^a-zA-Z0-9]/g, "") ?? value; + const numericValue = strippedValue.replace(/\D/g, ""); + + if (numericValue.length >= 20) { + return numericValue.slice(0, 20).match(/.{1,4}/g)?.join("-") ?? numericValue; + } + + return strippedValue.length > 24 ? `${strippedValue.slice(0, 4)}-${strippedValue.slice(-16).match(/.{1,4}/g)?.join("-")}` : value; +} + +function readableIssuer(value: string) { + if (value.startsWith("did:")) { + return value; + } + + try { + const url = new URL(value); + const hostnameParts = url.hostname.replace(/^www\./, "").split("."); + const issuerParts = hostnameParts.length > 1 ? hostnameParts.slice(0, -1) : hostnameParts; + return issuerParts.map(capitalize).join(" "); + } catch { + return value; + } +} + +function splitCamelCase(value: string) { + return value.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " "); +} + +function capitalize(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +function decodeJwtPayload(jwt: string): JsonObject { + const payload = jwt.split(".")[1]; + if (!payload) { + return {}; + } + + return JSON.parse(new TextDecoder().decode(base64UrlToBytes(payload))) as JsonObject; +} + +function getObjectPath(object: JsonObject, path: string[]) { + const value = getPath(object, path); + return isObject(value) ? value : undefined; +} + +function getStringPath(object: JsonObject, path: string[]) { + const value = getPath(object, path); + return typeof value === "string" && value.trim() ? value : undefined; +} + +function getPath(object: unknown, path: string[]) { + let current = object; + + for (const segment of path) { + if (!isObject(current)) { + return undefined; + } + + current = current[segment]; + } + + return current; +} + +function tryParseJson(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +function base64UrlToBytes(value: string) { + const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "="); + return Uint8Array.from(atob(base64), (character) => character.charCodeAt(0)); +} + +function isJwt(value: string) { + return value.split(".").length === 3; +} + +function isSdJwt(value: string) { + return value.includes("~"); +} + +function isObject(value: unknown): value is JsonObject { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function invalid(errorCode: PresentationValidationErrorCode): VerificationOutcome { + return { + credential: {}, + errorCode, + isValid: false + }; +} + +void ClaimFormat.JwtVc; diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationKeyManagement.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationKeyManagement.ts new file mode 100644 index 0000000000..ea9802f2f7 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationKeyManagement.ts @@ -0,0 +1,171 @@ +import { Kms, TypedArrayEncoder } from "@credo-ts/core"; +import { ed25519 } from "@noble/curves/ed25519.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import type { AgentContext } from "@credo-ts/core"; + +const supportedAlgorithms = [ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "ES256K", + "EdDSA", + "Ed25519" +] as const; + +type SupportedAlgorithm = (typeof supportedAlgorithms)[number]; + +export class BrowserVerificationKeyManagementService implements Kms.KeyManagementService { + public readonly backend = "browser-verification"; + + public isOperationSupported(_agentContext: AgentContext, operation: Kms.KmsOperation) { + return operation.operation === "randomBytes" || (operation.operation === "verify" && supportedAlgorithms.includes(operation.algorithm as SupportedAlgorithm)); + } + + public async getPublicKey() { + return null; + } + + public async createKey(): Promise { + throw new Kms.KeyManagementError("Die Prüfkomponente im Browser kann keine Schlüssel erzeugen."); + } + + public async importKey(): Promise { + throw new Kms.KeyManagementError("Die Prüfkomponente im Browser kann keine Schlüssel importieren."); + } + + public async deleteKey() { + return false; + } + + public async sign(): Promise { + throw new Kms.KeyManagementError("Die Prüfkomponente im Browser kann keine Daten signieren."); + } + + public async verify(_agentContext: AgentContext, options: Kms.KmsVerifyOptions): Promise { + const publicJwk = options.key.publicJwk; + + if (!publicJwk) { + throw new Kms.KeyManagementError("Im Browser wird nur die Prüfung mit öffentlichen JWK-Schlüsseln unterstützt."); + } + + Kms.assertAllowedSigningAlgForKey(publicJwk, options.algorithm); + Kms.assertKeyAllowsVerify(publicJwk); + + const verified = await this.verifyWithJwk(publicJwk, options.algorithm as SupportedAlgorithm, options.data, options.signature); + + if (!verified) { + return { verified: false }; + } + + return { + publicJwk: Kms.publicJwkFromPrivateJwk(publicJwk), + verified: true + }; + } + + public async encrypt(): Promise { + throw new Kms.KeyManagementError("Die Prüfkomponente im Browser kann keine Daten verschlüsseln."); + } + + public async decrypt(): Promise { + throw new Kms.KeyManagementError("Die Prüfkomponente im Browser kann keine Daten entschlüsseln."); + } + + public randomBytes(_agentContext: AgentContext, options: Kms.KmsRandomBytesOptions) { + const bytes = new Uint8Array(options.length); + crypto.getRandomValues(bytes); + return bytes; + } + + private async verifyWithJwk(jwk: Kms.KmsJwkPublic, algorithm: SupportedAlgorithm, data: Uint8Array, signature: Uint8Array) { + if (jwk.kty === "EC" && jwk.crv === "secp256k1") { + const publicKey = new Uint8Array([4, ...TypedArrayEncoder.fromBase64Url(jwk.x), ...TypedArrayEncoder.fromBase64Url(jwk.y)]); + return secp256k1.verify(signature, sha256(data), publicKey, { lowS: false }); + } + + if (jwk.kty === "OKP" && jwk.crv === "Ed25519") { + return ed25519.verify(signature, data, TypedArrayEncoder.fromBase64Url(jwk.x)); + } + + const key = await crypto.subtle.importKey("jwk", sanitizePublicJwk(jwk), webCryptoImportAlgorithm(algorithm, jwk), false, ["verify"]); + const verifyAlgorithm = webCryptoVerifyAlgorithm(algorithm); + const verified = await crypto.subtle.verify(verifyAlgorithm, key, toBrowserBytes(signature), toBrowserBytes(data)); + + if (verified || jwk.kty !== "EC") { + return verified; + } + + return crypto.subtle.verify(verifyAlgorithm, key, toBrowserBytes(Kms.rawEcSignatureToDer(signature, jwk.crv)), toBrowserBytes(data)); + } +} + +function toBrowserBytes(bytes: Uint8Array): Uint8Array { + return new Uint8Array(bytes); +} + +function sanitizePublicJwk(jwk: Kms.KmsJwkPublic): JsonWebKey { + const { alg: _alg, key_ops: _keyOps, use: _use, ...sanitizedJwk } = jwk; + return sanitizedJwk as JsonWebKey; +} + +function webCryptoImportAlgorithm(algorithm: SupportedAlgorithm, jwk: Kms.KmsJwkPublic): EcKeyImportParams | RsaHashedImportParams { + if (jwk.kty === "EC") { + return { + name: "ECDSA", + namedCurve: jwk.crv === "P-521" ? "P-521" : jwk.crv === "P-384" ? "P-384" : "P-256" + }; + } + + if (algorithm.startsWith("PS")) { + return { + hash: { name: hashForAlgorithm(algorithm) }, + name: "RSA-PSS" + }; + } + + return { + hash: { name: hashForAlgorithm(algorithm) }, + name: "RSASSA-PKCS1-v1_5" + }; +} + +function webCryptoVerifyAlgorithm(algorithm: SupportedAlgorithm): EcdsaParams | RsaPssParams | RsaHashedImportParams { + if (algorithm.startsWith("ES")) { + return { + hash: { name: hashForAlgorithm(algorithm) }, + name: "ECDSA" + }; + } + + if (algorithm.startsWith("PS")) { + return { + hash: { name: hashForAlgorithm(algorithm) }, + name: "RSA-PSS", + saltLength: Number.parseInt(algorithm.slice(2), 10) / 8 + }; + } + + return { + hash: { name: hashForAlgorithm(algorithm) }, + name: "RSASSA-PKCS1-v1_5" + }; +} + +function hashForAlgorithm(algorithm: SupportedAlgorithm): "SHA-256" | "SHA-384" | "SHA-512" { + if (algorithm.endsWith("384")) { + return "SHA-384"; + } + + if (algorithm.endsWith("512")) { + return "SHA-512"; + } + + return "SHA-256"; +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationStorage.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationStorage.ts new file mode 100644 index 0000000000..07f3ae4b5c --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/src/verificationStorage.ts @@ -0,0 +1,128 @@ +import { RecordDuplicateError, RecordNotFoundError } from "@credo-ts/core"; +import type { AgentContext, BaseRecord, BaseRecordConstructor, Query, QueryOptions, StorageService } from "@credo-ts/core"; + +type StoredRecord = BaseRecord; + +export class InMemoryStorageService implements StorageService { + public readonly supportsCursorPagination = false; + private readonly records = new Map(); + + public async save(_agentContext: AgentContext, record: StoredRecord) { + const key = this.keyFor(record.type, record.id); + + if (this.records.has(key)) { + throw new RecordDuplicateError(`Record with id '${record.id}' already exists.`, { recordType: record.type }); + } + + this.records.set(key, record); + } + + public async update(_agentContext: AgentContext, record: StoredRecord) { + const key = this.keyFor(record.type, record.id); + + if (!this.records.has(key)) { + throw new RecordNotFoundError(`Record with id '${record.id}' was not found.`, { recordType: record.type }); + } + + this.records.set(key, record); + } + + public async delete(_agentContext: AgentContext, record: StoredRecord) { + await this.deleteById(_agentContext, record.constructor as BaseRecordConstructor, record.id); + } + + public async deleteById(_agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string) { + if (!this.records.delete(this.keyFor(recordClass.type, id))) { + throw new RecordNotFoundError(`Record with id '${id}' was not found.`, { recordType: recordClass.type }); + } + } + + public async getById(_agentContext: AgentContext, recordClass: BaseRecordConstructor, id: string) { + const record = this.records.get(this.keyFor(recordClass.type, id)); + + if (!record) { + throw new RecordNotFoundError(`Record with id '${id}' was not found.`, { recordType: recordClass.type }); + } + + return record; + } + + public async getAll(_agentContext: AgentContext, recordClass: BaseRecordConstructor) { + return Array.from(this.records.values()).filter((record) => record.type === recordClass.type); + } + + public async findByQuery( + agentContext: AgentContext, + recordClass: BaseRecordConstructor, + query: Query, + queryOptions?: QueryOptions + ) { + const records = await this.getAll(agentContext, recordClass); + const matchingRecords = records.filter((record) => this.matchesQuery(record, query)); + const offset = queryOptions?.offset ?? 0; + const limit = queryOptions?.limit ?? matchingRecords.length; + + return matchingRecords.slice(offset, offset + limit); + } + + private matchesQuery(record: StoredRecord, query: Query): boolean { + const advancedQuery = query as { + $and?: Query[]; + $not?: Query; + $or?: Query[]; + }; + + if (advancedQuery.$and) { + return advancedQuery.$and.every((subQuery) => this.matchesQuery(record, subQuery)); + } + + if (advancedQuery.$or) { + return advancedQuery.$or.some((subQuery) => this.matchesQuery(record, subQuery)); + } + + if (advancedQuery.$not) { + return !this.matchesQuery(record, advancedQuery.$not); + } + + const tags = record.getTags(); + return Object.entries(query).every(([key, value]) => tags[key] === value); + } + + private keyFor(type: string, id: string) { + return `${type}:${id}`; + } +} + +export class BrowserFileSystem { + public readonly cachePath = "/"; + public readonly dataPath = "/"; + public readonly tempPath = "/"; + + public async exists() { + return false; + } + + public async createDirectory() { + return undefined; + } + + public async copyFile() { + return undefined; + } + + public async write() { + return undefined; + } + + public async read() { + return ""; + } + + public async delete() { + return undefined; + } + + public async downloadToFile() { + return undefined; + } +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/test/verification.test.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/test/verification.test.ts new file mode 100644 index 0000000000..d9f811e94a --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/test/verification.test.ts @@ -0,0 +1,386 @@ +import { DidJwk, Kms, TypedArrayEncoder, sdJwtVcHasher } from "@credo-ts/core"; +import { ed25519 } from "@noble/curves/ed25519.js"; +import { SDJwtInstance } from "@sd-jwt/core"; +import { describe, expect, it } from "vitest"; +import { PresentationValidationErrorCode, validatePresentedCredential } from "../src/verification"; + +const now = new Date("2030-01-01T12:00:00.000Z"); +const nowInSeconds = Math.floor(now.getTime() / 1000); +const expectedAudience = "defaultPresentationAudience"; +const expectedNonce = "TOK-unit-test-reference"; +const issuerKey = createSigningKey(1); +const holderKey = createSigningKey(101); +const otherHolderKey = createSigningKey(201); + +describe("validatePresentedCredential", () => { + it("accepts a correctly signed and request-bound SD-JWT presentation", async () => { + const presentation = await createPresentation(); + + const result = await validate(presentation); + + expect(result.isValid).toBe(true); + expect(result.errorCode).toBeUndefined(); + expect(result.credential.claims).toEqual( + expect.arrayContaining([ + { label: "Vorname", value: "Maria" }, + { label: "Nachname", value: "Müller" } + ]) + ); + }); + + it("rejects a presentation whose issuer signature was changed", async () => { + const presentation = await createPresentation(); + const tamperedPresentation = replacePart(presentation, 0, tamperJwtSignature); + + const result = await validate(tamperedPresentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidSignature); + }); + + it("rejects a presentation whose key-binding signature was changed", async () => { + const presentation = await createPresentation(); + const parts = presentation.split("~"); + let keyBindingIndex = parts.length - 1; + while (keyBindingIndex > 0 && parts[keyBindingIndex].split(".").length !== 3) { + keyBindingIndex--; + } + const tamperedPresentation = replacePart(presentation, keyBindingIndex, tamperJwtSignature); + + const result = await validate(tamperedPresentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidSignature); + }); + + it("rejects a disclosed claim that no longer matches the digest signed by the issuer", async () => { + const presentation = await createPresentation(); + const parts = presentation.split("~"); + const disclosure = JSON.parse(TypedArrayEncoder.toUtf8String(TypedArrayEncoder.fromBase64Url(parts[1]))) as [string, string, unknown]; + disclosure[2] = "Manipulated"; + parts[1] = TypedArrayEncoder.toBase64Url(TypedArrayEncoder.fromUtf8String(JSON.stringify(disclosure))); + + const result = await validate(parts.join("~")); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidPresentationBinding); + }); + + it("rejects an incorrect sd_hash even if the modified key-binding JWT has a valid holder signature", async () => { + const presentation = await createPresentation(); + const parts = presentation.split("~"); + const keyBindingIndex = parts.length - 1; + const [encodedHeader, encodedPayload] = parts[keyBindingIndex].split("."); + const payload = JSON.parse(TypedArrayEncoder.toUtf8String(TypedArrayEncoder.fromBase64Url(encodedPayload))) as Record; + payload.sd_hash = "not-the-hash-of-this-presentation"; + const changedPayload = TypedArrayEncoder.toBase64Url(TypedArrayEncoder.fromUtf8String(JSON.stringify(payload))); + const unsignedKeyBinding = `${encodedHeader}.${changedPayload}`; + parts[keyBindingIndex] = `${unsignedKeyBinding}.${await holderKey.signer(unsignedKeyBinding)}`; + + const result = await validate(parts.join("~")); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidPresentationBinding); + }); + + it("rejects a key-binding JWT signed by a key other than the holder key in cnf", async () => { + const presentation = await createPresentation({ presentationSigningKey: otherHolderKey }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidSignature); + }); + + it("rejects an SD-JWT without a key-binding presentation", async () => { + const credential = await createCredential(); + + const result = await validate(credential); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingKeyBinding); + }); + + it("rejects a correctly signed standalone JWT VC without presentation binding", async () => { + const credential = await createStandaloneJwtCredential(); + + const result = await validate(credential); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingKeyBinding); + }); + + it("rejects a standalone JSON-LD VC without presentation binding", async () => { + const result = await validate({ + "@context": ["https://www.w3.org/2018/credentials/v1"], + credentialSubject: { id: "did:example:holder", name: "Maria Müller" }, + issuer: "did:example:issuer", + proof: { type: "DataIntegrityProof" }, + type: ["VerifiableCredential"] + }); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingKeyBinding); + }); + + it("rejects a presentation created for a different nonce", async () => { + const presentation = await createPresentation({ nonce: "TOK-another-reference" }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.NonceMismatch); + }); + + it("rejects a presentation created for a different audience", async () => { + const presentation = await createPresentation({ audience: "another-audience" }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.AudienceMismatch); + }); + + it("rejects an expired credential even when both signatures are valid", async () => { + const presentation = await createPresentation({ expiresAt: nowInSeconds - 1 }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.CredentialExpired); + }); + + it("rejects a credential whose validity period has not started", async () => { + const presentation = await createPresentation({ notBefore: nowInSeconds + 1 }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.CredentialNotYetValid); + }); + + it("rejects an unsupported SD-JWT typ header", async () => { + const presentation = await createPresentation({ type: "JWT" }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.UnsupportedSdJwtType); + }); + + it("rejects a cryptographically valid SD-JWT without a vct claim", async () => { + const presentation = await createPresentation({ includeVct: false }); + + const result = await validate(presentation); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingCredentialType); + }); + + it("rejects the complete response when one of multiple presentations is invalid", async () => { + const validPresentation = await createPresentation(); + const invalidPresentation = replacePart(await createPresentation(), 0, tamperJwtSignature); + + const result = await validate([validPresentation, invalidPresentation]); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.InvalidSignature); + }); + + it("rejects empty input", async () => { + const result = await validatePresentedCredential(undefined, { + expectedAudience, + expectedNonce, + now + }); + + expect(result).toEqual({ + credential: {}, + errorCode: PresentationValidationErrorCode.MissingPresentation, + isValid: false + }); + }); + + it("rejects validation without a nonce before doing any cryptographic work", async () => { + const presentation = await createPresentation(); + + const result = await validatePresentedCredential(presentation, { + expectedAudience, + expectedNonce: "", + now + }); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingChallenge); + }); + + it("rejects validation without an audience before doing any cryptographic work", async () => { + const presentation = await createPresentation(); + + const result = await validatePresentedCredential(presentation, { + expectedAudience: "", + expectedNonce, + now + }); + + expect(result.isValid).toBe(false); + expect(result.errorCode).toBe(PresentationValidationErrorCode.MissingAudience); + }); +}); + +type SigningKey = { + privateKey: Uint8Array; + publicJwk: Kms.KmsJwkPublicOkp; + signer: (data: string) => Promise; +}; + +type PresentationOverrides = { + audience?: string; + expiresAt?: number; + includeVct?: boolean; + nonce?: string; + notBefore?: number; + presentationSigningKey?: SigningKey; + type?: string; +}; + +async function validate(presentation: unknown) { + return validatePresentedCredential(presentation, { + expectedAudience, + expectedNonce, + now + }); +} + +async function createPresentation(overrides: PresentationOverrides = {}) { + const credential = await createCredential(overrides); + const presenter = new SDJwtInstance({ + hasher: sdJwtVcHasher, + kbSignAlg: "EdDSA", + kbSigner: (overrides.presentationSigningKey ?? holderKey).signer + }); + + return presenter.present( + credential, + { GivenName: true, Surname: true }, + { + kb: { + payload: { + aud: overrides.audience ?? expectedAudience, + iat: nowInSeconds, + nonce: overrides.nonce ?? expectedNonce + } + } + } + ); +} + +async function createCredential(overrides: PresentationOverrides = {}) { + const issuerDid = DidJwk.fromPublicJwk(Kms.PublicJwk.fromPublicJwk(issuerKey.publicJwk)); + const issuer = new SDJwtInstance({ + hasher: sdJwtVcHasher, + saltGenerator: generateSalt, + signAlg: "EdDSA", + signer: issuerKey.signer + }); + const payload: TestCredentialPayload = { + GivenName: "Maria", + Surname: "Müller", + cnf: { jwk: holderKey.publicJwk }, + exp: overrides.expiresAt ?? nowInSeconds + 3600, + iat: nowInSeconds - 60, + iss: issuerDid.did, + nbf: overrides.notBefore ?? nowInSeconds - 60 + }; + + if (overrides.includeVct !== false) { + payload.vct = "https://example.com/credentials/heidelberg-pass"; + } + + return issuer.issue( + payload, + { _sd: ["GivenName", "Surname"] }, + { + header: { + kid: issuerDid.verificationMethodId, + typ: overrides.type ?? "dc+sd-jwt" + } + } + ); +} + +async function createStandaloneJwtCredential() { + const issuerDid = DidJwk.fromPublicJwk(Kms.PublicJwk.fromPublicJwk(issuerKey.publicJwk)); + const header = { + alg: "EdDSA", + kid: issuerDid.verificationMethodId, + typ: "JWT" + }; + const payload = { + exp: nowInSeconds + 3600, + iss: issuerDid.did, + nbf: nowInSeconds - 60, + sub: "did:example:holder", + vc: { + "@context": ["https://www.w3.org/2018/credentials/v1"], + credentialSubject: { + id: "did:example:holder", + name: "Maria Müller" + }, + type: ["VerifiableCredential", "IdentityCredential"] + } + }; + const encodedHeader = TypedArrayEncoder.toBase64Url(TypedArrayEncoder.fromUtf8String(JSON.stringify(header))); + const encodedPayload = TypedArrayEncoder.toBase64Url(TypedArrayEncoder.fromUtf8String(JSON.stringify(payload))); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + return `${signingInput}.${await issuerKey.signer(signingInput)}`; +} + +type TestCredentialPayload = { + GivenName: string; + Surname: string; + cnf: { jwk: Kms.KmsJwkPublicOkp }; + exp: number; + iat: number; + iss: string; + nbf: number; + vct?: string; +}; + +function createSigningKey(seed: number): SigningKey { + const privateKey = Uint8Array.from({ length: 32 }, (_value, index) => (seed + index) % 256); + const publicJwk: Kms.KmsJwkPublicOkp = { + alg: "EdDSA", + crv: "Ed25519", + key_ops: ["verify"], + kty: "OKP", + use: "sig", + x: TypedArrayEncoder.toBase64Url(ed25519.getPublicKey(privateKey)) + }; + + return { + privateKey, + publicJwk, + signer: async (data) => TypedArrayEncoder.toBase64Url(ed25519.sign(TypedArrayEncoder.fromUtf8String(data), privateKey)) + }; +} + +function generateSalt(length: number) { + const salt = new Uint8Array(length); + crypto.getRandomValues(salt); + return TypedArrayEncoder.toBase64Url(salt); +} + +function replacePart(value: string, index: number, transform: (part: string) => string) { + const parts = value.split("~"); + parts[index] = transform(parts[index]); + return parts.join("~"); +} + +function tamperJwtSignature(jwt: string) { + const parts = jwt.split("."); + const signature = parts[2]; + parts[2] = `${signature.startsWith("A") ? "B" : "A"}${signature.slice(1)}`; + return parts.join("."); +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/tsconfig.json b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/tsconfig.json new file mode 100644 index 0000000000..4a48665030 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "lib": ["DOM", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "useDefineForClassFields": true + }, + "include": ["src", "test"] +} diff --git a/Applications/ConsumerApi/src/OpenId4VpVerifierSite/vite.config.ts b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/vite.config.ts new file mode 100644 index 0000000000..533eced488 --- /dev/null +++ b/Applications/ConsumerApi/src/OpenId4VpVerifierSite/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + base: "/openid4vp-verifier/", + build: { + assetsDir: "assets", + chunkSizeWarningLimit: 5000, + cssCodeSplit: false, + emptyOutDir: true, + lib: { + entry: "src/main.ts", + formats: ["es"] + }, + outDir: "dist/openid4vp-verifier", + rollupOptions: { + output: { + assetFileNames: (assetInfo) => (assetInfo.name?.endsWith(".css") ? "assets/verifier.css" : "assets/[name][extname]"), + entryFileNames: "assets/verifier.js" + } + }, + sourcemap: false + } +}); diff --git a/Applications/ConsumerApi/src/Views/AppOnboarding/AppOnboarding.cshtml b/Applications/ConsumerApi/src/Views/AppOnboarding/AppOnboarding.cshtml index 8b6fb0903b..ba98d78019 100644 --- a/Applications/ConsumerApi/src/Views/AppOnboarding/AppOnboarding.cshtml +++ b/Applications/ConsumerApi/src/Views/AppOnboarding/AppOnboarding.cshtml @@ -1,16 +1,21 @@ @model Backbone.ConsumerApi.Controllers.Onboarding.AppOnboardingModel +@inject Microsoft.AspNetCore.Mvc.ViewFeatures.IFileVersionProvider FileVersionProvider @{ Layout = null; + var verifierStylePath = FileVersionProvider.AddFileVersionToPath(Context.Request.PathBase, "/openid4vp-verifier/assets/verifier.css"); + var verifierScriptPath = FileVersionProvider.AddFileVersionToPath(Context.Request.PathBase, "/openid4vp-verifier/assets/verifier.js"); } - Onboarding + Einrichtung @@ -87,13 +92,108 @@ + + + + - + +
+
+
+

+ Ein Nachweis wurde Ihnen präsentiert.
+ Überprüfen Sie die Gültigkeit! +

+ +
+ + + +
+ +
+

Der Nachweis
wird geprüft.

+ + + +
+ + + +
+ +
+
+ + +
+
+
@@ -136,4 +236,5 @@
+ diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/AppOnboarding/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/AppOnboarding/GET.feature index 56dc89b822..9803293b27 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/AppOnboarding/GET.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/AppOnboarding/GET.feature @@ -20,3 +20,8 @@ User requests app onboarding page When a call is made to the /r/resourceId endpoint for the app "enmeshed" Then the response contains a link with the domain name "apps.apple.com" And the response contains a link with the domain name "play.google.com" + + Scenario: User requests app onboarding page and verifier bootstrap is contained on the response page + Given an http client with user agent "unknown" + When a call is made to the /r/resourceId endpoint for the app "enmeshed" + Then the response contains the OpenID4VP verifier bootstrap diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/AppOnboardingStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/AppOnboardingStepDefinitions.cs index 7478473969..4055160033 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/AppOnboardingStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/AppOnboardingStepDefinitions.cs @@ -71,5 +71,19 @@ public void ThenTheResponseDoesNotContainALinkContaining(string url) matches.ShouldNotContain(link => link.Value.Contains(url)); } + [Then("the response contains the OpenID4VP verifier bootstrap")] + public async Task ThenTheResponseContainsTheOpenId4VpVerifierBootstrap() + { + _onboardingResponse.ShouldNotBeNull(); + + var responseContent = await _onboardingResponse.Content.ReadAsStringAsync(); + + responseContent.ShouldContain("id=\"openid4vp-verifier-root\""); + responseContent.ShouldContain("data-reference-id=\"tok12345\""); + responseContent.ShouldContain("/openid4vp-verifier/assets/verifier.css"); + responseContent.ShouldContain("/openid4vp-verifier/assets/verifier.js"); + responseContent.ShouldContain("Der Nachweis
wird geprüft."); + } + #endregion } diff --git a/appsettings.override.json b/appsettings.override.json index 0528753d11..79a9a76e7b 100644 --- a/appsettings.override.json +++ b/appsettings.override.json @@ -6,7 +6,7 @@ "ApiKey": "test" }, "Cors": { - "AllowedOrigins": "http://localhost:8080;https://localhost:8080", + "AllowedOrigins": "*", "AccessControlAllowCredentials": true }, "Infrastructure": {