From 96e76a9d553bff34274b5ad243d0154cd220061b Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Sun, 18 Aug 2024 12:49:15 +0200 Subject: [PATCH] Feat/type metadata (#247) feat: add type metadata fetching and validation via vct URL Signed-off-by: Mirko Mollik --- examples/sd-jwt-vc-example/all.ts | 2 +- examples/sd-jwt-vc-example/basic.ts | 2 +- examples/sd-jwt-vc-example/custom.ts | 2 +- examples/sd-jwt-vc-example/custom_header.ts | 2 +- examples/sd-jwt-vc-example/decoy.ts | 2 +- examples/sd-jwt-vc-example/kb.ts | 2 +- packages/browser-crypto/src/crypto.ts | 7 +- packages/core/src/index.ts | 4 +- packages/node-crypto/src/crypto.ts | 11 +- packages/sd-jwt-vc/README.md | 20 ++ packages/sd-jwt-vc/package.json | 9 +- packages/sd-jwt-vc/src/sd-jwt-vc-config.ts | 12 +- packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts | 185 ++++++++++- packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts | 2 + .../src/sd-jwt-vc-type-metadata-format.ts | 13 + packages/sd-jwt-vc/src/sd-jwt-vc-vct.ts | 6 + packages/sd-jwt-vc/src/test/index.spec.ts | 5 +- packages/sd-jwt-vc/src/test/vct.spec.ts | 128 ++++++++ packages/sd-jwt-vc/src/verification-result.ts | 15 + packages/sd-jwt-vc/test/app-e2e.spec.ts | 4 +- packages/types/src/type.ts | 5 +- pnpm-lock.yaml | 297 +++++++++++++++++- 22 files changed, 699 insertions(+), 36 deletions(-) create mode 100644 packages/sd-jwt-vc/src/sd-jwt-vc-type-metadata-format.ts create mode 100644 packages/sd-jwt-vc/src/sd-jwt-vc-vct.ts create mode 100644 packages/sd-jwt-vc/src/test/vct.spec.ts create mode 100644 packages/sd-jwt-vc/src/verification-result.ts diff --git a/examples/sd-jwt-vc-example/all.ts b/examples/sd-jwt-vc-example/all.ts index c2d92756..9b64dfa9 100644 --- a/examples/sd-jwt-vc-example/all.ts +++ b/examples/sd-jwt-vc-example/all.ts @@ -57,7 +57,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/examples/sd-jwt-vc-example/basic.ts b/examples/sd-jwt-vc-example/basic.ts index 81f702f1..2ed209bb 100644 --- a/examples/sd-jwt-vc-example/basic.ts +++ b/examples/sd-jwt-vc-example/basic.ts @@ -34,7 +34,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/examples/sd-jwt-vc-example/custom.ts b/examples/sd-jwt-vc-example/custom.ts index fbd5e48a..1a2a5a7a 100644 --- a/examples/sd-jwt-vc-example/custom.ts +++ b/examples/sd-jwt-vc-example/custom.ts @@ -34,7 +34,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/examples/sd-jwt-vc-example/custom_header.ts b/examples/sd-jwt-vc-example/custom_header.ts index 24dccc85..31eb3d8b 100644 --- a/examples/sd-jwt-vc-example/custom_header.ts +++ b/examples/sd-jwt-vc-example/custom_header.ts @@ -34,7 +34,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/examples/sd-jwt-vc-example/decoy.ts b/examples/sd-jwt-vc-example/decoy.ts index 6f581bae..109e4c47 100644 --- a/examples/sd-jwt-vc-example/decoy.ts +++ b/examples/sd-jwt-vc-example/decoy.ts @@ -30,7 +30,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/examples/sd-jwt-vc-example/kb.ts b/examples/sd-jwt-vc-example/kb.ts index ce3ccd04..6ce9858c 100644 --- a/examples/sd-jwt-vc-example/kb.ts +++ b/examples/sd-jwt-vc-example/kb.ts @@ -37,7 +37,7 @@ import { createSignerVerifier, digest, generateSalt } from './utils'; { iss: 'Issuer', iat: new Date().getTime(), - vct: 'https://example.com', + vct: 'ExampleCredentials', ...claims, }, disclosureFrame, diff --git a/packages/browser-crypto/src/crypto.ts b/packages/browser-crypto/src/crypto.ts index baa50a4a..4d116a01 100644 --- a/packages/browser-crypto/src/crypto.ts +++ b/packages/browser-crypto/src/crypto.ts @@ -14,11 +14,14 @@ export const generateSalt = (length: number): string => { }; export async function digest( - data: string, + data: string | ArrayBuffer, algorithm = 'SHA-256', ): Promise { const ec = new TextEncoder(); - const digest = await window.crypto.subtle.digest(algorithm, ec.encode(data)); + const digest = await window.crypto.subtle.digest( + algorithm, + typeof data === 'string' ? ec.encode(data) : data, + ); return new Uint8Array(digest); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8b64aaa2..4550b8d0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,9 +10,9 @@ import { type PresentationFrame, type SDJWTCompact, type SDJWTConfig, + type JwtPayload, } from '@sd-jwt/types'; import { getSDAlgAndPayload } from '@sd-jwt/decode'; -import type { JwtPayload } from '@sd-jwt/types'; export * from './sdjwt'; export * from './kbjwt'; @@ -25,7 +25,7 @@ export class SDJwtInstance { //header type protected type?: string; - public static DEFAULT_hashAlg = 'sha-256'; + public static readonly DEFAULT_hashAlg = 'sha-256'; protected userConfig: SDJWTConfig = {}; diff --git a/packages/node-crypto/src/crypto.ts b/packages/node-crypto/src/crypto.ts index 32a35023..04c1ce72 100644 --- a/packages/node-crypto/src/crypto.ts +++ b/packages/node-crypto/src/crypto.ts @@ -9,10 +9,17 @@ export const generateSalt = (length: number): string => { return salt.substring(0, length); }; -export const digest = (data: string, algorithm = 'SHA-256'): Uint8Array => { +export const digest = ( + data: string | ArrayBuffer, + algorithm = 'SHA-256', +): Uint8Array => { const nodeAlg = toNodeCryptoAlg(algorithm); const hash = createHash(nodeAlg); - hash.update(data); + if (typeof data === 'string') { + hash.update(data); + } else { + hash.update(Buffer.from(data)); + } const hashBuffer = hash.digest(); return new Uint8Array(hashBuffer); }; diff --git a/packages/sd-jwt-vc/README.md b/packages/sd-jwt-vc/README.md index 067e2bd0..98c2261a 100644 --- a/packages/sd-jwt-vc/README.md +++ b/packages/sd-jwt-vc/README.md @@ -84,8 +84,28 @@ const verified = await sdjwt.verify(presentation); Check out more details in our [documentation](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/docs) or [examples](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/examples) ### Revocation + To add revocation capabilities, you can use the `@sd-jwt/jwt-status-list` library to create a JWT Status List and include it in the SD-JWT-VC. +### Type Metadata + +By setting the `loadTypeMetadataFormat` to `true` like this: + +```typescript +const sdjwt = new SDJwtVcInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + loadTypeMetadataFormat: true, +}); +``` + +The library will load load the type metadata format based on the `vct` value according to the [SD-JWT-VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata) and validate this schema. + +Since at this point the display is not yet implemented, the library will only validate the schema and return the type metadata format. In the future the values of the type metadata can be fetched via a function call. ### Dependencies diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index e5a77a0e..5e19fd75 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -15,7 +15,7 @@ "build": "rm -rf **/dist && tsup", "lint": "biome lint ./src", "test": "pnpm run test:node && pnpm run test:browser && pnpm run test:e2e && pnpm run test:cov", - "test:node": "vitest run ./src/test/*.spec.ts && vitest run ./src/test/*.spec.ts --environment jsdom", + "test:node": "vitest run ./src/test/*.spec.ts", "test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom", "test:e2e": "vitest run ./test/*e2e.spec.ts --environment node", "test:cov": "vitest run --coverage" @@ -37,12 +37,15 @@ "dependencies": { "@sd-jwt/core": "workspace:*", "@sd-jwt/jwt-status-list": "workspace:*", - "@sd-jwt/utils": "workspace:*" + "@sd-jwt/utils": "workspace:*", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" }, "devDependencies": { "@sd-jwt/crypto-nodejs": "workspace:*", "@sd-jwt/types": "workspace:*", - "jose": "^5.2.2" + "jose": "^5.2.2", + "msw": "^2.3.5" }, "publishConfig": { "access": "public" diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-config.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-config.ts index 661cb8b5..aefa55ef 100644 --- a/packages/sd-jwt-vc/src/sd-jwt-vc-config.ts +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-config.ts @@ -1,11 +1,19 @@ import type { SDJWTConfig } from '@sd-jwt/types'; +import type { VcTFetcher } from './sd-jwt-vc-vct'; + +export type StatusListFetcher = (uri: string) => Promise; +export type StatusValidator = (status: number) => Promise; /** * Configuration for SD-JWT-VC */ export type SDJWTVCConfig = SDJWTConfig & { // A function that fetches the status list from the uri. If not provided, the library will assume that the response is a compact JWT. - statusListFetcher?: (uri: string) => Promise; + statusListFetcher?: StatusListFetcher; // validte the status and decide if the status is valid or not. If not provided, the code will continue if it is 0, otherwise it will throw an error. - statusValidator?: (status: number) => Promise; + statusValidator?: StatusValidator; + // a function that fetches the type metadata format from the uri. If not provided, the library will assume that the response is a TypeMetadataFormat. Caching has to be implemented in this function. If the integrity value is passed, it to be validated according to https://www.w3.org/TR/SRI/ + vctFetcher?: VcTFetcher; + // if set to true, it will load the metadata format based on the vct value. If not provided, it will default to false. + loadTypeMetadataFormat?: boolean; }; diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts index 8406a971..ee1f0087 100644 --- a/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts @@ -1,13 +1,23 @@ import { Jwt, SDJwtInstance } from '@sd-jwt/core'; -import type { DisclosureFrame, Verifier } from '@sd-jwt/types'; +import type { DisclosureFrame, Hasher, Verifier } from '@sd-jwt/types'; import { SDJWTException } from '@sd-jwt/utils'; import type { SdJwtVcPayload } from './sd-jwt-vc-payload'; -import type { SDJWTVCConfig } from './sd-jwt-vc-config'; +import type { + SDJWTVCConfig, + StatusListFetcher, + StatusValidator, +} from './sd-jwt-vc-config'; import { type StatusListJWTPayload, getListFromStatusListJWT, + type StatusListJWTHeaderParameters, } from '@sd-jwt/jwt-status-list'; -import type { StatusListJWTHeaderParameters } from '@sd-jwt/jwt-status-list'; +import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format'; +import Ajv, { type SchemaObject } from 'ajv'; +import type { VerificationResult } from './verification-result'; +import addFormats from 'ajv-formats'; +import type { VcTFetcher } from './sd-jwt-vc-vct'; + export class SDJwtVcInstance extends SDJwtInstance { /** * The type of the SD-JWT-VC set in the header.typ field. @@ -95,7 +105,7 @@ export class SDJwtVcInstance extends SDJwtInstance { } /** - * Verifies the SD-JWT-VC. + * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT. */ async verify( encodedSDJwt: string, @@ -103,7 +113,7 @@ export class SDJwtVcInstance extends SDJwtInstance { requireKeyBindings?: boolean, ) { // Call the parent class's verify method - const result = await super + const result: VerificationResult = await super .verify(encodedSDJwt, requiredClaimKeys, requireKeyBindings) .then((res) => { return { @@ -113,12 +123,167 @@ export class SDJwtVcInstance extends SDJwtInstance { }; }); + await this.verifyStatus(result); + if (this.userConfig.loadTypeMetadataFormat) { + await this.verifyVct(result); + } + return result; + } + + /** + * Default function to fetch the VCT from the uri. We assume that the vct is a URL that is used to fetch the VCT. + * @param uri + * @returns + */ + private async vctFetcher( + uri: string, + integrity?: string, + ): Promise { + // modify the uri based on https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#section-6.3.1 + const elements = uri.split('/'); + //insert a new element on the thrid position, but not replace it + elements.splice(3, 0, '.well-known/vct'); + const url = elements.join('/'); + return this.fetch(url, integrity); + } + + /** + * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown. + * @param integrity + * @param response + */ + private async validateIntegrity( + response: Response, + url: string, + integrity?: string, + ) { + if (integrity) { + // validate the integrity of the response according to https://www.w3.org/TR/SRI/ + const arrayBuffer = await response.arrayBuffer(); + const alg = integrity.split('-')[0]; + //TODO: error handling when a hasher is passed that is not supporting the required algorithm acording to the spec + const hashBuffer = await (this.userConfig.hasher as Hasher)( + arrayBuffer, + alg, + ); + const integrityHash = integrity.split('-')[1]; + const hash = Array.from(new Uint8Array(hashBuffer)) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); + if (hash !== integrityHash) { + throw new Error( + `Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`, + ); + } + } + } + + /** + * Fetches the content from the url with a timeout of 10 seconds. + * @param url + * @returns + */ + private async fetch(url: string, integrity?: string) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + try { + const response = await fetch(url, { + signal: controller.signal, + }); + if (!response.ok) { + throw new Error(await response.text()); + } + await this.validateIntegrity(response.clone(), url, integrity); + return response.json() as Promise; + } finally { + clearTimeout(timeoutId); + } + } + + /** + * Loads the schema either from the object or as fallback from the uri. + * @param typeMetadataFormat + * @returns + */ + private async loadSchema(typeMetadataFormat: TypeMetadataFormat) { + //if schema is present, return it + if (typeMetadataFormat.schema) return typeMetadataFormat.schema; + if (typeMetadataFormat.schema_uri) { + const schema = await this.fetch( + typeMetadataFormat.schema_uri, + typeMetadataFormat['schema_uri#Integrity'], + ); + return schema; + } + throw new Error('No schema or schema_uri found'); + } + + /** + * Verifies the VCT of the SD-JWT-VC. Returns the type metadata format. If the schema does not match, an error is thrown. If it matches, it will return the type metadata format. + * @param result + * @returns + */ + private async verifyVct( + result: VerificationResult, + ): Promise { + const fetcher: VcTFetcher = + this.userConfig.vctFetcher ?? this.vctFetcher.bind(this); + const typeMetadataFormat = await fetcher( + result.payload.vct, + result.payload['vct#Integrity'], + ); + + if (typeMetadataFormat.extends) { + // implement based on https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-extending-type-metadata + //TODO: needs to be implemented. Unclear at this point which values will overwrite the values from the extended type metadata format + } + + //init the json schema validator, load referenced schemas on demand + const schema = await this.loadSchema(typeMetadataFormat); + const loadedSchemas = new Set(); + // init the json schema validator + const ajv = new Ajv({ + loadSchema: async (uri: string) => { + if (loadedSchemas.has(uri)) { + return {}; + } + const response = await fetch(uri); + if (!response.ok) { + throw new Error( + `Error fetching schema: ${ + response.status + } ${await response.text()}`, + ); + } + loadedSchemas.add(uri); + return response.json(); + }, + }); + addFormats(ajv); + const validate = await ajv.compileAsync(schema); + const valid = validate(result.payload); + + if (!valid) { + throw new SDJWTException( + `Payload does not match the schema: ${JSON.stringify(validate.errors)}`, + ); + } + + return typeMetadataFormat; + } + + /** + * Verifies the status of the SD-JWT-VC. + * @param result + */ + private async verifyStatus(result: VerificationResult): Promise { if (result.payload.status) { //checks if a status field is present in the payload based on https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html if (result.payload.status.status_list) { // fetch the status list from the uri - const fetcher = - this.userConfig.statusListFetcher ?? this.statusListFetcher; + const fetcher: StatusListFetcher = + this.userConfig.statusListFetcher ?? + this.statusListFetcher.bind(this); // fetch the status list from the uri const statusListJWT = await fetcher( result.payload.status.status_list.uri, @@ -146,12 +311,10 @@ export class SDJwtVcInstance extends SDJwtInstance { ); // validate the status - const statusValidator = - this.userConfig.statusValidator ?? this.statusValidator; + const statusValidator: StatusValidator = + this.userConfig.statusValidator ?? this.statusValidator.bind(this); await statusValidator(status); } } - - return result; } } diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts index 62878a44..da5655f2 100644 --- a/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts @@ -12,6 +12,8 @@ export interface SdJwtVcPayload extends SdJwtPayload { cnf?: unknown; // REQUIRED. The type of the Verifiable Credential, e.g., https://credentials.example.com/identity_credential, as defined in Section 3.2.2.1.1. vct: string; + // OPTIONAL. If passed, the loaded type metadata format has to be validated according to https://www.w3.org/TR/SRI/ + 'vct#Integrity'?: string; // OPTIONAL. The information on how to read the status of the Verifiable Credential. See [https://www.ietf.org/archive/id/draft-ietf-oauth-status-list-02.html] for more information. status?: SDJWTVCStatusReference; // OPTIONAL. The identifier of the Subject of the Verifiable Credential. The Issuer MAY use it to provide the Subject identifier known by the Issuer. There is no requirement for a binding to exist between sub and cnf claims. diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-type-metadata-format.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-type-metadata-format.ts new file mode 100644 index 00000000..140b3636 --- /dev/null +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-type-metadata-format.ts @@ -0,0 +1,13 @@ +/** + * https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#name-type-metadata-format + */ +export type TypeMetadataFormat = { + vct: string; // REQUIRED. A URI that uniquely identifies the type. This URI MUST be dereferenceable to a JSON document that describes the type. + name?: string; // OPTIONAL. A human-readable name for the type, intended for developers reading the JSON document. + description?: string; // OPTIONAL. A human-readable description for the type, intended for developers reading the JSON document. + extends?: string; // OPTIONAL. A URI of another type that this type extends, as described in Section 6.4. + 'extends#Integrity'?: string; // OPTIONAL. Validating the ingegrity of the extends field + schema?: object; // OPTIONAL. An embedded JSON Schema document describing the structure of the Verifiable Credential as described in Section 6.5.1. schema MUST NOT be used if schema_uri is present. + schema_uri?: string; // OPTIONAL. A URL pointing to a JSON Schema document describing the structure of the Verifiable Credential as described in Section 6.5.1. schema_uri MUST NOT be used if schema is present. + 'schema_uri#Integrity'?: string; // OPTIONAL. Validating the integrity of the schema_uri field. +}; diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-vct.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-vct.ts new file mode 100644 index 00000000..62c70521 --- /dev/null +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-vct.ts @@ -0,0 +1,6 @@ +import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format'; + +export type VcTFetcher = ( + uri: string, + integrity?: string, +) => Promise; diff --git a/packages/sd-jwt-vc/src/test/index.spec.ts b/packages/sd-jwt-vc/src/test/index.spec.ts index b38cd9c8..5b684731 100644 --- a/packages/sd-jwt-vc/src/test/index.spec.ts +++ b/packages/sd-jwt-vc/src/test/index.spec.ts @@ -17,10 +17,13 @@ import { import { SignJWT } from 'jose'; const iss = 'ExampleIssuer'; -const vct = 'https://example.com/schema/1'; +const vct = 'ExampleCredentialType'; const iat = new Date().getTime() / 1000; const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); + +//TODO: to simulate a hosted status list, use the same appraoch as in vct.spec.ts + const createSignerVerifier = () => { const signer: Signer = async (data: string) => { const sig = Crypto.sign(null, Buffer.from(data), privateKey); diff --git a/packages/sd-jwt-vc/src/test/vct.spec.ts b/packages/sd-jwt-vc/src/test/vct.spec.ts new file mode 100644 index 00000000..f4e273fa --- /dev/null +++ b/packages/sd-jwt-vc/src/test/vct.spec.ts @@ -0,0 +1,128 @@ +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import type { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types'; +import { describe, test, beforeAll, afterAll } from 'vitest'; +import { SDJwtVcInstance } from '..'; +import type { SdJwtVcPayload } from '../sd-jwt-vc-payload'; +import Crypto from 'node:crypto'; +import { setupServer } from 'msw/node'; +import { HttpResponse, http } from 'msw'; +import { afterEach } from 'node:test'; +import type { TypeMetadataFormat } from '../sd-jwt-vc-type-metadata-format'; + +const restHandlers = [ + http.get('http://example.com/schema/example', () => { + const res = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + vct: { + type: 'string', + }, + iss: { + type: 'string', + }, + nbf: { + type: 'number', + }, + exp: { + type: 'number', + }, + cnf: { + type: 'object', + }, + status: { + type: 'object', + }, + firstName: { + type: 'string', + }, + }, + required: ['iss', 'vct'], + }; + return HttpResponse.json(res); + }), + http.get('http://exmaple.com/.well-known/vct/example', () => { + const res: TypeMetadataFormat = { + vct: 'http://example.com/example', + name: 'ExampleCredentialType', + description: 'An example credential type', + schema_uri: 'http://example.com/schema/example', + //this value could be generated on demand to make it easier when changing the values + 'schema_uri#Integrity': + 'sha256-48a61b283ded3b55e8d9a9b063327641dc4c53f76bd5daa96c23f232822167ae', + }; + return HttpResponse.json(res); + }), +]; + +//this value could be generated on demand to make it easier when changing the values +const vctIntegrity = + 'sha256-96bed58130a44af05ae8970aa9caa0bf0135cd15afe721ea29f553394692acef'; + +const server = setupServer(...restHandlers); + +const iss = 'ExampleIssuer'; +const vct = 'http://exmaple.com/example'; +const iat = new Date().getTime() / 1000; + +const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); + +const createSignerVerifier = () => { + const signer: Signer = async (data: string) => { + const sig = Crypto.sign(null, Buffer.from(data), privateKey); + return Buffer.from(sig).toString('base64url'); + }; + const verifier: Verifier = async (data: string, sig: string) => { + return Crypto.verify( + null, + Buffer.from(data), + publicKey, + Buffer.from(sig, 'base64url'), + ); + }; + return { signer, verifier }; +}; + +describe('App', () => { + beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); + + afterAll(() => server.close()); + + afterEach(() => server.resetHandlers()); + + test('VCT Validation', async () => { + const { signer, verifier } = createSignerVerifier(); + const sdjwt = new SDJwtVcInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + loadTypeMetadataFormat: true, + }); + + const claims = { + firstname: 'John', + }; + const disclosureFrame = { + _sd: ['firstname'], + }; + + const expectedPayload: SdJwtVcPayload = { + iat, + iss, + vct, + 'vct#Integrity': vctIntegrity, + ...claims, + }; + const encodedSdjwt = await sdjwt.issue( + expectedPayload, + disclosureFrame as unknown as DisclosureFrame, + ); + + await sdjwt.verify(encodedSdjwt); + }); + + //TODO: we need tests with an embedded schema, extended and maybe also to test the errors when schema information is not available or the integrity is not valid +}); diff --git a/packages/sd-jwt-vc/src/verification-result.ts b/packages/sd-jwt-vc/src/verification-result.ts new file mode 100644 index 00000000..8a3fd442 --- /dev/null +++ b/packages/sd-jwt-vc/src/verification-result.ts @@ -0,0 +1,15 @@ +import type { kbPayload, kbHeader } from '@sd-jwt/types'; +import type { SdJwtVcPayload } from './sd-jwt-vc-payload'; +import type { TypeMetadataFormat } from './sd-jwt-vc-type-metadata-format'; + +export type VerificationResult = { + payload: SdJwtVcPayload; + header: Record | undefined; + kb: + | { + payload: kbPayload; + header: kbHeader; + } + | undefined; + typeMetadataFormat?: TypeMetadataFormat; +}; diff --git a/packages/sd-jwt-vc/test/app-e2e.spec.ts b/packages/sd-jwt-vc/test/app-e2e.spec.ts index 51c62683..7f8958fd 100644 --- a/packages/sd-jwt-vc/test/app-e2e.spec.ts +++ b/packages/sd-jwt-vc/test/app-e2e.spec.ts @@ -1,5 +1,5 @@ import Crypto from 'node:crypto'; -import { SDJwtVcInstance, SdJwtVcPayload } from '../src/index'; +import { SDJwtVcInstance } from '../src/index'; import type { DisclosureFrame, PresentationFrame, @@ -29,7 +29,7 @@ const createSignerVerifier = () => { }; const iss = 'ExampleIssuer'; -const vct = 'https://example.com/schema/1'; +const vct = 'ExampleCredentials'; const iat = new Date().getTime() / 1000; describe('App', () => { diff --git a/packages/types/src/type.ts b/packages/types/src/type.ts index 6129a0af..f88cdaa7 100644 --- a/packages/types/src/type.ts +++ b/packages/types/src/type.ts @@ -79,7 +79,10 @@ export type KbVerifier = ( sig: string, payload: JwtPayload, ) => OrPromise; -export type Hasher = (data: string, alg: string) => OrPromise; +export type Hasher = ( + data: string | ArrayBuffer, + alg: string, +) => OrPromise; export type SaltGenerator = (length: number) => OrPromise; export type HasherAndAlg = { hasher: Hasher; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 929a5355..6bee7049 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,12 @@ importers: '@sd-jwt/utils': specifier: workspace:* version: link:../utils + ajv: + specifier: ^8.17.1 + version: 8.17.1 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.17.1) devDependencies: '@sd-jwt/crypto-nodejs': specifier: workspace:* @@ -241,6 +247,9 @@ importers: jose: specifier: ^5.2.2 version: 5.2.4 + msw: + specifier: ^2.3.5 + version: 2.3.5(typescript@5.4.5) packages/types: {} @@ -344,6 +353,15 @@ packages: cpu: [x64] os: [win32] + '@bundled-es-modules/cookie@2.0.0': + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -628,6 +646,22 @@ packages: resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} + '@inquirer/confirm@3.1.22': + resolution: {integrity: sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==} + engines: {node: '>=18'} + + '@inquirer/core@9.0.10': + resolution: {integrity: sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.5': + resolution: {integrity: sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==} + engines: {node: '>=18'} + + '@inquirer/type@1.5.2': + resolution: {integrity: sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==} + engines: {node: '>=18'} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -665,6 +699,10 @@ packages: resolution: {integrity: sha512-GzScCIkAW3tg3+Yn/MKCH9963bzG+zpjGz2NdfYDlYWI7p0f/SH46v1dqpPpYmZ2E/m3JK8HjTNNNL8eIm8/YQ==} engines: {node: '>=18.0.0'} + '@mswjs/interceptors@0.29.1': + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} + '@noble/hashes@1.0.0': resolution: {integrity: sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==} @@ -841,6 +879,15 @@ packages: '@octokit/types@9.3.2': resolution: {integrity: sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -995,6 +1042,9 @@ packages: resolution: {integrity: sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==} engines: {node: ^16.14.0 || >=18.0.0} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1004,6 +1054,9 @@ packages: '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node@20.11.19': resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} @@ -1013,12 +1066,24 @@ packages: '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@22.3.0': + resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} '@types/pako@2.0.3': resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@vitest/coverage-v8@1.5.1': resolution: {integrity: sha512-Zx+dYEDcZg+44ksjIWvWosIGlPLJB1PPpN3O8+Xrh/1qa7WSFA6Y8H7lsZJTYrxu4G2unk9tvP5TgjIGDliF1w==} peerDependencies: @@ -1086,6 +1151,17 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1131,6 +1207,7 @@ packages: are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1302,6 +1379,10 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -1394,6 +1475,10 @@ packages: engines: {node: '>=14'} hasBin: true + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1594,10 +1679,16 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-uri@3.0.1: + resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1670,6 +1761,7 @@ packages: gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -1733,10 +1825,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -1749,6 +1843,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1773,6 +1871,9 @@ packages: resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -1876,6 +1977,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1933,6 +2035,9 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2076,6 +2181,9 @@ packages: resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -2333,6 +2441,16 @@ packages: ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + msw@2.3.5: + resolution: {integrity: sha512-+GUI4gX5YC5Bv33epBrD+BGdmDvBg2XGruiWnI3GbIbRmMMBeZ5gs3mJ51OWSGHgJKztZ8AtZeYMMNMVrje2/Q==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + multimatch@5.0.0: resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} engines: {node: '>=10'} @@ -2462,6 +2580,7 @@ packages: npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} @@ -2509,6 +2628,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -2625,6 +2747,9 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} + path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -2753,10 +2878,12 @@ packages: read-package-json@6.0.4: resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. Please use @npmcli/package-json instead. read-package-json@7.0.0: resolution: {integrity: sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==} engines: {node: ^16.14.0 || >=18.0.0} + deprecated: This package is no longer supported. Please use @npmcli/package-json instead. read-pkg-up@3.0.0: resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} @@ -2797,6 +2924,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -2830,6 +2961,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@4.4.1: @@ -2983,9 +3115,16 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3116,8 +3255,8 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} tr46@0.0.3: @@ -3213,6 +3352,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.24.0: + resolution: {integrity: sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==} + engines: {node: '>=16'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -3237,6 +3380,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.18.2: + resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} + unique-filename@3.0.0: resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3485,6 +3631,10 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + snapshots: '@ampproject/remapping@2.2.1': @@ -3554,6 +3704,19 @@ snapshots: '@biomejs/cli-win32-x64@1.7.1': optional: true + '@bundled-es-modules/cookie@2.0.0': + dependencies: + cookie: 0.5.0 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -3698,6 +3861,33 @@ snapshots: '@hutson/parse-repository-url@3.0.2': {} + '@inquirer/confirm@3.1.22': + dependencies: + '@inquirer/core': 9.0.10 + '@inquirer/type': 1.5.2 + + '@inquirer/core@9.0.10': + dependencies: + '@inquirer/figures': 1.0.5 + '@inquirer/type': 1.5.2 + '@types/mute-stream': 0.0.4 + '@types/node': 22.3.0 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + + '@inquirer/figures@1.0.5': {} + + '@inquirer/type@1.5.2': + dependencies: + mute-stream: 1.0.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3810,6 +4000,15 @@ snapshots: - supports-color - typescript + '@mswjs/interceptors@0.29.1': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@noble/hashes@1.0.0': {} '@nodelib/fs.scandir@2.1.5': @@ -4012,6 +4211,15 @@ snapshots: dependencies: '@octokit/openapi-types': 18.1.1 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -4137,12 +4345,18 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.3 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.5': {} '@types/minimatch@3.0.5': {} '@types/minimist@1.2.5': {} + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 20.12.7 + '@types/node@20.11.19': dependencies: undici-types: 5.26.5 @@ -4155,10 +4369,20 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.3.0': + dependencies: + undici-types: 6.18.2 + '@types/normalize-package-data@2.4.4': {} '@types/pako@2.0.3': {} + '@types/statuses@2.0.5': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/wrap-ansi@3.0.0': {} + '@vitest/coverage-v8@1.5.1(vitest@1.5.1(@types/node@20.12.7)(jsdom@24.0.0))': dependencies: '@ampproject/remapping': 2.2.1 @@ -4252,6 +4476,17 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.1 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -4471,6 +4706,8 @@ snapshots: cli-width@3.0.0: {} + cli-width@4.1.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -4586,6 +4823,8 @@ snapshots: git-semver-tags: 5.0.1 meow: 8.1.2 + cookie@0.5.0: {} + core-util-is@1.0.3: {} cosmiconfig@8.3.6(typescript@5.4.5): @@ -4812,6 +5051,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-deep-equal@3.1.3: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4820,6 +5061,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.5 + fast-uri@3.0.1: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -4989,6 +5232,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.9.0: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -5010,6 +5255,8 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: {} + hosted-git-info@2.8.9: {} hosted-git-info@3.0.8: @@ -5179,6 +5426,8 @@ snapshots: is-lambda@1.0.1: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-obj@2.0.0: {} @@ -5300,7 +5549,7 @@ snapshots: rrweb-cssom: 0.6.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.3 + tough-cookie: 4.1.4 w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 3.1.1 @@ -5319,6 +5568,8 @@ snapshots: json-parse-even-better-errors@3.0.1: {} + json-schema-traverse@1.0.0: {} + json-stringify-safe@5.0.1: {} json5@2.2.3: {} @@ -5685,6 +5936,28 @@ snapshots: ms@2.1.2: {} + msw@2.3.5(typescript@5.4.5): + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 3.1.22 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.24.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.4.5 + multimatch@5.0.0: dependencies: '@types/minimatch': 3.0.5 @@ -5956,6 +6229,8 @@ snapshots: os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + p-finally@1.0.0: {} p-limit@1.3.0: @@ -6076,6 +6351,8 @@ snapshots: lru-cache: 10.2.0 minipass: 7.0.4 + path-to-regexp@6.2.2: {} + path-type@3.0.0: dependencies: pify: 3.0.0 @@ -6237,6 +6514,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-cwd@3.0.0: @@ -6431,8 +6710,12 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.7.0: {} + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6567,7 +6850,7 @@ snapshots: dependencies: is-number: 7.0.0 - tough-cookie@4.1.3: + tough-cookie@4.1.4: dependencies: psl: 1.9.0 punycode: 2.3.1 @@ -6703,6 +6986,8 @@ snapshots: type-fest@0.8.1: {} + type-fest@4.24.0: {} + typedarray@0.0.6: {} typescript@5.3.3: {} @@ -6716,6 +7001,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.18.2: {} + unique-filename@3.0.0: dependencies: unique-slug: 4.0.0 @@ -6958,3 +7245,5 @@ snapshots: yn@3.1.1: {} yocto-queue@1.0.0: {} + + yoctocolors-cjs@2.1.2: {}