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