@vidos-id/openid4vc-wallet 0.0.0-test1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1055 @@
1
+ import { pack } from "@sd-jwt/core";
2
+ import { hasher } from "@sd-jwt/hash";
3
+ import { EncryptJWT, SignJWT, base64url, calculateJwkThumbprint, decodeJwt, decodeProtectedHeader, exportJWK, generateKeyPair, importJWK, jwtVerify } from "jose";
4
+ import { z } from "zod";
5
+ import { inflateSync } from "node:zlib";
6
+ import { decodeSdJwt, getClaims, splitSdJwt } from "@sd-jwt/decode";
7
+ import { present } from "@sd-jwt/present";
8
+ import { DcqlPresentation, DcqlQuery, runDcqlQuery } from "dcql";
9
+ //#region src/crypto.ts
10
+ const HOLDER_KEY_ALG = "ES256";
11
+ const SD_JWT_HASH_ALG = "sha-256";
12
+ async function sha256Base64Url(input) {
13
+ const digest = hasher(input, "sha256");
14
+ return base64url.encode(digest);
15
+ }
16
+ async function sdJwtHasher(data, alg) {
17
+ if (alg !== "sha-256") throw new Error(`Unsupported SD-JWT hash algorithm: ${alg}`);
18
+ return hasher(data, "sha256");
19
+ }
20
+ async function createHolderKeyRecord(alg) {
21
+ const algorithm = alg ?? "ES256";
22
+ const { privateKey, publicKey } = algorithm === "EdDSA" ? await generateKeyPair("EdDSA", {
23
+ crv: "Ed25519",
24
+ extractable: true
25
+ }) : await generateKeyPair(algorithm, { extractable: true });
26
+ const publicJwk = await exportJWK(publicKey);
27
+ const privateJwk = await exportJWK(privateKey);
28
+ return {
29
+ id: await calculateJwkThumbprint(publicJwk),
30
+ algorithm,
31
+ publicJwk,
32
+ privateJwk,
33
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
34
+ };
35
+ }
36
+ async function getJwkThumbprint(jwk) {
37
+ return calculateJwkThumbprint(jwk);
38
+ }
39
+ async function importPrivateKey(jwk, alg = HOLDER_KEY_ALG) {
40
+ return importJWK(jwk, alg);
41
+ }
42
+ async function importPublicKey(jwk, alg) {
43
+ return importJWK(jwk, alg);
44
+ }
45
+ async function createKbJwt(input) {
46
+ const alg = input.alg ?? "ES256";
47
+ const privateKey = await importPrivateKey(input.holderPrivateJwk, alg);
48
+ const sdHash = await sha256Base64Url(input.sdJwtPresentation);
49
+ return new SignJWT({
50
+ aud: input.aud,
51
+ nonce: input.nonce,
52
+ sd_hash: sdHash,
53
+ iat: Math.floor(Date.now() / 1e3)
54
+ }).setProtectedHeader({
55
+ alg,
56
+ typ: "kb+jwt"
57
+ }).sign(privateKey);
58
+ }
59
+ async function createOpenId4VciProofJwt(input) {
60
+ const alg = input.alg ?? "ES256";
61
+ const privateKey = await importPrivateKey(input.holderPrivateJwk, alg);
62
+ return new SignJWT({
63
+ aud: input.aud,
64
+ nonce: input.nonce,
65
+ iat: Math.floor(Date.now() / 1e3)
66
+ }).setProtectedHeader({
67
+ alg,
68
+ typ: "openid4vci-proof+jwt",
69
+ jwk: input.holderPublicJwk
70
+ }).sign(privateKey);
71
+ }
72
+ async function issueDemoCredential(input) {
73
+ const signingAlg = input.issuerAlg ?? "ES256";
74
+ const issuerKey = await importPrivateKey(input.issuerPrivateJwk, signingAlg);
75
+ let saltIndex = 0;
76
+ const saltGenerator = input.saltGenerator ?? (async () => {
77
+ saltIndex += 1;
78
+ return `salt-${saltIndex}`;
79
+ });
80
+ const { packedClaims, disclosures } = await pack({
81
+ iss: input.issuer,
82
+ vct: input.vct,
83
+ cnf: { jwk: input.holderPublicJwk },
84
+ ...input.claims
85
+ }, input.disclosureFrame, {
86
+ alg: SD_JWT_HASH_ALG,
87
+ hasher: sdJwtHasher
88
+ }, saltGenerator);
89
+ return [
90
+ await new SignJWT({
91
+ ...packedClaims,
92
+ _sd_alg: input.disclosureFrame ? SD_JWT_HASH_ALG : void 0,
93
+ iat: input.issuedAt ?? Math.floor(Date.now() / 1e3)
94
+ }).setProtectedHeader({
95
+ alg: signingAlg,
96
+ typ: "dc+sd-jwt",
97
+ kid: input.issuerKid,
98
+ ...input.headers
99
+ }).sign(issuerKey),
100
+ ...disclosures.map((disclosure) => disclosure.encode()),
101
+ ""
102
+ ].join("~");
103
+ }
104
+ //#endregion
105
+ //#region src/schemas.ts
106
+ const JwkSchema = z.record(z.string(), z.unknown()).refine((value) => typeof value.kty === "string", "JWK must include kty");
107
+ const HolderKeyRecordSchema = z.object({
108
+ id: z.string().min(1),
109
+ algorithm: z.enum([
110
+ "ES256",
111
+ "ES384",
112
+ "EdDSA"
113
+ ]),
114
+ publicJwk: JwkSchema,
115
+ privateJwk: JwkSchema,
116
+ createdAt: z.string().datetime()
117
+ });
118
+ const CredentialStatusListReferenceSchema = z.object({
119
+ idx: z.number().int().nonnegative(),
120
+ uri: z.string().min(1)
121
+ });
122
+ const CredentialStatusSchema = z.object({ status_list: CredentialStatusListReferenceSchema });
123
+ const StoredCredentialRecordSchema = z.object({
124
+ id: z.string().min(1),
125
+ format: z.literal("dc+sd-jwt"),
126
+ compactSdJwt: z.string().min(1),
127
+ issuer: z.string().min(1),
128
+ vct: z.string().min(1),
129
+ holderKeyId: z.string().min(1),
130
+ claims: z.record(z.string(), z.unknown()),
131
+ status: CredentialStatusSchema.optional(),
132
+ issuerKeyMaterial: z.lazy(() => IssuerKeyMaterialSchema).optional(),
133
+ importedAt: z.string().datetime()
134
+ });
135
+ const IssuerJwksSchema = z.object({
136
+ issuer: z.string().min(1),
137
+ jwks: z.object({ keys: z.array(JwkSchema).min(1) })
138
+ });
139
+ const IssuerJwkSchema = z.object({
140
+ issuer: z.string().min(1),
141
+ jwk: JwkSchema
142
+ });
143
+ const IssuerKeyMaterialSchema = z.union([IssuerJwksSchema, IssuerJwkSchema]);
144
+ const ImportCredentialInputSchema = z.object({
145
+ credential: z.string().min(1),
146
+ issuer: IssuerKeyMaterialSchema.optional()
147
+ });
148
+ const ResponseModeSchema = z.enum(["direct_post", "direct_post.jwt"]);
149
+ const VerifierClientMetadataSchema = z.object({
150
+ jwks: z.object({ keys: z.array(JwkSchema).min(1) }).optional(),
151
+ encrypted_response_enc_values_supported: z.array(z.string().min(1)).min(1).optional(),
152
+ vp_formats_supported: z.unknown().optional()
153
+ }).passthrough();
154
+ const OpenId4VpRequestSchema = z.object({
155
+ client_id: z.string().min(1),
156
+ nonce: z.string().min(1),
157
+ dcql_query: z.unknown(),
158
+ state: z.string().min(1).optional(),
159
+ response_type: z.literal("vp_token").optional(),
160
+ response_mode: ResponseModeSchema.optional(),
161
+ response_uri: z.string().min(1).optional(),
162
+ client_metadata: VerifierClientMetadataSchema.optional(),
163
+ scope: z.unknown().optional(),
164
+ presentation_definition: z.unknown().optional()
165
+ }).superRefine((value, ctx) => {
166
+ if (value.scope !== void 0) ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ message: "scope-encoded queries are unsupported",
169
+ path: ["scope"]
170
+ });
171
+ if (value.presentation_definition !== void 0) ctx.addIssue({
172
+ code: z.ZodIssueCode.custom,
173
+ message: "Presentation Exchange is unsupported",
174
+ path: ["presentation_definition"]
175
+ });
176
+ if (value.response_type !== void 0 && value.response_type !== "vp_token") ctx.addIssue({
177
+ code: z.ZodIssueCode.custom,
178
+ message: "Only response_type=vp_token is supported",
179
+ path: ["response_type"]
180
+ });
181
+ if (value.response_mode && !value.response_uri) ctx.addIssue({
182
+ code: z.ZodIssueCode.custom,
183
+ message: "response_uri is required for direct_post response modes",
184
+ path: ["response_uri"]
185
+ });
186
+ });
187
+ const WalletConfigSchema = z.object({ storage: z.custom((value) => typeof value === "object" && value !== null, { message: "storage is required" }) });
188
+ //#endregion
189
+ //#region src/wallet.ts
190
+ const RESERVED_TOP_LEVEL_CLAIMS = new Set([
191
+ "_sd",
192
+ "_sd_alg",
193
+ "cnf",
194
+ "exp",
195
+ "iat",
196
+ "iss",
197
+ "jti",
198
+ "nbf",
199
+ "status",
200
+ "sub",
201
+ "vct"
202
+ ]);
203
+ var WalletError = class extends Error {
204
+ constructor(message) {
205
+ super(message);
206
+ this.name = "WalletError";
207
+ }
208
+ };
209
+ var Wallet = class {
210
+ constructor(storage) {
211
+ this.storage = storage;
212
+ }
213
+ async getOrCreateHolderKey(alg) {
214
+ const existing = await this.storage.getHolderKey();
215
+ if (existing) return existing;
216
+ const created = await createHolderKeyRecord(alg);
217
+ await this.storage.setHolderKey(created);
218
+ return created;
219
+ }
220
+ async importHolderKey(input) {
221
+ if (await this.storage.getHolderKey()) throw new WalletError("Holder key already exists in this wallet");
222
+ const record = {
223
+ id: await getJwkThumbprint(input.publicJwk),
224
+ algorithm: input.algorithm,
225
+ publicJwk: input.publicJwk,
226
+ privateJwk: input.privateJwk,
227
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
228
+ };
229
+ HolderKeyRecordSchema.parse(record);
230
+ await this.storage.setHolderKey(record);
231
+ return record;
232
+ }
233
+ async listCredentials() {
234
+ return this.storage.listCredentials();
235
+ }
236
+ async getCredentialStatus(credentialId, options) {
237
+ const credential = await this.storage.getCredential(credentialId);
238
+ if (!credential) throw new WalletError(`Credential ${credentialId} not found`);
239
+ if (!credential.status?.status_list) return null;
240
+ const doFetch = options?.fetch ?? fetch;
241
+ const statusJwt = await fetchStatusListJwt(credential.status.status_list.uri, doFetch);
242
+ const protectedHeader = decodeProtectedHeader(statusJwt);
243
+ const payload = parseStatusListTokenPayload((await jwtVerify(statusJwt, await resolveIssuerVerificationKey(credential.issuerKeyMaterial ?? await fetchIssuerKeyMaterial(credential.issuer, doFetch), protectedHeader.kid, typeof protectedHeader.alg === "string" ? protectedHeader.alg : void 0), {
244
+ typ: "statuslist+jwt",
245
+ subject: credential.status.status_list.uri
246
+ })).payload);
247
+ const value = readStatusValue(payload.status_list.lst, payload.status_list.bits, credential.status.status_list.idx);
248
+ return {
249
+ credentialId: credential.id,
250
+ statusReference: credential.status.status_list,
251
+ status: {
252
+ value,
253
+ label: labelForTokenStatus(value),
254
+ isValid: value === 0
255
+ },
256
+ statusList: {
257
+ uri: credential.status.status_list.uri,
258
+ bits: payload.status_list.bits,
259
+ iat: payload.iat,
260
+ exp: payload.exp,
261
+ ttl: payload.ttl,
262
+ aggregationUri: payload.status_list.aggregation_uri,
263
+ jwt: statusJwt
264
+ }
265
+ };
266
+ }
267
+ getVpFormatsSupported() {
268
+ return { "dc+sd-jwt": {
269
+ sd_jwt_alg_values: [
270
+ "ES256",
271
+ "ES384",
272
+ "EdDSA"
273
+ ],
274
+ kb_jwt_alg_values: [
275
+ "ES256",
276
+ "ES384",
277
+ "EdDSA"
278
+ ]
279
+ } };
280
+ }
281
+ async importCredential(input) {
282
+ const parsedInput = ImportCredentialInputSchema.parse(input);
283
+ const holderKey = await this.getOrCreateHolderKey();
284
+ const compactSdJwt = normalizeSdJwt(parsedInput.credential);
285
+ const split = splitSdJwt(compactSdJwt);
286
+ if (split.kbJwt) throw new WalletError("Wallet only imports issuer-bound credentials, not presented credentials");
287
+ const header = decodeProtectedHeader(split.jwt);
288
+ if (header.typ !== "dc+sd-jwt") throw new WalletError("Unsupported credential typ");
289
+ let payload;
290
+ if (parsedInput.issuer) {
291
+ const verificationKey = await resolveIssuerVerificationKey(parsedInput.issuer, header.kid, header.alg);
292
+ payload = (await jwtVerify(split.jwt, verificationKey, {
293
+ issuer: parsedInput.issuer.issuer,
294
+ typ: "dc+sd-jwt"
295
+ })).payload;
296
+ } else {
297
+ const jwtPart = split.jwt.split(".")[1];
298
+ if (!jwtPart) throw new WalletError("Invalid credential format");
299
+ const decodedPayload = JSON.parse(Buffer.from(jwtPart, "base64url").toString("utf8"));
300
+ if (typeof decodedPayload.iss !== "string" || decodedPayload.iss.length === 0) throw new WalletError("Credential is missing issuer");
301
+ payload = decodedPayload;
302
+ }
303
+ const vct = readStringClaim(payload.vct, "Credential is missing vct");
304
+ const issuer = readStringClaim(payload.iss, "Credential is missing iss");
305
+ const cnfJwk = readRecordClaim(readRecordClaim(payload.cnf, "Credential is missing cnf").jwk, "Credential cnf is missing jwk");
306
+ if (await getJwkThumbprint(holderKey.publicJwk) !== await getJwkThumbprint(cnfJwk)) throw new WalletError("Credential cnf.jwk does not match the wallet holder key");
307
+ if (typeof payload._sd_alg !== "string") throw new WalletError("Credential is missing _sd_alg");
308
+ const decoded = await decodeSdJwt(compactSdJwt, sdJwtHasher);
309
+ const allClaims = await getClaims(decoded.jwt.payload, decoded.disclosures, sdJwtHasher);
310
+ const credentialRecord = StoredCredentialRecordSchema.parse({
311
+ id: typeof payload.jti === "string" && payload.jti.length > 0 ? payload.jti : await sha256Base64Url(compactSdJwt),
312
+ format: "dc+sd-jwt",
313
+ compactSdJwt,
314
+ issuer,
315
+ vct,
316
+ holderKeyId: holderKey.id,
317
+ claims: stripReservedClaims(allClaims),
318
+ status: parseCredentialStatus(payload.status),
319
+ issuerKeyMaterial: parsedInput.issuer,
320
+ importedAt: (/* @__PURE__ */ new Date()).toISOString()
321
+ });
322
+ await this.storage.setCredential(credentialRecord);
323
+ return credentialRecord;
324
+ }
325
+ async matchDcqlQuery(input) {
326
+ const inspected = await this.inspectDcqlQuery(input);
327
+ return {
328
+ query: inspected.query,
329
+ credentials: inspected.queries.map((queryMatch) => {
330
+ const selected = queryMatch.credentials[0];
331
+ if (!selected) throw new WalletError(`No credential matched query ${queryMatch.queryId}`);
332
+ return selected;
333
+ })
334
+ };
335
+ }
336
+ async inspectDcqlQuery(input) {
337
+ const request = OpenId4VpRequestSchema.parse(input);
338
+ const query = DcqlQuery.parse(request.dcql_query);
339
+ assertSupportedDcqlQuery(query);
340
+ const credentials = await this.storage.listCredentials();
341
+ const result = runDcqlQuery(query, {
342
+ credentials: credentials.map((credential) => ({
343
+ credential_format: "dc+sd-jwt",
344
+ vct: credential.vct,
345
+ claims: credential.claims,
346
+ cryptographic_holder_binding: true
347
+ })),
348
+ presentation: false
349
+ });
350
+ if (!result.can_be_satisfied) throw new WalletError("No stored credential satisfies the supported DCQL query");
351
+ const queryMatches = [];
352
+ for (const credentialQuery of query.credentials) {
353
+ const queryId = credentialQuery.id;
354
+ const credentialMatch = result.credential_matches[queryId];
355
+ if (!credentialMatch?.success || credentialMatch.valid_credentials.length === 0) throw new WalletError(`No credential matched query ${queryId}`);
356
+ const claimPaths = (credentialQuery.claims ?? []).map((claim) => [...claim.path]);
357
+ queryMatches.push({
358
+ queryId,
359
+ credentials: credentialMatch.valid_credentials.map((candidate) => {
360
+ const storedCredential = credentials[candidate.input_credential_index];
361
+ if (!storedCredential) throw new WalletError(`Matched credential for query ${queryId} was not found`);
362
+ return {
363
+ queryId,
364
+ credentialId: storedCredential.id,
365
+ issuer: storedCredential.issuer,
366
+ vct: storedCredential.vct,
367
+ claims: storedCredential.claims,
368
+ claimPaths
369
+ };
370
+ })
371
+ });
372
+ }
373
+ return {
374
+ query,
375
+ queries: queryMatches
376
+ };
377
+ }
378
+ async createPresentation(input, options) {
379
+ const request = OpenId4VpRequestSchema.parse(input);
380
+ const holderKey = await this.getOrCreateHolderKey();
381
+ const inspected = await this.inspectDcqlQuery(request);
382
+ const matchedCredentials = inspected.queries.map((queryMatch) => {
383
+ const selectedCredentialId = options?.selectedCredentials?.[queryMatch.queryId];
384
+ if (selectedCredentialId) {
385
+ const selected = queryMatch.credentials.find((candidate) => candidate.credentialId === selectedCredentialId);
386
+ if (!selected) throw new WalletError(`Credential ${selectedCredentialId} does not satisfy query ${queryMatch.queryId}`);
387
+ return selected;
388
+ }
389
+ const first = queryMatch.credentials[0];
390
+ if (!first) throw new WalletError(`No credential matched query ${queryMatch.queryId}`);
391
+ return first;
392
+ });
393
+ const presentations = {};
394
+ for (const matched of matchedCredentials) {
395
+ const storedCredential = await this.storage.getCredential(matched.credentialId);
396
+ if (!storedCredential) throw new WalletError(`Stored credential ${matched.credentialId} not found`);
397
+ const frame = matched.claimPaths.length === 0 ? void 0 : claimPathsToPresentationFrame(matched.claimPaths);
398
+ const sdJwtPresentation = await present(normalizeSdJwt(storedCredential.compactSdJwt), frame ?? {}, sdJwtHasher);
399
+ const kbJwt = await createKbJwt({
400
+ holderPrivateJwk: holderKey.privateJwk,
401
+ aud: request.client_id,
402
+ nonce: request.nonce,
403
+ sdJwtPresentation
404
+ });
405
+ presentations[matched.queryId] = [`${stripTrailingEmptyPart(sdJwtPresentation)}~${kbJwt}`];
406
+ }
407
+ return {
408
+ query: inspected.query,
409
+ matchedCredentials,
410
+ dcqlPresentation: presentations,
411
+ vpToken: DcqlPresentation.encode(presentations)
412
+ };
413
+ }
414
+ };
415
+ function normalizeSdJwt(compact) {
416
+ return compact.endsWith("~") ? compact : `${compact}~`;
417
+ }
418
+ function stripTrailingEmptyPart(compact) {
419
+ return compact.endsWith("~") ? compact.slice(0, -1) : compact;
420
+ }
421
+ async function fetchStatusListJwt(uri, doFetch) {
422
+ const response = await doFetch(uri, { headers: { accept: "application/statuslist+jwt, application/jwt, text/plain" } });
423
+ if (!response.ok) throw new WalletError(`Status list fetch failed with status ${response.status}`);
424
+ const payload = (await response.text()).trim();
425
+ if (!payload) throw new WalletError("Status list response is empty");
426
+ return payload;
427
+ }
428
+ async function fetchIssuerKeyMaterial(issuer, doFetch) {
429
+ const response = await doFetch(getCredentialIssuerMetadataUrl$1(issuer), { headers: { accept: "application/json" } });
430
+ if (!response.ok) throw new WalletError(`Issuer metadata fetch failed with status ${response.status}`);
431
+ let payload;
432
+ try {
433
+ payload = await response.json();
434
+ } catch {
435
+ throw new WalletError("Failed to parse issuer metadata response");
436
+ }
437
+ return {
438
+ issuer,
439
+ jwks: readRecordClaim(readRecordClaim(payload, "Issuer metadata must be a JSON object").jwks, "Issuer metadata is missing jwks")
440
+ };
441
+ }
442
+ function getCredentialIssuerMetadataUrl$1(credentialIssuer) {
443
+ const issuerUrl = new URL(credentialIssuer);
444
+ const issuerPath = issuerUrl.pathname === "/" ? "" : issuerUrl.pathname;
445
+ return new URL(`/.well-known/openid-credential-issuer${issuerPath}`, issuerUrl.origin).toString();
446
+ }
447
+ function parseCredentialStatus(value) {
448
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
449
+ const statusList = value.status_list;
450
+ if (!statusList || typeof statusList !== "object" || Array.isArray(statusList)) return;
451
+ const parsedStatusList = statusList;
452
+ if (typeof parsedStatusList.idx !== "number" || !Number.isInteger(parsedStatusList.idx) || parsedStatusList.idx < 0 || typeof parsedStatusList.uri !== "string" || parsedStatusList.uri.length === 0) return;
453
+ return { status_list: {
454
+ idx: parsedStatusList.idx,
455
+ uri: parsedStatusList.uri
456
+ } };
457
+ }
458
+ function parseStatusListTokenPayload(payload) {
459
+ const sub = readStringClaim(payload.sub, "Status list token is missing sub");
460
+ const iat = readIntegerClaim(payload.iat, "Status list token is missing iat");
461
+ const exp = payload.exp === void 0 ? void 0 : readIntegerClaim(payload.exp, "Status list token exp must be an integer");
462
+ const ttl = payload.ttl === void 0 ? void 0 : readPositiveIntegerClaim(payload.ttl, "Status list token ttl must be a positive integer");
463
+ const statusList = readRecordClaim(payload.status_list, "Status list token is missing status_list");
464
+ return {
465
+ sub,
466
+ iat,
467
+ exp,
468
+ ttl,
469
+ status_list: {
470
+ bits: readStatusListBits(statusList.bits),
471
+ lst: readStringClaim(statusList.lst, "Status list token is missing lst"),
472
+ aggregation_uri: statusList.aggregation_uri === void 0 ? void 0 : readStringClaim(statusList.aggregation_uri, "Status list aggregation_uri must be a string")
473
+ }
474
+ };
475
+ }
476
+ function readStatusValue(lst, bits, idx) {
477
+ let bytes;
478
+ try {
479
+ bytes = inflateSync(Buffer.from(lst, "base64url"));
480
+ } catch {
481
+ throw new WalletError("Failed to decode status list payload");
482
+ }
483
+ const bitOffset = idx * bits;
484
+ const byteIndex = Math.floor(bitOffset / 8);
485
+ const intraByteOffset = bitOffset % 8;
486
+ const selectedByte = bytes[byteIndex];
487
+ if (selectedByte === void 0) throw new WalletError(`Status list index ${idx} is out of bounds`);
488
+ return selectedByte >> intraByteOffset & (1 << bits) - 1;
489
+ }
490
+ function readStatusListBits(value) {
491
+ if (value === 1 || value === 2 || value === 4 || value === 8) return value;
492
+ throw new WalletError("Status list bits must be one of 1, 2, 4, or 8");
493
+ }
494
+ function readIntegerClaim(value, message) {
495
+ if (typeof value !== "number" || !Number.isInteger(value)) throw new WalletError(message);
496
+ return value;
497
+ }
498
+ function readPositiveIntegerClaim(value, message) {
499
+ const parsed = readIntegerClaim(value, message);
500
+ if (parsed <= 0) throw new WalletError(message);
501
+ return parsed;
502
+ }
503
+ function labelForTokenStatus(value) {
504
+ if (value === 0) return "VALID";
505
+ if (value === 1) return "INVALID";
506
+ if (value === 2) return "SUSPENDED";
507
+ if (value === 3 || value >= 12 && value <= 15) return "APPLICATION_SPECIFIC";
508
+ return "UNASSIGNED";
509
+ }
510
+ async function resolveIssuerVerificationKey(issuer, kid, alg) {
511
+ if ("jwk" in issuer) return importPublicKey(issuer.jwk, alg ?? "ES256");
512
+ const selected = kid ? issuer.jwks.keys.find((candidate) => candidate.kid === kid) : issuer.jwks.keys.length === 1 ? issuer.jwks.keys[0] : void 0;
513
+ if (!selected) throw new WalletError("Unable to resolve issuer verification key from supplied metadata");
514
+ return importPublicKey(selected, alg ?? "ES256");
515
+ }
516
+ function assertSupportedDcqlQuery(query) {
517
+ for (const credentialQuery of query.credentials) {
518
+ if (credentialQuery.format !== "dc+sd-jwt") throw new WalletError(`Unsupported credential format: ${credentialQuery.format}`);
519
+ if (credentialQuery.multiple) throw new WalletError("multiple=true is unsupported in the demo wallet");
520
+ if (credentialQuery.claim_sets) throw new WalletError("claim_sets are unsupported in the demo wallet");
521
+ if (credentialQuery.trusted_authorities) throw new WalletError("trusted_authorities are unsupported in the demo wallet");
522
+ if (credentialQuery.meta && Object.keys(credentialQuery.meta).some((key) => key !== "vct_values")) throw new WalletError("Only meta.vct_values is supported for dc+sd-jwt queries");
523
+ for (const claim of credentialQuery.claims ?? []) {
524
+ if (claim.values) throw new WalletError("Claim value filters are unsupported in the demo wallet");
525
+ for (const segment of claim.path ?? []) if (typeof segment !== "string") throw new WalletError("Only string claim path segments are supported in the demo wallet");
526
+ }
527
+ }
528
+ }
529
+ function stripReservedClaims(claims) {
530
+ return Object.fromEntries(Object.entries(claims).filter(([key]) => !RESERVED_TOP_LEVEL_CLAIMS.has(key)));
531
+ }
532
+ function readStringClaim(value, message) {
533
+ if (typeof value !== "string" || value.length === 0) throw new WalletError(message);
534
+ return value;
535
+ }
536
+ function readRecordClaim(value, message) {
537
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new WalletError(message);
538
+ return value;
539
+ }
540
+ function claimPathsToPresentationFrame(paths) {
541
+ const frame = {};
542
+ for (const path of paths) {
543
+ let cursor = frame;
544
+ for (const [index, segment] of path.entries()) {
545
+ if (typeof segment !== "string") throw new WalletError("Only string claim path segments are supported in presentation frames");
546
+ if (index === path.length - 1) {
547
+ cursor[segment] = true;
548
+ continue;
549
+ }
550
+ const next = cursor[segment];
551
+ if (!next || typeof next !== "object" || Array.isArray(next)) cursor[segment] = {};
552
+ cursor = cursor[segment];
553
+ }
554
+ }
555
+ return frame;
556
+ }
557
+ //#endregion
558
+ //#region src/openid4vci.ts
559
+ const PRE_AUTHORIZED_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
560
+ const OPENID_CREDENTIAL_OFFER_WELL_KNOWN = "/.well-known/openid-credential-issuer";
561
+ const jwkSchema = z.object({
562
+ kty: z.string().min(1),
563
+ kid: z.string().min(1).optional(),
564
+ alg: z.string().min(1).optional(),
565
+ crv: z.string().min(1).optional(),
566
+ x: z.string().min(1).optional(),
567
+ y: z.string().min(1).optional(),
568
+ d: z.string().min(1).optional(),
569
+ n: z.string().min(1).optional(),
570
+ e: z.string().min(1).optional(),
571
+ x5c: z.array(z.string().min(1)).optional()
572
+ }).catchall(z.unknown());
573
+ const credentialOfferSchema = z.object({
574
+ credential_issuer: z.string().url(),
575
+ credential_configuration_ids: z.array(z.string().min(1)).length(1),
576
+ grants: z.object({ [PRE_AUTHORIZED_GRANT_TYPE]: z.object({
577
+ "pre-authorized_code": z.string().min(1),
578
+ tx_code: z.never().optional()
579
+ }) })
580
+ });
581
+ const credentialOfferReferenceSchema = z.object({ credential_offer_uri: z.string().url() });
582
+ const issuerMetadataSchema = z.object({
583
+ credential_issuer: z.string().url(),
584
+ token_endpoint: z.string().url(),
585
+ credential_endpoint: z.string().url(),
586
+ nonce_endpoint: z.string().url().optional(),
587
+ jwks: z.object({ keys: z.array(jwkSchema).min(1) }),
588
+ credential_configurations_supported: z.record(z.string(), z.object({
589
+ format: z.literal("dc+sd-jwt"),
590
+ vct: z.string().min(1),
591
+ scope: z.string().min(1),
592
+ proof_types_supported: z.object({ jwt: z.object({ proof_signing_alg_values_supported: z.array(z.string().min(1)).min(1) }) }),
593
+ credential_signing_alg_values_supported: z.array(z.string().min(1)).min(1)
594
+ }))
595
+ });
596
+ const tokenResponseSchema = z.object({
597
+ access_token: z.string().min(1),
598
+ token_type: z.literal("Bearer"),
599
+ expires_in: z.number().int().positive(),
600
+ credential_configuration_id: z.string().min(1),
601
+ c_nonce: z.string().min(1).optional(),
602
+ c_nonce_expires_in: z.number().int().positive().optional()
603
+ });
604
+ const nonceResponseSchema = z.object({
605
+ c_nonce: z.string().min(1),
606
+ c_nonce_expires_in: z.number().int().positive()
607
+ });
608
+ const credentialResponseSchema = z.object({
609
+ format: z.literal("dc+sd-jwt"),
610
+ credential: z.string().min(1),
611
+ c_nonce: z.string().min(1).optional(),
612
+ c_nonce_expires_in: z.number().int().positive().optional()
613
+ });
614
+ function parseCredentialOffer(input) {
615
+ if (typeof input === "string") {
616
+ const trimmed = input.trim();
617
+ if (trimmed.startsWith("openid-credential-offer://")) return parseCredentialOfferUri(trimmed);
618
+ try {
619
+ return credentialOfferSchema.parse(JSON.parse(trimmed));
620
+ } catch (error) {
621
+ if (error instanceof z.ZodError) throw new WalletError("Unsupported credential offer input");
622
+ throw new WalletError("Credential offer JSON must be valid JSON");
623
+ }
624
+ }
625
+ try {
626
+ return credentialOfferSchema.parse(input);
627
+ } catch {
628
+ throw new WalletError("Unsupported credential offer input");
629
+ }
630
+ }
631
+ async function fetchIssuerMetadata(credentialIssuer, options) {
632
+ const json = await parseJsonResponse(await (options?.fetch ?? fetch)(getCredentialIssuerMetadataUrl(credentialIssuer), { headers: { accept: "application/json" } }), "issuer metadata");
633
+ const metadata = issuerMetadataSchema.parse(json);
634
+ if (metadata.credential_issuer !== credentialIssuer) throw new WalletError("Issuer metadata credential_issuer mismatch");
635
+ return metadata;
636
+ }
637
+ async function receiveCredentialFromOffer(wallet, offerInput, options) {
638
+ const doFetch = options?.fetch ?? fetch;
639
+ const offer = await resolveCredentialOffer(offerInput, doFetch);
640
+ const metadata = await fetchIssuerMetadata(offer.credential_issuer, { fetch: doFetch });
641
+ const credentialConfigurationId = offer.credential_configuration_ids[0];
642
+ if (!credentialConfigurationId) throw new WalletError("Credential offer must contain one credential configuration");
643
+ const configuration = metadata.credential_configurations_supported[credentialConfigurationId];
644
+ if (!configuration) throw new WalletError(`Issuer metadata does not support ${credentialConfigurationId}`);
645
+ if (!configuration.proof_types_supported.jwt) throw new WalletError("Issuer does not support jwt proofs");
646
+ const tokenResponse = await exchangePreAuthorizedCode(metadata.token_endpoint, offer.grants[PRE_AUTHORIZED_GRANT_TYPE]["pre-authorized_code"], doFetch);
647
+ const nonce = tokenResponse.c_nonce ?? (await fetchNonce(metadata.nonce_endpoint, doFetch)).c_nonce;
648
+ const holderKey = await wallet.getOrCreateHolderKey();
649
+ const proofJwt = await createOpenId4VciProofJwt({
650
+ holderPrivateJwk: holderKey.privateJwk,
651
+ holderPublicJwk: holderKey.publicJwk,
652
+ aud: metadata.credential_issuer,
653
+ nonce,
654
+ alg: holderKey.algorithm
655
+ });
656
+ if (!configuration.proof_types_supported.jwt.proof_signing_alg_values_supported.includes(holderKey.algorithm)) throw new WalletError(`Issuer does not support holder proof algorithm ${holderKey.algorithm}`);
657
+ const credentialResponse = await requestCredential(metadata.credential_endpoint, tokenResponse.access_token, {
658
+ format: "dc+sd-jwt",
659
+ credential_configuration_id: credentialConfigurationId,
660
+ proofs: { jwt: [{
661
+ proof_type: "jwt",
662
+ jwt: proofJwt
663
+ }] }
664
+ }, doFetch);
665
+ return wallet.importCredential({
666
+ credential: credentialResponse.credential,
667
+ issuer: {
668
+ issuer: metadata.credential_issuer,
669
+ jwks: metadata.jwks
670
+ }
671
+ });
672
+ }
673
+ async function resolveCredentialOffer(input, doFetch) {
674
+ if (typeof input === "string") {
675
+ const trimmed = input.trim();
676
+ if (trimmed.startsWith("openid-credential-offer://")) {
677
+ const parsed = parseCredentialOfferUriOrReference(trimmed);
678
+ if ("credential_offer_uri" in parsed) return fetchCredentialOffer(parsed.credential_offer_uri, doFetch);
679
+ return parsed;
680
+ }
681
+ }
682
+ return parseCredentialOffer(input);
683
+ }
684
+ function parseCredentialOfferUri(input) {
685
+ const parsed = parseCredentialOfferUriOrReference(input);
686
+ if ("credential_offer_uri" in parsed) throw new WalletError("Credential offer URI references a remote offer; fetch it via receiveCredentialFromOffer");
687
+ return parsed;
688
+ }
689
+ function parseCredentialOfferUriOrReference(input) {
690
+ let url;
691
+ try {
692
+ url = new URL(input);
693
+ } catch {
694
+ throw new WalletError("Invalid credential offer URI");
695
+ }
696
+ if (url.protocol !== "openid-credential-offer:") throw new WalletError("Credential offer URI must use the openid-credential-offer:// scheme");
697
+ const offerUri = getSingleSearchParam$1(url, "credential_offer_uri");
698
+ if (offerUri) try {
699
+ return credentialOfferReferenceSchema.parse({ credential_offer_uri: offerUri });
700
+ } catch {
701
+ throw new WalletError("Credential offer URI contains an invalid credential_offer_uri");
702
+ }
703
+ const encodedOffer = getSingleSearchParam$1(url, "credential_offer");
704
+ if (!encodedOffer) throw new WalletError("Credential offer URI is missing credential_offer");
705
+ try {
706
+ return credentialOfferSchema.parse(JSON.parse(encodedOffer));
707
+ } catch {
708
+ throw new WalletError("Credential offer URI contains invalid credential_offer JSON");
709
+ }
710
+ }
711
+ async function fetchCredentialOffer(endpoint, doFetch) {
712
+ const json = await parseJsonResponse(await doFetch(endpoint, { headers: { accept: "application/json" } }), "credential offer");
713
+ return credentialOfferSchema.parse(json);
714
+ }
715
+ function getCredentialIssuerMetadataUrl(credentialIssuer) {
716
+ const issuerUrl = new URL(credentialIssuer);
717
+ const issuerPath = issuerUrl.pathname === "/" ? "" : issuerUrl.pathname;
718
+ return new URL(`${OPENID_CREDENTIAL_OFFER_WELL_KNOWN}${issuerPath}`, issuerUrl.origin).toString();
719
+ }
720
+ async function exchangePreAuthorizedCode(endpoint, preAuthorizedCode, doFetch) {
721
+ const response = await doFetch(endpoint, {
722
+ method: "POST",
723
+ headers: {
724
+ accept: "application/json",
725
+ "content-type": "application/x-www-form-urlencoded"
726
+ },
727
+ body: new URLSearchParams({
728
+ grant_type: PRE_AUTHORIZED_GRANT_TYPE,
729
+ "pre-authorized_code": preAuthorizedCode
730
+ })
731
+ });
732
+ return tokenResponseSchema.parse(await parseJsonResponse(response, "token exchange"));
733
+ }
734
+ async function fetchNonce(endpoint, doFetch) {
735
+ if (!endpoint) throw new WalletError("Issuer metadata is missing nonce_endpoint");
736
+ const response = await doFetch(endpoint, {
737
+ method: "POST",
738
+ headers: { accept: "application/json" }
739
+ });
740
+ return nonceResponseSchema.parse(await parseJsonResponse(response, "nonce"));
741
+ }
742
+ async function requestCredential(endpoint, accessToken, body, doFetch) {
743
+ const response = await doFetch(endpoint, {
744
+ method: "POST",
745
+ headers: {
746
+ accept: "application/json",
747
+ authorization: `Bearer ${accessToken}`,
748
+ "content-type": "application/json"
749
+ },
750
+ body: JSON.stringify(body)
751
+ });
752
+ return credentialResponseSchema.parse(await parseJsonResponse(response, "credential request"));
753
+ }
754
+ async function parseJsonResponse(response, label) {
755
+ let payload;
756
+ try {
757
+ payload = await response.json();
758
+ } catch {
759
+ throw new WalletError(`Failed to parse ${label} response`);
760
+ }
761
+ if (!response.ok) {
762
+ const message = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.error_description ?? payload.error : void 0;
763
+ throw new WalletError(`${label} failed with status ${response.status}${typeof message === "string" ? `: ${message}` : ""}`);
764
+ }
765
+ return payload;
766
+ }
767
+ function getSingleSearchParam$1(url, key) {
768
+ const values = url.searchParams.getAll(key);
769
+ if (values.length === 0) return;
770
+ if (values.length > 1) throw new WalletError(`Credential offer URI must include only one ${key}`);
771
+ return values[0] || void 0;
772
+ }
773
+ //#endregion
774
+ //#region src/openid4vp.ts
775
+ const openid4vpAuthorizationUrlSchema = z.string().min(1);
776
+ const requestObjectHeaderSchema = z.object({ typ: z.literal("oauth-authz-req+jwt") });
777
+ const requestObjectClaimsSchema = z.object({
778
+ client_id: z.string().min(1).optional(),
779
+ nonce: z.string().min(1).optional(),
780
+ state: z.string().min(1).optional(),
781
+ response_type: z.literal("vp_token").optional(),
782
+ response_mode: z.string().min(1).optional(),
783
+ response_uri: z.string().min(1).optional(),
784
+ client_metadata: z.unknown().optional(),
785
+ dcql_query: z.unknown().optional(),
786
+ scope: z.unknown().optional(),
787
+ presentation_definition: z.unknown().optional()
788
+ }).passthrough();
789
+ const openId4VpRawRequestSchema = z.object({
790
+ client_id: z.string().min(1).optional(),
791
+ nonce: z.string().min(1).optional(),
792
+ state: z.string().min(1).optional(),
793
+ response_type: z.literal("vp_token").optional(),
794
+ response_mode: z.string().optional(),
795
+ response_uri: z.string().min(1).optional(),
796
+ client_metadata: z.unknown().optional(),
797
+ dcql_query: z.unknown().optional(),
798
+ request: z.string().min(1).optional(),
799
+ request_uri: z.string().min(1).optional(),
800
+ request_uri_method: z.string().optional(),
801
+ scope: z.unknown().optional(),
802
+ presentation_definition: z.unknown().optional()
803
+ }).passthrough().superRefine((value, ctx) => {
804
+ if (value.request && value.request_uri) ctx.addIssue({
805
+ code: z.ZodIssueCode.custom,
806
+ message: "Use only one of request or request_uri",
807
+ path: ["request"]
808
+ });
809
+ if (value.request_uri_method && !value.request_uri) ctx.addIssue({
810
+ code: z.ZodIssueCode.custom,
811
+ message: "request_uri_method requires request_uri",
812
+ path: ["request_uri_method"]
813
+ });
814
+ if ((value.request || value.request_uri) && value.dcql_query !== void 0) ctx.addIssue({
815
+ code: z.ZodIssueCode.custom,
816
+ message: "Inline dcql_query cannot be combined with request or request_uri",
817
+ path: ["dcql_query"]
818
+ });
819
+ });
820
+ async function parseOpenid4VpAuthorizationUrl(input) {
821
+ const value = openid4vpAuthorizationUrlSchema.parse(input).trim();
822
+ let url;
823
+ try {
824
+ url = new URL(value);
825
+ } catch {
826
+ throw new WalletError("Invalid openid4vp authorization URL");
827
+ }
828
+ if (url.protocol !== "openid4vp:") throw new WalletError("Authorization URL must use the openid4vp:// scheme");
829
+ return resolveOpenId4VpRequest({
830
+ client_id: getSingleSearchParam(url, "client_id"),
831
+ nonce: getOptionalSingleSearchParam(url, "nonce"),
832
+ state: getOptionalSingleSearchParam(url, "state"),
833
+ response_type: getOptionalSingleSearchParam(url, "response_type"),
834
+ response_mode: getOptionalSingleSearchParam(url, "response_mode"),
835
+ response_uri: getOptionalSingleSearchParam(url, "response_uri"),
836
+ client_metadata: parseJsonParam(getOptionalSingleSearchParam(url, "client_metadata"), "client_metadata"),
837
+ dcql_query: parseDcqlQueryParam(getOptionalSingleSearchParam(url, "dcql_query")),
838
+ request: getOptionalSingleSearchParam(url, "request"),
839
+ request_uri: getOptionalSingleSearchParam(url, "request_uri"),
840
+ request_uri_method: getOptionalSingleSearchParam(url, "request_uri_method"),
841
+ scope: getOptionalSingleSearchParam(url, "scope"),
842
+ presentation_definition: getOptionalSingleSearchParam(url, "presentation_definition")
843
+ });
844
+ }
845
+ async function resolveOpenId4VpRequest(input) {
846
+ const request = openId4VpRawRequestSchema.parse(input);
847
+ const requestUriMethod = request.request_uri_method;
848
+ if (requestUriMethod !== void 0 && requestUriMethod !== "get" && requestUriMethod !== "post") throw new WalletError("invalid_request_uri_method");
849
+ const requestObject = request.request_uri ? await fetchRequestObject(request.request_uri, request.client_id) : request.request ? parseRequestObject(request.request) : void 0;
850
+ return OpenId4VpRequestSchema.parse({
851
+ client_id: request.client_id ?? requestObject?.client_id,
852
+ nonce: request.nonce ?? requestObject?.nonce,
853
+ state: request.state ?? requestObject?.state,
854
+ response_type: request.response_type ?? requestObject?.response_type,
855
+ response_mode: request.response_mode ?? requestObject?.response_mode,
856
+ response_uri: request.response_uri ?? requestObject?.response_uri,
857
+ client_metadata: parseClientMetadata(request.client_metadata ?? requestObject?.client_metadata),
858
+ dcql_query: request.dcql_query ?? requestObject?.dcql_query,
859
+ scope: request.scope ?? requestObject?.scope,
860
+ presentation_definition: request.presentation_definition ?? requestObject?.presentation_definition
861
+ });
862
+ }
863
+ function createOpenId4VpAuthorizationResponse(request, presentation) {
864
+ const parsedRequest = OpenId4VpRequestSchema.parse(request);
865
+ return {
866
+ vp_token: presentation.vpToken,
867
+ state: parsedRequest.state
868
+ };
869
+ }
870
+ async function submitOpenId4VpAuthorizationResponse(request, response) {
871
+ const parsedRequest = OpenId4VpRequestSchema.parse(request);
872
+ if (!parsedRequest.response_mode) throw new WalletError("response_mode is required for submission");
873
+ const responseUrl = parseHttpsUrl(parsedRequest.response_uri, "response_uri must use https");
874
+ const body = parsedRequest.response_mode === "direct_post" ? createDirectPostBody(response) : await createDirectPostJwtBody(parsedRequest, response);
875
+ let fetchResponse;
876
+ try {
877
+ fetchResponse = await fetch(responseUrl, {
878
+ method: "POST",
879
+ headers: { "content-type": "application/x-www-form-urlencoded" },
880
+ body
881
+ });
882
+ } catch {
883
+ throw new WalletError("Failed to submit authorization response");
884
+ }
885
+ const parsedBody = await parseSubmissionBody(fetchResponse);
886
+ const redirectUri = readRedirectUri(parsedBody);
887
+ return {
888
+ responseMode: parsedRequest.response_mode,
889
+ responseUri: responseUrl.toString(),
890
+ status: fetchResponse.status,
891
+ body: parsedBody,
892
+ redirectUri
893
+ };
894
+ }
895
+ async function fetchRequestObject(requestUri, clientId) {
896
+ let url;
897
+ try {
898
+ url = new URL(requestUri);
899
+ } catch {
900
+ throw new WalletError("request_uri must be a valid URL");
901
+ }
902
+ if (url.protocol !== "https:") throw new WalletError("request_uri must use https");
903
+ assertRequestUriMatchesClientId(url, clientId);
904
+ let response;
905
+ try {
906
+ response = await fetch(url, { headers: { accept: "application/oauth-authz-req+jwt" } });
907
+ } catch {
908
+ throw new WalletError("Failed to fetch request_uri");
909
+ }
910
+ if (!response.ok) throw new WalletError(`request_uri fetch failed with status ${response.status}`);
911
+ if (!(response.headers.get("content-type")?.toLowerCase())?.startsWith("application/oauth-authz-req+jwt")) throw new WalletError("request_uri response must use content-type application/oauth-authz-req+jwt");
912
+ return parseRequestObject(await response.text());
913
+ }
914
+ function parseRequestObject(compactJwt) {
915
+ const value = z.string().min(1).parse(compactJwt).trim();
916
+ if (value.split(".").length !== 3) throw new WalletError("Only compact JWS request objects are supported in the demo wallet");
917
+ try {
918
+ requestObjectHeaderSchema.parse(decodeProtectedHeader(value));
919
+ return requestObjectClaimsSchema.parse(decodeJwt(value));
920
+ } catch {
921
+ throw new WalletError("Request object must be a valid JWT with typ=oauth-authz-req+jwt");
922
+ }
923
+ }
924
+ function parseDcqlQueryParam(value) {
925
+ if (value === void 0) return;
926
+ try {
927
+ return JSON.parse(value);
928
+ } catch {
929
+ throw new WalletError("dcql_query must be valid JSON in the openid4vp:// URL");
930
+ }
931
+ }
932
+ function parseJsonParam(value, label) {
933
+ if (value === void 0) return;
934
+ try {
935
+ return JSON.parse(value);
936
+ } catch {
937
+ throw new WalletError(`${label} must be valid JSON in the openid4vp:// URL`);
938
+ }
939
+ }
940
+ function parseClientMetadata(value) {
941
+ if (value === void 0) return;
942
+ return z.object({
943
+ jwks: z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) }).optional(),
944
+ encrypted_response_enc_values_supported: z.array(z.string().min(1)).min(1).optional(),
945
+ vp_formats_supported: z.unknown().optional()
946
+ }).passthrough().parse(value);
947
+ }
948
+ function parseHttpsUrl(value, errorMessage) {
949
+ if (!value) throw new WalletError(errorMessage);
950
+ let url;
951
+ try {
952
+ url = new URL(value);
953
+ } catch {
954
+ throw new WalletError(errorMessage);
955
+ }
956
+ if (url.protocol !== "https:") throw new WalletError(errorMessage);
957
+ return url;
958
+ }
959
+ function createDirectPostBody(response) {
960
+ const body = new URLSearchParams({ vp_token: response.vp_token });
961
+ if (response.state) body.set("state", response.state);
962
+ return body;
963
+ }
964
+ async function createDirectPostJwtBody(request, response) {
965
+ const jwt = await encryptAuthorizationResponse(request, response);
966
+ return new URLSearchParams({ response: jwt });
967
+ }
968
+ async function encryptAuthorizationResponse(request, response) {
969
+ const parsedRequest = OpenId4VpRequestSchema.parse(request);
970
+ const jwk = parsedRequest.client_metadata?.jwks?.keys[0];
971
+ if (!jwk) throw new WalletError("direct_post.jwt requires client_metadata.jwks with one encryption key");
972
+ const alg = resolveJweAlg(jwk);
973
+ const enc = parsedRequest.client_metadata?.encrypted_response_enc_values_supported?.[0] ?? "A128GCM";
974
+ return new EncryptJWT(response).setProtectedHeader({
975
+ alg,
976
+ enc,
977
+ typ: "oauth-authz-resp+jwt"
978
+ }).setAudience(parsedRequest.client_id).setIssuedAt().encrypt(await importEncryptionKey(jwk, alg));
979
+ }
980
+ async function importEncryptionKey(jwk, alg) {
981
+ return importJWK(jwk, alg);
982
+ }
983
+ function resolveJweAlg(jwk) {
984
+ if (typeof jwk.alg === "string" && jwk.alg.length > 0) return jwk.alg;
985
+ if (jwk.kty === "RSA") return "RSA-OAEP-256";
986
+ if (jwk.kty === "EC" || jwk.kty === "OKP") return "ECDH-ES";
987
+ throw new WalletError("Unsupported verifier encryption key");
988
+ }
989
+ async function parseSubmissionBody(response) {
990
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
991
+ const body = await response.text();
992
+ if (body.length === 0) return;
993
+ if (contentType.startsWith("application/json")) return JSON.parse(body);
994
+ return body;
995
+ }
996
+ function readRedirectUri(body) {
997
+ if (!body || typeof body !== "object" || Array.isArray(body)) return;
998
+ const value = body.redirect_uri;
999
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1000
+ }
1001
+ function assertRequestUriMatchesClientId(url, clientId) {
1002
+ const clientIdHostname = getClientIdHostname(clientId);
1003
+ if (!clientIdHostname) return;
1004
+ if (url.hostname !== clientIdHostname) throw new WalletError("request_uri hostname must match client_id hostname");
1005
+ }
1006
+ function getClientIdHostname(clientId) {
1007
+ if (!clientId) return;
1008
+ try {
1009
+ return new URL(clientId).hostname;
1010
+ } catch {}
1011
+ if (clientId.startsWith("x509_san_dns:")) return clientId.slice(13) || void 0;
1012
+ const separator = clientId.indexOf(":");
1013
+ if (separator > 0) try {
1014
+ return new URL(clientId.slice(separator + 1)).hostname;
1015
+ } catch {
1016
+ return;
1017
+ }
1018
+ }
1019
+ function getSingleSearchParam(url, key) {
1020
+ const values = url.searchParams.getAll(key);
1021
+ if (values.length === 0 || values[0]?.length === 0) throw new WalletError(`Authorization URL is missing ${key}`);
1022
+ if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
1023
+ return values[0];
1024
+ }
1025
+ function getOptionalSingleSearchParam(url, key) {
1026
+ const values = url.searchParams.getAll(key);
1027
+ if (values.length === 0) return;
1028
+ if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
1029
+ return values[0] || void 0;
1030
+ }
1031
+ //#endregion
1032
+ //#region src/storage.ts
1033
+ var InMemoryWalletStorage = class {
1034
+ holderKey = null;
1035
+ credentials = /* @__PURE__ */ new Map();
1036
+ async getHolderKey() {
1037
+ return this.holderKey ? HolderKeyRecordSchema.parse(this.holderKey) : null;
1038
+ }
1039
+ async setHolderKey(record) {
1040
+ this.holderKey = HolderKeyRecordSchema.parse(record);
1041
+ }
1042
+ async listCredentials() {
1043
+ return [...this.credentials.values()].map((record) => StoredCredentialRecordSchema.parse(record));
1044
+ }
1045
+ async getCredential(id) {
1046
+ const record = this.credentials.get(id);
1047
+ return record ? StoredCredentialRecordSchema.parse(record) : null;
1048
+ }
1049
+ async setCredential(record) {
1050
+ const parsed = StoredCredentialRecordSchema.parse(record);
1051
+ this.credentials.set(parsed.id, parsed);
1052
+ }
1053
+ };
1054
+ //#endregion
1055
+ export { CredentialStatusListReferenceSchema, CredentialStatusSchema, HOLDER_KEY_ALG, HolderKeyRecordSchema, ImportCredentialInputSchema, InMemoryWalletStorage, IssuerJwkSchema, IssuerJwksSchema, IssuerKeyMaterialSchema, JwkSchema, OpenId4VpRequestSchema, ResponseModeSchema, SD_JWT_HASH_ALG, StoredCredentialRecordSchema, VerifierClientMetadataSchema, Wallet, WalletConfigSchema, WalletError, createHolderKeyRecord, createKbJwt, createOpenId4VciProofJwt, createOpenId4VpAuthorizationResponse, fetchIssuerMetadata, getJwkThumbprint, importPrivateKey, importPublicKey, issueDemoCredential, parseCredentialOffer, parseOpenid4VpAuthorizationUrl, receiveCredentialFromOffer, resolveOpenId4VpRequest, sdJwtHasher, sha256Base64Url, submitOpenId4VpAuthorizationResponse };