@openeudi/openid4vp 0.3.0 → 0.4.0

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.js CHANGED
@@ -1,9 +1,33 @@
1
- import { splitSdJwt, decodeSdJwt, getClaims } from '@sd-jwt/decode';
2
- import { decodeProtectedHeader, importX509, jwtVerify, importJWK } from 'jose';
3
- import { Encoder, Tag } from 'cbor-x';
4
1
  import { v4 } from 'uuid';
2
+ import { Encoder, Tag } from 'cbor-x';
3
+ import { importX509, decodeProtectedHeader, jwtVerify, importJWK } from 'jose';
4
+ import { splitSdJwt, decodeSdJwt, getClaims } from '@sd-jwt/decode';
5
+ import { matchQuery, buildSubmission } from '@openeudi/dcql';
6
+ export { DcqlMatchError, DcqlValidationError } from '@openeudi/dcql';
5
7
 
6
- // src/parsers/sd-jwt.parser.ts
8
+ // src/authorization.ts
9
+ function createAuthorizationRequest(input, query) {
10
+ if (!input.clientId) throw new TypeError("clientId is required");
11
+ if (!input.nonce) throw new TypeError("nonce is required");
12
+ if (!input.responseUri) throw new TypeError("responseUri is required");
13
+ const state = input.state ?? v4();
14
+ const responseMode = input.responseMode ?? "direct_post";
15
+ const params = new URLSearchParams({
16
+ response_type: "vp_token",
17
+ response_mode: responseMode,
18
+ response_uri: input.responseUri,
19
+ client_id: input.clientId,
20
+ nonce: input.nonce,
21
+ state,
22
+ dcql_query: JSON.stringify(query)
23
+ });
24
+ return {
25
+ uri: `openid4vp://authorize?${params.toString()}`,
26
+ dcqlQuery: query,
27
+ nonce: input.nonce,
28
+ state
29
+ };
30
+ }
7
31
 
8
32
  // src/errors.ts
9
33
  var InvalidSignatureError = class extends Error {
@@ -41,228 +65,131 @@ var NonceValidationError = class extends Error {
41
65
  Object.setPrototypeOf(this, new.target.prototype);
42
66
  }
43
67
  };
68
+ var HaipValidationError = class extends Error {
69
+ code;
70
+ credentialId;
71
+ constructor(code, message, credentialId) {
72
+ super(message);
73
+ this.name = "HaipValidationError";
74
+ this.code = code;
75
+ this.credentialId = credentialId;
76
+ Object.setPrototypeOf(this, new.target.prototype);
77
+ }
78
+ };
44
79
 
45
- // src/parsers/sd-jwt.parser.ts
46
- var DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512"];
47
- var sha256Hasher = async (data, _alg) => {
48
- const encoder2 = new TextEncoder();
49
- const input = typeof data === "string" ? encoder2.encode(data) : data;
50
- const hashBuffer = await crypto.subtle.digest("SHA-256", input);
51
- return new Uint8Array(hashBuffer);
80
+ // src/haip.ts
81
+ var HAIP_DOCTYPE_NAMESPACES = {
82
+ "org.iso.18013.5.1.mDL": "org.iso.18013.5.1",
83
+ "eu.europa.ec.eudi.pid.1": "eu.europa.ec.eudi.pid.1"
52
84
  };
53
- var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
54
- function base64ToBytes(base64) {
55
- const cleaned = base64.replace(/=+$/, "");
56
- const byteLength = Math.floor(cleaned.length * 3 / 4);
57
- const buffer = new ArrayBuffer(byteLength);
58
- const bytes = new Uint8Array(buffer);
59
- let byteIndex = 0;
60
- for (let i = 0; i < cleaned.length; i += 4) {
61
- const a = BASE64_CHARS.indexOf(cleaned[i]);
62
- const b = i + 1 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 1]) : 0;
63
- const c = i + 2 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 2]) : 0;
64
- const d = i + 3 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 3]) : 0;
65
- bytes[byteIndex++] = a << 2 | b >> 4;
66
- if (byteIndex < byteLength) bytes[byteIndex++] = (b & 15) << 4 | c >> 2;
67
- if (byteIndex < byteLength) bytes[byteIndex++] = (c & 3) << 6 | d;
68
- }
69
- return bytes;
70
- }
71
- function bytesEqual(a, b) {
72
- if (a.length !== b.length) return false;
73
- for (let i = 0; i < a.length; i++) {
74
- if (a[i] !== b[i]) return false;
75
- }
76
- return true;
77
- }
78
- function derToPem(base64Der) {
79
- const lines = base64Der.match(/.{1,64}/g) || [];
80
- return `-----BEGIN CERTIFICATE-----
81
- ${lines.join("\n")}
82
- -----END CERTIFICATE-----`;
83
- }
84
- function bytesToBase64url(bytes) {
85
- let binary = "";
86
- for (const byte of bytes) {
87
- binary += String.fromCharCode(byte);
88
- }
89
- return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
90
- }
91
- function mapToCredentialClaims(raw) {
92
- const claims = {};
93
- if (typeof raw["age_over_18"] === "boolean") {
94
- claims.age_over_18 = raw["age_over_18"];
95
- }
96
- if (typeof raw["age_over_21"] === "boolean") {
97
- claims.age_over_21 = raw["age_over_21"];
98
- }
99
- if (typeof raw["resident_country"] === "string") {
100
- claims.resident_country = raw["resident_country"];
101
- }
102
- if (typeof raw["nationality"] === "string") {
103
- claims.nationality = raw["nationality"];
104
- }
105
- if (typeof raw["family_name_birth"] === "string") {
106
- claims.family_name_birth = raw["family_name_birth"];
107
- }
108
- for (const [key, value] of Object.entries(raw)) {
109
- if (!(key in claims) && !key.startsWith("_sd") && key !== "iss" && key !== "exp" && key !== "iat" && key !== "cnf" && key !== "vct") {
110
- claims[key] = value;
85
+ function buildHaipQuery(input) {
86
+ if (input.claims.length === 0) {
87
+ throw new HaipValidationError(
88
+ "NO_CLAIMS",
89
+ `credential '${input.credentialId}' must declare at least one claim`,
90
+ input.credentialId
91
+ );
92
+ }
93
+ let meta;
94
+ let claims;
95
+ if (input.format === "dc+sd-jwt") {
96
+ if (!input.vctValues || input.vctValues.length === 0) {
97
+ throw new HaipValidationError(
98
+ "MISSING_SDJWT_META",
99
+ `credential '${input.credentialId}' with format 'dc+sd-jwt' must provide non-empty vctValues`,
100
+ input.credentialId
101
+ );
111
102
  }
112
- }
113
- return claims;
114
- }
115
- function extractCountryHint(issuer) {
116
- try {
117
- const url = new URL(issuer);
118
- const tld = url.hostname.split(".").pop()?.toUpperCase();
119
- if (tld && tld.length === 2) {
120
- return tld;
103
+ meta = { vct_values: input.vctValues };
104
+ claims = input.claims.map((c) => ({ path: [c] }));
105
+ } else {
106
+ if (!input.doctypeValue) {
107
+ throw new HaipValidationError(
108
+ "MISSING_MDOC_META",
109
+ `credential '${input.credentialId}' with format 'mso_mdoc' must provide doctypeValue`,
110
+ input.credentialId
111
+ );
121
112
  }
122
- } catch {
113
+ meta = { doctype_value: input.doctypeValue };
114
+ const namespace = HAIP_DOCTYPE_NAMESPACES[input.doctypeValue] ?? input.doctypeValue;
115
+ claims = input.claims.map((c) => ({ path: [namespace, c] }));
123
116
  }
124
- return "";
125
- }
126
- var SdJwtParser = class {
127
- format = "sd-jwt-vc";
128
- canParse(vpToken) {
129
- return typeof vpToken === "string" && vpToken.includes("~");
117
+ const credential = {
118
+ id: input.credentialId,
119
+ format: input.format,
120
+ meta,
121
+ claims
122
+ };
123
+ if (input.trustedAuthorities && input.trustedAuthorities.length > 0) {
124
+ credential.trusted_authorities = input.trustedAuthorities;
130
125
  }
131
- async parse(vpToken, options) {
132
- if (typeof vpToken !== "string") {
133
- throw new MalformedCredentialError("VP token must be a string");
134
- }
135
- const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS;
136
- const invalidResult = (error) => ({
137
- valid: false,
138
- format: this.format,
139
- claims: {},
140
- issuer: { certificate: new Uint8Array(), country: "" },
141
- error
142
- });
143
- let parts;
144
- try {
145
- parts = splitSdJwt(vpToken);
146
- } catch {
147
- throw new MalformedCredentialError("Failed to split SD-JWT structure");
148
- }
149
- let header;
150
- try {
151
- header = decodeProtectedHeader(parts.jwt);
152
- } catch {
153
- throw new MalformedCredentialError("Failed to decode SD-JWT issuer JWT header");
154
- }
155
- const alg = header.alg;
156
- if (typeof alg !== "string" || !allowedAlgorithms.includes(alg)) {
157
- return invalidResult(`Unsupported algorithm: ${alg}`);
158
- }
159
- const x5cArray = header.x5c;
160
- if (!Array.isArray(x5cArray) || x5cArray.length === 0 || typeof x5cArray[0] !== "string") {
161
- throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
162
- }
163
- let issuerKey;
164
- let issuerCertBytes;
165
- try {
166
- issuerKey = await importX509(derToPem(x5cArray[0]), alg);
167
- issuerCertBytes = base64ToBytes(x5cArray[0]);
168
- } catch (err) {
169
- if (err instanceof MalformedCredentialError) throw err;
170
- throw new MalformedCredentialError("Failed to import issuer certificate");
171
- }
172
- let payload;
173
- try {
174
- const result = await jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
175
- payload = result.payload;
176
- } catch {
177
- return invalidResult("Issuer JWT signature verification failed");
126
+ return { credentials: [credential] };
127
+ }
128
+ var HAIP_FORMATS = /* @__PURE__ */ new Set(["dc+sd-jwt", "mso_mdoc"]);
129
+ function validateHaipQuery(query) {
130
+ if (!query.credentials || query.credentials.length === 0) {
131
+ throw new HaipValidationError(
132
+ "EMPTY_QUERY",
133
+ "query.credentials must have at least one entry"
134
+ );
135
+ }
136
+ if (query.credential_sets !== void 0) {
137
+ throw new HaipValidationError(
138
+ "CREDENTIAL_SETS_DISALLOWED",
139
+ "credential_sets are not permitted in HAIP-minimal"
140
+ );
141
+ }
142
+ for (const credential of query.credentials) {
143
+ if (!HAIP_FORMATS.has(credential.format)) {
144
+ throw new HaipValidationError(
145
+ "UNSUPPORTED_FORMAT",
146
+ `credential '${credential.id}' uses unsupported format '${credential.format}' (allowed: dc+sd-jwt, mso_mdoc)`,
147
+ credential.id
148
+ );
178
149
  }
179
- const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
180
- const issuerInfo = {
181
- certificate: issuerCertBytes,
182
- country: extractCountryHint(issuerString)
183
- };
184
- if (options.trustedCertificates.length === 0) {
185
- if (options.skipTrustCheck !== true) {
186
- throw new MalformedCredentialError(
187
- "trustedCertificates must not be empty unless skipTrustCheck is true"
150
+ if (credential.format === "dc+sd-jwt") {
151
+ const vctValues = credential.meta?.vct_values;
152
+ if (!vctValues || vctValues.length === 0) {
153
+ throw new HaipValidationError(
154
+ "MISSING_SDJWT_META",
155
+ `credential '${credential.id}' with format 'dc+sd-jwt' must set meta.vct_values`,
156
+ credential.id
188
157
  );
189
158
  }
190
- } else {
191
- const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual(trusted, issuerCertBytes));
192
- if (!isTrusted) {
193
- return invalidResult("Issuer certificate is not trusted");
194
- }
195
159
  }
196
- const sdArray = payload["_sd"];
197
- if (parts.disclosures.length > 0) {
198
- if (!Array.isArray(sdArray) || sdArray.length === 0) {
199
- return invalidResult("Disclosures present but no _sd array in issuer JWT");
200
- }
201
- for (const disclosure of parts.disclosures) {
202
- const hashBytes = await sha256Hasher(disclosure);
203
- const hashB64url = bytesToBase64url(hashBytes);
204
- if (!sdArray.includes(hashB64url)) {
205
- return invalidResult("Disclosure hash mismatch");
206
- }
160
+ if (credential.format === "mso_mdoc") {
161
+ if (typeof credential.meta?.doctype_value !== "string") {
162
+ throw new HaipValidationError(
163
+ "MISSING_MDOC_META",
164
+ `credential '${credential.id}' with format 'mso_mdoc' must set meta.doctype_value`,
165
+ credential.id
166
+ );
207
167
  }
208
168
  }
209
- if (parts.kbJwt) {
210
- const cnf = payload["cnf"];
211
- if (!cnf || typeof cnf !== "object" || !cnf["jwk"]) {
212
- throw new MalformedCredentialError("Missing cnf.jwk in issuer JWT for key binding");
213
- }
214
- let holderKey;
215
- try {
216
- const kbHeader = decodeProtectedHeader(parts.kbJwt);
217
- const kbAlg = kbHeader.alg;
218
- if (typeof kbAlg !== "string" || !allowedAlgorithms.includes(kbAlg)) {
219
- return invalidResult(`Unsupported KB-JWT algorithm: ${kbAlg}`);
220
- }
221
- holderKey = await importJWK(cnf["jwk"], kbAlg);
222
- } catch (err) {
223
- if (err instanceof MalformedCredentialError) throw err;
224
- throw new MalformedCredentialError("Failed to import holder key from cnf.jwk");
225
- }
226
- let kbPayload;
227
- try {
228
- const kbResult = await jwtVerify(parts.kbJwt, holderKey, {
229
- algorithms: allowedAlgorithms,
230
- ...options.audience ? { audience: options.audience } : {}
231
- });
232
- kbPayload = kbResult.payload;
233
- } catch {
234
- return invalidResult("Key binding JWT signature verification failed");
235
- }
236
- if (kbPayload["nonce"] !== options.nonce) {
237
- return invalidResult("Key binding JWT nonce does not match");
238
- }
239
- const disclosurePart = parts.disclosures.length > 0 ? parts.disclosures.join("~") + "~" : "";
240
- const sdJwtForHash = parts.jwt + "~" + disclosurePart;
241
- const sdHashBytes = await sha256Hasher(sdJwtForHash);
242
- const expectedSdHash = bytesToBase64url(sdHashBytes);
243
- if (kbPayload["sd_hash"] !== expectedSdHash) {
244
- return invalidResult("Key binding JWT sd_hash does not match");
245
- }
169
+ if (!credential.claims || credential.claims.length === 0) {
170
+ throw new HaipValidationError(
171
+ "NO_CLAIMS",
172
+ `credential '${credential.id}' must declare at least one claim`,
173
+ credential.id
174
+ );
246
175
  }
247
- let resolvedClaims;
248
- try {
249
- const decoded = await decodeSdJwt(vpToken, sha256Hasher);
250
- resolvedClaims = await getClaims(
251
- decoded.jwt.payload,
252
- decoded.disclosures,
253
- sha256Hasher
176
+ if (credential.claim_sets !== void 0) {
177
+ throw new HaipValidationError(
178
+ "CLAIM_SETS_DISALLOWED",
179
+ `credential '${credential.id}' uses claim_sets, not permitted in HAIP-minimal`,
180
+ credential.id
254
181
  );
255
- } catch {
256
- throw new MalformedCredentialError("Failed to decode SD-JWT disclosures");
257
182
  }
258
- return {
259
- valid: true,
260
- format: this.format,
261
- claims: mapToCredentialClaims(resolvedClaims),
262
- issuer: issuerInfo
263
- };
264
183
  }
265
- };
184
+ }
185
+ function isHaipQuery(query) {
186
+ try {
187
+ validateHaipQuery(query);
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
266
193
  var decoder = new Encoder({ mapsAsObjects: false, useRecords: false });
267
194
  var HEADER_ALG = 1;
268
195
  var HEADER_X5CHAIN = 33;
@@ -369,7 +296,7 @@ async function computeItemDigest(itemBytes, digestAlgorithm) {
369
296
  const buf = await crypto.subtle.digest(digestAlgorithm, new Uint8Array(itemBytes));
370
297
  return new Uint8Array(buf);
371
298
  }
372
- function bytesEqual2(a, b) {
299
+ function bytesEqual(a, b) {
373
300
  if (a.length !== b.length) return false;
374
301
  for (let i = 0; i < a.length; i++) {
375
302
  if (a[i] !== b[i]) return false;
@@ -415,7 +342,7 @@ async function verifyAllDigests(nameSpaces, mso) {
415
342
  if (!expectedDigest) {
416
343
  throw new MalformedCredentialError(`digestID ${digestID} missing from MSO valueDigests[${ns}]`);
417
344
  }
418
- if (!bytesEqual2(hash, expectedDigest)) {
345
+ if (!bytesEqual(hash, expectedDigest)) {
419
346
  throw new MalformedCredentialError(`digest mismatch for ns=${ns} digestID=${digestID}`);
420
347
  }
421
348
  }
@@ -529,15 +456,15 @@ function validateMsoDocType(mso, expectedDocType) {
529
456
  // src/parsers/mdoc.parser.ts
530
457
  var decoder4 = new Encoder({ mapsAsObjects: false, useRecords: false });
531
458
  var encoder = new Encoder({ mapsAsObjects: false, useRecords: false });
532
- var DEFAULT_ALLOWED_ALGORITHMS2 = ["ES256", "ES384", "ES512"];
533
- function bytesEqual3(a, b) {
459
+ var DEFAULT_ALLOWED_ALGORITHMS = ["ES256", "ES384", "ES512"];
460
+ function bytesEqual2(a, b) {
534
461
  if (a.length !== b.length) return false;
535
462
  for (let i = 0; i < a.length; i++) {
536
463
  if (a[i] !== b[i]) return false;
537
464
  }
538
465
  return true;
539
466
  }
540
- function derToPem2(der) {
467
+ function derToPem(der) {
541
468
  let binary = "";
542
469
  for (const b of der) binary += String.fromCharCode(b);
543
470
  const b64 = globalThis.btoa(binary);
@@ -562,8 +489,10 @@ function extractRawItemBytes(item) {
562
489
  throw new MalformedCredentialError("IssuerSignedItem must be a tag-24 byte string");
563
490
  }
564
491
  function mapRawToClaims(nameSpaces) {
565
- const out = {};
566
- for (const items of nameSpaces.values()) {
492
+ const flat = {};
493
+ const namespaced = {};
494
+ for (const [ns, items] of nameSpaces.entries()) {
495
+ const nsBucket = namespaced[ns] ?? {};
567
496
  for (const itemBytes of items) {
568
497
  let decoded;
569
498
  try {
@@ -585,12 +514,14 @@ function mapRawToClaims(nameSpaces) {
585
514
  const id = inner.get("elementIdentifier");
586
515
  const val = inner.get("elementValue");
587
516
  if (typeof id === "string") {
588
- out[id] = val;
517
+ flat[id] = val;
518
+ nsBucket[id] = val;
589
519
  }
590
520
  }
591
521
  }
522
+ namespaced[ns] = nsBucket;
592
523
  }
593
- return out;
524
+ return { flat, namespaced };
594
525
  }
595
526
  var MdocParser = class {
596
527
  format = "mdoc";
@@ -612,7 +543,7 @@ var MdocParser = class {
612
543
  if (!(vpToken instanceof Uint8Array)) {
613
544
  throw new MalformedCredentialError("mDOC vpToken must be a Uint8Array");
614
545
  }
615
- const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS2;
546
+ const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS;
616
547
  let deviceResponse;
617
548
  try {
618
549
  const decoded = decoder4.decode(vpToken);
@@ -659,7 +590,7 @@ var MdocParser = class {
659
590
  );
660
591
  }
661
592
  } else {
662
- const trusted = options.trustedCertificates.some((t) => bytesEqual3(t, issuerCertBytes));
593
+ const trusted = options.trustedCertificates.some((t) => bytesEqual2(t, issuerCertBytes));
663
594
  if (!trusted) {
664
595
  return {
665
596
  valid: false,
@@ -670,7 +601,7 @@ var MdocParser = class {
670
601
  };
671
602
  }
672
603
  }
673
- const publicKey = await importX509(derToPem2(issuerCertBytes), cose.alg);
604
+ const publicKey = await importX509(derToPem(issuerCertBytes), cose.alg);
674
605
  try {
675
606
  await verifyCoseSign1(cose, publicKey, allowedAlgorithms);
676
607
  } catch (err) {
@@ -728,52 +659,243 @@ var MdocParser = class {
728
659
  nameSpaces.set(ns, items.map(extractRawItemBytes));
729
660
  }
730
661
  await verifyAllDigests(nameSpaces, mso);
731
- const claims = mapRawToClaims(nameSpaces);
662
+ const { flat: claims, namespaced: namespacedClaims } = mapRawToClaims(nameSpaces);
732
663
  const issuer = {
733
664
  certificate: issuerCertBytes,
734
665
  country: extractCountryHintFromCert()
735
666
  };
736
- return { valid: true, format: this.format, claims, issuer };
667
+ return {
668
+ valid: true,
669
+ format: this.format,
670
+ claims,
671
+ issuer,
672
+ docType: mso.docType,
673
+ namespacedClaims
674
+ };
737
675
  }
738
676
  };
739
- function createAuthorizationRequest(input) {
740
- const state = input.state ?? v4();
741
- const presentationDefinition = {
742
- id: v4(),
743
- input_descriptors: input.requestedAttributes.map((attr, index) => ({
744
- id: `descriptor_${index}`,
745
- format: Object.fromEntries(
746
- input.acceptedFormats.map((fmt) => {
747
- if (fmt === "sd-jwt-vc") return ["vc+sd-jwt", { "sd-jwt_alg_values": ["ES256"] }];
748
- return ["mso_mdoc", { alg: ["ES256"] }];
749
- })
750
- ),
751
- constraints: {
752
- fields: [
753
- {
754
- path: [`$.${attr}`, `$['${attr}']`],
755
- filter: { type: attr.startsWith("age_over") ? "boolean" : "string" }
756
- }
757
- ]
758
- }
759
- }))
760
- };
761
- const params = new URLSearchParams({
762
- response_type: "vp_token",
763
- response_mode: "direct_post",
764
- response_uri: input.responseUri,
765
- client_id: input.clientId,
766
- nonce: input.nonce,
767
- state,
768
- presentation_definition: JSON.stringify(presentationDefinition)
769
- });
770
- return {
771
- uri: `openid4vp://authorize?${params.toString()}`,
772
- presentationDefinition,
773
- nonce: input.nonce,
774
- state
775
- };
677
+ var DEFAULT_ALLOWED_ALGORITHMS2 = ["ES256", "ES384", "ES512"];
678
+ var sha256Hasher = async (data, _alg) => {
679
+ const encoder2 = new TextEncoder();
680
+ const input = typeof data === "string" ? encoder2.encode(data) : data;
681
+ const hashBuffer = await crypto.subtle.digest("SHA-256", input);
682
+ return new Uint8Array(hashBuffer);
683
+ };
684
+ var BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
685
+ function base64ToBytes(base64) {
686
+ const cleaned = base64.replace(/=+$/, "");
687
+ const byteLength = Math.floor(cleaned.length * 3 / 4);
688
+ const buffer = new ArrayBuffer(byteLength);
689
+ const bytes = new Uint8Array(buffer);
690
+ let byteIndex = 0;
691
+ for (let i = 0; i < cleaned.length; i += 4) {
692
+ const a = BASE64_CHARS.indexOf(cleaned[i]);
693
+ const b = i + 1 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 1]) : 0;
694
+ const c = i + 2 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 2]) : 0;
695
+ const d = i + 3 < cleaned.length ? BASE64_CHARS.indexOf(cleaned[i + 3]) : 0;
696
+ bytes[byteIndex++] = a << 2 | b >> 4;
697
+ if (byteIndex < byteLength) bytes[byteIndex++] = (b & 15) << 4 | c >> 2;
698
+ if (byteIndex < byteLength) bytes[byteIndex++] = (c & 3) << 6 | d;
699
+ }
700
+ return bytes;
701
+ }
702
+ function bytesEqual3(a, b) {
703
+ if (a.length !== b.length) return false;
704
+ for (let i = 0; i < a.length; i++) {
705
+ if (a[i] !== b[i]) return false;
706
+ }
707
+ return true;
708
+ }
709
+ function derToPem2(base64Der) {
710
+ const lines = base64Der.match(/.{1,64}/g) || [];
711
+ return `-----BEGIN CERTIFICATE-----
712
+ ${lines.join("\n")}
713
+ -----END CERTIFICATE-----`;
714
+ }
715
+ function bytesToBase64url(bytes) {
716
+ let binary = "";
717
+ for (const byte of bytes) {
718
+ binary += String.fromCharCode(byte);
719
+ }
720
+ return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
776
721
  }
722
+ function mapToCredentialClaims(raw) {
723
+ const claims = {};
724
+ if (typeof raw["age_over_18"] === "boolean") {
725
+ claims.age_over_18 = raw["age_over_18"];
726
+ }
727
+ if (typeof raw["age_over_21"] === "boolean") {
728
+ claims.age_over_21 = raw["age_over_21"];
729
+ }
730
+ if (typeof raw["resident_country"] === "string") {
731
+ claims.resident_country = raw["resident_country"];
732
+ }
733
+ if (typeof raw["nationality"] === "string") {
734
+ claims.nationality = raw["nationality"];
735
+ }
736
+ if (typeof raw["family_name_birth"] === "string") {
737
+ claims.family_name_birth = raw["family_name_birth"];
738
+ }
739
+ for (const [key, value] of Object.entries(raw)) {
740
+ if (!(key in claims) && !key.startsWith("_sd") && key !== "iss" && key !== "exp" && key !== "iat" && key !== "cnf" && key !== "vct") {
741
+ claims[key] = value;
742
+ }
743
+ }
744
+ return claims;
745
+ }
746
+ function extractCountryHint(issuer) {
747
+ try {
748
+ const url = new URL(issuer);
749
+ const tld = url.hostname.split(".").pop()?.toUpperCase();
750
+ if (tld && tld.length === 2) {
751
+ return tld;
752
+ }
753
+ } catch {
754
+ }
755
+ return "";
756
+ }
757
+ var SdJwtParser = class {
758
+ format = "sd-jwt-vc";
759
+ canParse(vpToken) {
760
+ return typeof vpToken === "string" && vpToken.includes("~");
761
+ }
762
+ async parse(vpToken, options) {
763
+ if (typeof vpToken !== "string") {
764
+ throw new MalformedCredentialError("VP token must be a string");
765
+ }
766
+ const allowedAlgorithms = options.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS2;
767
+ const invalidResult = (error) => ({
768
+ valid: false,
769
+ format: this.format,
770
+ claims: {},
771
+ issuer: { certificate: new Uint8Array(), country: "" },
772
+ error
773
+ });
774
+ let parts;
775
+ try {
776
+ parts = splitSdJwt(vpToken);
777
+ } catch {
778
+ throw new MalformedCredentialError("Failed to split SD-JWT structure");
779
+ }
780
+ let header;
781
+ try {
782
+ header = decodeProtectedHeader(parts.jwt);
783
+ } catch {
784
+ throw new MalformedCredentialError("Failed to decode SD-JWT issuer JWT header");
785
+ }
786
+ const alg = header.alg;
787
+ if (typeof alg !== "string" || !allowedAlgorithms.includes(alg)) {
788
+ return invalidResult(`Unsupported algorithm: ${alg}`);
789
+ }
790
+ const x5cArray = header.x5c;
791
+ if (!Array.isArray(x5cArray) || x5cArray.length === 0 || typeof x5cArray[0] !== "string") {
792
+ throw new MalformedCredentialError("Missing or invalid x5c in JWT header");
793
+ }
794
+ let issuerKey;
795
+ let issuerCertBytes;
796
+ try {
797
+ issuerKey = await importX509(derToPem2(x5cArray[0]), alg);
798
+ issuerCertBytes = base64ToBytes(x5cArray[0]);
799
+ } catch (err) {
800
+ if (err instanceof MalformedCredentialError) throw err;
801
+ throw new MalformedCredentialError("Failed to import issuer certificate");
802
+ }
803
+ let payload;
804
+ try {
805
+ const result = await jwtVerify(parts.jwt, issuerKey, { algorithms: allowedAlgorithms });
806
+ payload = result.payload;
807
+ } catch {
808
+ return invalidResult("Issuer JWT signature verification failed");
809
+ }
810
+ const issuerString = typeof payload["iss"] === "string" ? payload["iss"] : "";
811
+ const issuerInfo = {
812
+ certificate: issuerCertBytes,
813
+ country: extractCountryHint(issuerString)
814
+ };
815
+ if (options.trustedCertificates.length === 0) {
816
+ if (options.skipTrustCheck !== true) {
817
+ throw new MalformedCredentialError(
818
+ "trustedCertificates must not be empty unless skipTrustCheck is true"
819
+ );
820
+ }
821
+ } else {
822
+ const isTrusted = options.trustedCertificates.some((trusted) => bytesEqual3(trusted, issuerCertBytes));
823
+ if (!isTrusted) {
824
+ return invalidResult("Issuer certificate is not trusted");
825
+ }
826
+ }
827
+ const sdArray = payload["_sd"];
828
+ if (parts.disclosures.length > 0) {
829
+ if (!Array.isArray(sdArray) || sdArray.length === 0) {
830
+ return invalidResult("Disclosures present but no _sd array in issuer JWT");
831
+ }
832
+ for (const disclosure of parts.disclosures) {
833
+ const hashBytes = await sha256Hasher(disclosure);
834
+ const hashB64url = bytesToBase64url(hashBytes);
835
+ if (!sdArray.includes(hashB64url)) {
836
+ return invalidResult("Disclosure hash mismatch");
837
+ }
838
+ }
839
+ }
840
+ if (parts.kbJwt) {
841
+ const cnf = payload["cnf"];
842
+ if (!cnf || typeof cnf !== "object" || !cnf["jwk"]) {
843
+ throw new MalformedCredentialError("Missing cnf.jwk in issuer JWT for key binding");
844
+ }
845
+ let holderKey;
846
+ try {
847
+ const kbHeader = decodeProtectedHeader(parts.kbJwt);
848
+ const kbAlg = kbHeader.alg;
849
+ if (typeof kbAlg !== "string" || !allowedAlgorithms.includes(kbAlg)) {
850
+ return invalidResult(`Unsupported KB-JWT algorithm: ${kbAlg}`);
851
+ }
852
+ holderKey = await importJWK(cnf["jwk"], kbAlg);
853
+ } catch (err) {
854
+ if (err instanceof MalformedCredentialError) throw err;
855
+ throw new MalformedCredentialError("Failed to import holder key from cnf.jwk");
856
+ }
857
+ let kbPayload;
858
+ try {
859
+ const kbResult = await jwtVerify(parts.kbJwt, holderKey, {
860
+ algorithms: allowedAlgorithms,
861
+ ...options.audience ? { audience: options.audience } : {}
862
+ });
863
+ kbPayload = kbResult.payload;
864
+ } catch {
865
+ return invalidResult("Key binding JWT signature verification failed");
866
+ }
867
+ if (kbPayload["nonce"] !== options.nonce) {
868
+ return invalidResult("Key binding JWT nonce does not match");
869
+ }
870
+ const disclosurePart = parts.disclosures.length > 0 ? parts.disclosures.join("~") + "~" : "";
871
+ const sdJwtForHash = parts.jwt + "~" + disclosurePart;
872
+ const sdHashBytes = await sha256Hasher(sdJwtForHash);
873
+ const expectedSdHash = bytesToBase64url(sdHashBytes);
874
+ if (kbPayload["sd_hash"] !== expectedSdHash) {
875
+ return invalidResult("Key binding JWT sd_hash does not match");
876
+ }
877
+ }
878
+ let resolvedClaims;
879
+ try {
880
+ const decoded = await decodeSdJwt(vpToken, sha256Hasher);
881
+ resolvedClaims = await getClaims(
882
+ decoded.jwt.payload,
883
+ decoded.disclosures,
884
+ sha256Hasher
885
+ );
886
+ } catch {
887
+ throw new MalformedCredentialError("Failed to decode SD-JWT disclosures");
888
+ }
889
+ const vct = typeof payload["vct"] === "string" ? payload["vct"] : void 0;
890
+ return {
891
+ valid: true,
892
+ format: this.format,
893
+ claims: mapToCredentialClaims(resolvedClaims),
894
+ issuer: issuerInfo,
895
+ ...vct !== void 0 ? { vct } : {}
896
+ };
897
+ }
898
+ };
777
899
 
778
900
  // src/presentation.ts
779
901
  var parsers = [new SdJwtParser(), new MdocParser()];
@@ -791,10 +913,69 @@ async function parsePresentation(vpToken, options) {
791
913
  error: "Unsupported credential format"
792
914
  };
793
915
  }
916
+ var INTERNAL_TO_DCQL_FORMAT = {
917
+ "sd-jwt-vc": "dc+sd-jwt",
918
+ mdoc: "mso_mdoc"
919
+ };
920
+ function classifyMismatch(query, decoded, hasDetail) {
921
+ if (decoded.format !== query.format) return "format_mismatch";
922
+ if (query.format === "dc+sd-jwt") {
923
+ const allowed = query.meta?.vct_values;
924
+ if (allowed && allowed.length > 0 && !allowed.includes(decoded.vct ?? "")) {
925
+ return "vct_mismatch";
926
+ }
927
+ }
928
+ if (query.format === "mso_mdoc") {
929
+ const expected = query.meta?.doctype_value;
930
+ if (expected && expected !== decoded.doctype) return "doctype_mismatch";
931
+ }
932
+ if (query.trusted_authorities && query.trusted_authorities.length > 0) {
933
+ const decodedAuthIds = decoded.trusted_authority_ids ?? [];
934
+ const anyMatch = query.trusted_authorities.some(
935
+ (ta) => ta.values.some((v) => decodedAuthIds.includes(v))
936
+ );
937
+ if (!anyMatch) return "trusted_authority_mismatch";
938
+ }
939
+ if (hasDetail) return "missing_claims";
940
+ return "no_credential_found";
941
+ }
942
+ function refineUnmatched(query, decoded, match) {
943
+ if (match.unmatched.length === 0) return match;
944
+ const refined = match.unmatched.map((entry) => {
945
+ const credentialQuery = query.credentials.find((c) => c.id === entry.queryId);
946
+ if (!credentialQuery) return entry;
947
+ const specific = classifyMismatch(credentialQuery, decoded, entry.detail !== void 0);
948
+ return { ...entry, reason: specific };
949
+ });
950
+ return { ...match, unmatched: refined };
951
+ }
952
+ async function verifyPresentation(vpToken, query, options) {
953
+ const parsed = await parsePresentation(vpToken, options);
954
+ const dcqlFormat = INTERNAL_TO_DCQL_FORMAT[parsed.format] ?? parsed.format;
955
+ const decodedClaims = parsed.format === "mdoc" && parsed.namespacedClaims !== void 0 ? parsed.namespacedClaims : parsed.claims;
956
+ const decoded = {
957
+ id: query.credentials[0]?.id ?? "presented",
958
+ format: dcqlFormat,
959
+ claims: decodedClaims
960
+ };
961
+ if (parsed.format === "sd-jwt-vc" && typeof parsed.vct === "string") {
962
+ decoded.vct = parsed.vct;
963
+ }
964
+ if (parsed.format === "mdoc" && typeof parsed.docType === "string") {
965
+ decoded.doctype = parsed.docType;
966
+ }
967
+ const rawMatch = matchQuery(query, [decoded]);
968
+ const match = refineUnmatched(query, decoded, rawMatch);
969
+ const submission = match.satisfied ? buildSubmission(query, match) : null;
970
+ return {
971
+ parsed,
972
+ match,
973
+ submission,
974
+ valid: parsed.valid && match.satisfied
975
+ };
976
+ }
977
+ var VERSION = "0.4.0";
794
978
 
795
- // src/index.ts
796
- var VERSION = "0.3.0";
797
-
798
- export { ExpiredCredentialError, InvalidSignatureError, MalformedCredentialError, MdocParser, NonceValidationError, SdJwtParser, UnsupportedFormatError, VERSION, createAuthorizationRequest, parsePresentation };
979
+ export { ExpiredCredentialError, HAIP_DOCTYPE_NAMESPACES, HaipValidationError, InvalidSignatureError, MalformedCredentialError, MdocParser, NonceValidationError, SdJwtParser, UnsupportedFormatError, VERSION, buildHaipQuery, createAuthorizationRequest, isHaipQuery, parsePresentation, validateHaipQuery, verifyPresentation };
799
980
  //# sourceMappingURL=index.js.map
800
981
  //# sourceMappingURL=index.js.map