@digitalbazaar/oid4-client 5.1.0 → 5.2.1

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.
@@ -0,0 +1,126 @@
1
+ /*!
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {assert, fetchJSON} from '../util.js';
5
+
6
+ const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
7
+
8
+ export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
9
+ try {
10
+ assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
11
+
12
+ const response = await fetchJSON({url: issuerConfigUrl, agent});
13
+ if(!response.data) {
14
+ const error = new Error('Issuer configuration format is not JSON.');
15
+ error.name = 'DataError';
16
+ throw error;
17
+ }
18
+ const {data: issuerMetaData} = response;
19
+ const {issuer, authorization_server} = issuerMetaData;
20
+
21
+ if(authorization_server && authorization_server !== issuer) {
22
+ // not yet implemented
23
+ throw new Error('Separate authorization server not yet implemented.');
24
+ }
25
+
26
+ // validate `issuer`
27
+ if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
28
+ const error = new Error('"issuer" is not an HTTPS URL.');
29
+ error.name = 'DataError';
30
+ throw error;
31
+ }
32
+
33
+ // ensure `credential_issuer` matches `issuer`, if present
34
+ const {credential_issuer} = issuerMetaData;
35
+ if(credential_issuer !== undefined && credential_issuer !== issuer) {
36
+ const error = new Error('"credential_issuer" must match "issuer".');
37
+ error.name = 'DataError';
38
+ throw error;
39
+ }
40
+
41
+ /* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
42
+
43
+ The `origin` and `path` element must be parsed from `issuer` and checked
44
+ against `issuerConfigUrl` like so:
45
+
46
+ For issuer `<origin>` (no path), `issuerConfigUrl` must match:
47
+ `<origin>/.well-known/<any-path-segment>`
48
+
49
+ For issuer `<origin><path>`, `issuerConfigUrl` must be:
50
+ `<origin>/.well-known/<any-path-segment><path>` */
51
+ const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
52
+ const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
53
+ const {origin, pathname} = new URL(issuer);
54
+ let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
55
+ if(pathname !== '/') {
56
+ expectedConfigUrl += pathname;
57
+ }
58
+ if(issuerConfigUrl !== expectedConfigUrl) {
59
+ // alternatively, against RFC 8414, but according to OID4VCI, make sure
60
+ // the issuer config URL matches:
61
+ // <origin><path>/.well-known/<any-path-segment>
62
+ expectedConfigUrl = origin;
63
+ if(pathname !== '/') {
64
+ expectedConfigUrl += pathname;
65
+ }
66
+ expectedConfigUrl += `/.well-known/${anyPathSegment}`;
67
+ if(issuerConfigUrl !== expectedConfigUrl) {
68
+ const error = new Error('"issuer" does not match configuration URL.');
69
+ error.name = 'DataError';
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ // fetch AS meta data
75
+ const asMetaDataUrl =
76
+ `${origin}/.well-known/oauth-authorization-server${pathname}`;
77
+ const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
78
+ if(!asMetaDataResponse.data) {
79
+ const error = new Error('Authorization server meta data is not JSON.');
80
+ error.name = 'DataError';
81
+ throw error;
82
+ }
83
+
84
+ const {data: asMetaData} = response;
85
+ // merge AS meta data into total issuer config
86
+ const issuerConfig = {...issuerMetaData, ...asMetaData};
87
+
88
+ // ensure `token_endpoint` is valid
89
+ const {token_endpoint} = asMetaData;
90
+ assert(token_endpoint, 'token_endpoint', 'string');
91
+
92
+ // return merged config and separate issuer and AS configs
93
+ const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
94
+ return {issuerConfig, metadata};
95
+ } catch(cause) {
96
+ const error = new Error('Could not get OpenID issuer configuration.');
97
+ error.name = 'OperationError';
98
+ error.cause = cause;
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ export async function robustDiscoverIssuer({issuer, agent} = {}) {
104
+ // try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
105
+ const parsedIssuer = new URL(issuer);
106
+ const {origin} = parsedIssuer;
107
+ const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
108
+
109
+ const issuerConfigUrls = [
110
+ // OID4VCI
111
+ `${origin}${path}/.well-known/openid-credential-issuer`,
112
+ // RFC 8414
113
+ `${origin}/.well-known/openid-credential-issuer${path}`
114
+ ];
115
+
116
+ let error;
117
+ for(const issuerConfigUrl of issuerConfigUrls) {
118
+ try {
119
+ const config = await discoverIssuer({issuerConfigUrl, agent});
120
+ return config;
121
+ } catch(e) {
122
+ error = e;
123
+ }
124
+ }
125
+ throw error;
126
+ }
@@ -0,0 +1,50 @@
1
+ /*!
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {signJWT} from '../util.js';
5
+
6
+ export async function generateDIDProofJWT({
7
+ signer, nonce, iss, aud, exp, nbf
8
+ } = {}) {
9
+ /* Example:
10
+ {
11
+ "alg": "ES256",
12
+ "kid":"did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1"
13
+ }.
14
+ {
15
+ "iss": "s6BhdRkqt3",
16
+ "aud": "https://server.example.com",
17
+ "iat": 1659145924,
18
+ "nonce": "tZignsnFbp"
19
+ }
20
+ */
21
+
22
+ if(exp === undefined) {
23
+ // default to 5 minute expiration time
24
+ exp = Math.floor(Date.now() / 1000) + 60 * 5;
25
+ }
26
+ if(nbf === undefined) {
27
+ // default to now
28
+ nbf = Math.floor(Date.now() / 1000);
29
+ }
30
+
31
+ const {id: kid} = signer;
32
+ const alg = _curveToAlg(signer.algorithm);
33
+ const payload = {nonce, iss, aud, exp, nbf};
34
+ const protectedHeader = {alg, kid};
35
+
36
+ return signJWT({payload, protectedHeader, signer});
37
+ }
38
+
39
+ function _curveToAlg(crv) {
40
+ if(crv === 'Ed25519' || crv === 'Ed448') {
41
+ return 'EdDSA';
42
+ }
43
+ if(crv?.startsWith('P-')) {
44
+ return `ES${crv.slice(2)}`;
45
+ }
46
+ if(crv === 'secp256k1') {
47
+ return 'ES256K';
48
+ }
49
+ return crv;
50
+ }
@@ -2,8 +2,9 @@
2
2
  * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import {
5
- assert, assertOptional, base64Encode, createNamedError, fetchJSON, selectJwk
6
- } from './util.js';
5
+ assert, assertOptional, base64Encode,
6
+ createNamedError, fetchJSON, selectJwk, sha256
7
+ } from '../util.js';
7
8
  import {decodeJwt, importX509, jwtVerify} from 'jose';
8
9
  import {
9
10
  hasDomainSubjectAltName, parseCertificateChain, verifyCertificateChain
@@ -269,7 +270,7 @@ async function _checkClientIdSchemeRequirements({
269
270
  and it includes the client ID. */
270
271
  } else if(clientIdScheme === 'x509_hash') {
271
272
  // `x509_hash:<base64url sha256-hash of DER leaf cert>`
272
- const hash = base64Encode(await _sha256(chain[0].toBER()));
273
+ const hash = base64Encode(await sha256(chain[0].toBER()));
273
274
  if(clientId !== hash) {
274
275
  throw createNamedError({
275
276
  message:
@@ -444,14 +445,6 @@ function _parseOID4VPUrl({url}) {
444
445
  return {authorizationRequest};
445
446
  }
446
447
 
447
- async function _sha256(data) {
448
- if(typeof data === 'string') {
449
- data = new TextEncoder().encode(data);
450
- }
451
- const algorithm = {name: 'SHA-256'};
452
- return new Uint8Array(await crypto.subtle.digest(algorithm, data));
453
- }
454
-
455
448
  function _throwKeyNotFound(protectedHeader) {
456
449
  const error = new Error(
457
450
  'Could not verify signed authorization request; ' +
@@ -1,11 +1,11 @@
1
1
  /*!
2
2
  * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {createNamedError, selectJwk} from './util.js';
4
+ import {createNamedError, selectJwk} from '../util.js';
5
5
  import {EncryptJWT} from 'jose';
6
6
  import {httpClient} from '@digitalbazaar/http-client';
7
- import jsonpointer from 'jsonpointer';
8
- import {pathsToVerifiableCredentialPointers} from './convert.js';
7
+ import {pathsToVerifiableCredentialPointers} from '../convert/index.js';
8
+ import {resolvePointer} from '../query/util.js';
9
9
 
10
10
  const TEXT_ENCODER = new TextEncoder();
11
11
 
@@ -340,7 +340,7 @@ function _matchesInputDescriptor({
340
340
 
341
341
  // check for a value at at least one path
342
342
  for(const pointer of pointers) {
343
- const existing = jsonpointer.get(verifiableCredential, pointer);
343
+ const existing = resolvePointer(verifiableCredential, pointer);
344
344
  if(existing === undefined) {
345
345
  // VC does not match
346
346
  return false;
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export * as authzRequest from './authorizationRequest.js';
5
5
  export * as authzResponse from './authorizationResponse.js';
6
- export * as convert from './convert.js';
6
+ export * as convert from '../convert/index.js';
7
7
  export * as verifier from './verifier.js';
8
8
 
9
9
  // backwards compatibility APIs
@@ -14,11 +14,7 @@ export {
14
14
  createPresentationSubmission,
15
15
  send as sendAuthorizationResponse
16
16
  } from './authorizationResponse.js';
17
- export {
18
- fromVpr, toVpr,
19
- // exported for testing purposes only
20
- _fromQueryByExampleQuery
21
- } from './convert.js';
17
+ export {fromVpr, toVpr} from '../convert/index.js';
22
18
 
23
19
  // Note: for examples of presentation request and responses, see:
24
20
  // eslint-disable-next-line max-len
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {createNamedError, selectJwk} from './util.js';
4
+ import {createNamedError, parseJSON, selectJwk} from '../util.js';
5
5
  import {importJWK, jwtDecrypt} from 'jose';
6
6
 
7
7
  // parses (and decrypts) an authz response from a response body object
@@ -31,7 +31,7 @@ export async function parseAuthorizationResponse({
31
31
  responseMode = 'direct_post';
32
32
  _assertSupportedResponseMode({responseMode, supportedResponseModes});
33
33
  payload = body;
34
- parsed.presentationSubmission = _jsonParse(
34
+ parsed.presentationSubmission = parseJSON(
35
35
  payload.presentation_submission, 'presentation_submission');
36
36
  }
37
37
 
@@ -46,7 +46,7 @@ export async function parseAuthorizationResponse({
46
46
  if(typeof vp_token === 'string' &&
47
47
  (vp_token.startsWith('{') || vp_token.startsWith('[') ||
48
48
  vp_token.startsWith('"'))) {
49
- parsed.vpToken = _jsonParse(vp_token, 'vp_token');
49
+ parsed.vpToken = parseJSON(vp_token, 'vp_token');
50
50
  } else {
51
51
  parsed.vpToken = vp_token;
52
52
  }
@@ -100,16 +100,3 @@ async function _decrypt({jwt, getDecryptParameters}) {
100
100
  keyManagementAlgorithms: ['ECDH-ES']
101
101
  });
102
102
  }
103
-
104
- function _jsonParse(x, name) {
105
- try {
106
- return JSON.parse(x);
107
- } catch(cause) {
108
- throw createNamedError({
109
- message: `Could not parse "${name}".`,
110
- name: 'DataError',
111
- details: {httpStatusCode: 400, public: true},
112
- cause
113
- });
114
- }
115
- }
@@ -6,7 +6,7 @@ import {
6
6
  CertificateChainValidationEngine,
7
7
  id_SubjectAltName
8
8
  } from 'pkijs';
9
- import {base64Decode} from './util.js';
9
+ import {base64Decode} from '../util.js';
10
10
 
11
11
  export function fromPemOrBase64(str) {
12
12
  const tag = 'CERTIFICATE';
@@ -0,0 +1,244 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {
5
+ fromJsonPointerMap, isNumber, toJsonPointerMap, toNumberIfNumber
6
+ } from './util.js';
7
+ import {exampleToJsonPointerMap} from './queryByExample.js';
8
+ import jsonpointer from 'json-pointer';
9
+
10
+ const MDOC_MDL = 'org.iso.18013.5.1.mDL';
11
+
12
+ export function dcqlQueryToVprGroups({dcql_query} = {}) {
13
+ const {credentials = []} = dcql_query;
14
+ let {credential_sets: credentialSets} = dcql_query;
15
+ if(!credentialSets) {
16
+ credentialSets = [{
17
+ options: [credentials.map(q => q.id)]
18
+ }];
19
+ }
20
+
21
+ // convert `credentials` into a map based on `ID`
22
+ const credentialQueryMap = new Map(credentials.map(
23
+ query => [query.id, query]));
24
+
25
+ // output group map
26
+ const groupMap = new Map();
27
+ for(const credentialSet of credentialSets) {
28
+ for(const option of credentialSet.options) {
29
+ const groupId = crypto.randomUUID();
30
+ const queries = option.map(id => {
31
+ const dcqlCredentialQuery = credentialQueryMap.get(id);
32
+ return {
33
+ type: 'QueryByExample',
34
+ group: groupId,
35
+ credentialQuery: _toQueryByExampleQuery({dcqlCredentialQuery})
36
+ };
37
+ });
38
+ groupMap.set(groupId, new Map([['QueryByExample', queries]]));
39
+ }
40
+ }
41
+ return groupMap;
42
+ }
43
+
44
+ export function dcqlCredentialQueryToJsonPointerMap({
45
+ dcqlCredentialQuery
46
+ } = {}) {
47
+ const queryByExample = _toQueryByExampleQuery({dcqlCredentialQuery});
48
+ return exampleToJsonPointerMap(queryByExample);
49
+ }
50
+
51
+ export function vprGroupsToDcqlQuery({groupMap, options = {}} = {}) {
52
+ // if there is any DCQL query, return it as-is
53
+ const groups = [...groupMap.values()];
54
+ let dcqlQuery = groups
55
+ .filter(g => g.has('DigitalCredentialQueryLanguage'))
56
+ .map(g => g.get('DigitalCredentialQueryLanguage'))
57
+ .at(-1);
58
+ if(dcqlQuery) {
59
+ return dcqlQuery;
60
+ }
61
+
62
+ // convert all `QueryByExample` queries to a single DCQL query
63
+ const {nullyifyArrayIndices = false} = options;
64
+ dcqlQuery = {};
65
+ const credentials = [];
66
+ const credentialSets = [{options: []}];
67
+
68
+ // note: same group ID is logical "AND" and different group ID is "OR"
69
+ for(const queries of groups) {
70
+ // only `QueryByExample` is convertible
71
+ const queryByExamples = queries.get('QueryByExample');
72
+ if(!(queryByExamples?.length > 0)) {
73
+ continue;
74
+ }
75
+
76
+ // for each `QueryByExample`, add another option
77
+ const option = [];
78
+ for(const queryByExample of queryByExamples) {
79
+ // should only be one `credentialQuery` but handle each one as a new
80
+ // DCQL credential query
81
+ const all = Array.isArray(queryByExample.credentialQuery) ?
82
+ queryByExample.credentialQuery : [queryByExample.credentialQuery];
83
+ for(const credentialQuery of all) {
84
+ const result = _fromQueryByExampleQuery({
85
+ credentialQuery, nullyifyArrayIndices
86
+ });
87
+ credentials.push(result);
88
+ // DCQL credential set "option" includes all queries in the "AND" group
89
+ option.push(result.id);
90
+ }
91
+ }
92
+
93
+ // add "option" as another "OR" in the DCQL "options"
94
+ credentialSets[0].options.push(option);
95
+ }
96
+
97
+ if(credentials.length > 0) {
98
+ dcqlQuery.credentials = credentials;
99
+ }
100
+ if(credentialSets[0].options?.length > 0) {
101
+ dcqlQuery.credential_sets = credentialSets;
102
+ }
103
+
104
+ return dcqlQuery;
105
+ }
106
+
107
+ // exported for testing purposes only
108
+ export function _fromQueryByExampleQuery({
109
+ credentialQuery, nullyifyArrayIndices = false
110
+ }) {
111
+ const result = {
112
+ id: crypto.randomUUID(),
113
+ format: 'ldp_vc',
114
+ meta: {
115
+ type_values: ['https://www.w3.org/2018/credentials#VerifiableCredential']
116
+ }
117
+ };
118
+ if(credentialQuery?.reason) {
119
+ result.meta.reason = credentialQuery.reason;
120
+ }
121
+
122
+ const {example = {}} = credentialQuery ?? {};
123
+
124
+ // determine credential format
125
+ if(Array.isArray(credentialQuery.acceptedEnvelopes)) {
126
+ const set = new Set(credentialQuery.acceptedEnvelopes);
127
+ if(set.has('application/jwt')) {
128
+ result.format = 'jwt_vc_json';
129
+ } else if(set.has('application/mdl')) {
130
+ result.format = 'mso_mdoc';
131
+ result.meta = {doctype_value: MDOC_MDL};
132
+ } else if(set.has('application/dc+sd-jwt')) {
133
+ result.format = 'dc+sd-jwt';
134
+ result.meta = {vct_values: []};
135
+ if(Array.isArray(example?.type)) {
136
+ result.meta.vct_values.push(...example.type);
137
+ } else if(typeof example.type === 'string') {
138
+ result.meta.vct_values.push(example.type);
139
+ }
140
+ }
141
+ }
142
+
143
+ // convert `example` into json pointers and walk to produce DCQL claim paths
144
+ const pointers = toJsonPointerMap({obj: example, flat: true});
145
+ const pathsMap = new Map();
146
+ for(const [pointer, value] of pointers) {
147
+ // parse path into DCQL path w/ numbers for array indexes
148
+ let path = jsonpointer.parse(pointer).map(toNumberIfNumber);
149
+
150
+ // special process non-`@context` paths to convert some array indexes
151
+ // to DCQL `null` (which means "any" index)
152
+ if(path[0] !== '@context') {
153
+ if(nullyifyArrayIndices) {
154
+ // brute force convert every array index to `null` by request
155
+ path = path.map(p => isNumber(p) ? null : p);
156
+ } else if(isNumber(path.at(-1)) && !isNumber(path.at(-2))) {
157
+ // when a pointer terminates at an array element it means candidate
158
+ // matching values are expressed in the `example`, so make sure to
159
+ // share the path for all candidates
160
+ path[path.length - 1] = null;
161
+ }
162
+ }
163
+
164
+ // compile processed path back to a key to consolidate `values`
165
+ const key = jsonpointer.compile(path.map(p => p === null ? 'null' : p));
166
+
167
+ // create shared entry for path and candidate matching values
168
+ let entry = pathsMap.get(key);
169
+ if(!entry) {
170
+ entry = {path, valueSet: new Set()};
171
+ pathsMap.set(key, entry);
172
+ }
173
+
174
+ // add any non-QueryByExample-wildcard as a DCQL candidate match value
175
+ if(!(value === '' || value instanceof Map || value instanceof Set)) {
176
+ entry.valueSet.add(value);
177
+ }
178
+ }
179
+
180
+ // produce DCQL `claims`
181
+ const claims = [...pathsMap.values()].map(({path, valueSet}) => {
182
+ const claim = {path};
183
+ if(valueSet.size > 0) {
184
+ claim.values = [...valueSet];
185
+ }
186
+ return claim;
187
+ });
188
+
189
+ if(claims.length > 0) {
190
+ result.claims = claims;
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ // exported for testing purposes only
197
+ export function _toQueryByExampleQuery({dcqlCredentialQuery}) {
198
+ // convert DCQL credential query to pointers
199
+ const pointers = new Map();
200
+ const {format, meta, claims = []} = dcqlCredentialQuery;
201
+ for(const claim of claims) {
202
+ const {values} = claim;
203
+
204
+ // a trailing `null` in a path means `values` should be treated as a set
205
+ // of candidates inside an array value at path-1
206
+ const path = claim.path?.at(-1) === null ?
207
+ claim.path.slice(0, -1) : claim.path;
208
+
209
+ // convert `null` path tokens to an index; assume the use of `null` will
210
+ // not be combined with any other index
211
+ const pointer = jsonpointer.compile(path.map(p => p === null ? '0' : p));
212
+ if(!values) {
213
+ pointers.set(pointer, '');
214
+ } else if(values.length === 1 && claim.path.at(-1) !== null) {
215
+ // convert a single choice for a non-array value to a primitive
216
+ pointers.set(pointer, values[0]);
217
+ } else {
218
+ pointers.set(pointer, new Set(values));
219
+ }
220
+ }
221
+
222
+ const credentialQuery = {};
223
+ if(meta?.reason) {
224
+ credentialQuery.reason = meta.reason;
225
+ }
226
+
227
+ // convert pointers to example object
228
+ credentialQuery.example = fromJsonPointerMap({map: pointers});
229
+
230
+ if(format === 'jwt_vc_json') {
231
+ credentialQuery.acceptedEnvelopes = ['application/jwt'];
232
+ } else if(format === 'mso_mdoc') {
233
+ if(meta?.doctype_value === MDOC_MDL) {
234
+ credentialQuery.acceptedEnvelopes = ['application/mdl'];
235
+ } else {
236
+ credentialQuery.acceptedEnvelopes = ['application/mdoc'];
237
+ }
238
+ } else if(format === 'dc+sd-jwt') {
239
+ // FIXME: consider adding `vct_values` as params
240
+ credentialQuery.acceptedEnvelopes = ['application/dc+sd-jwt'];
241
+ }
242
+
243
+ return credentialQuery;
244
+ }
@@ -0,0 +1,18 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {dcqlCredentialQueryToJsonPointerMap} from './dcql.js';
5
+ import {exampleToJsonPointerMap} from './queryByExample.js';
6
+ import {inputDescriptorToJsonPointerMap} from './presentationExchange.js';
7
+
8
+ export {credentialMatches} from './match.js';
9
+
10
+ export const dcql = {
11
+ toJsonPointerMap: dcqlCredentialQueryToJsonPointerMap
12
+ };
13
+ export const presentationExchange = {
14
+ toJsonPointerMap: inputDescriptorToJsonPointerMap
15
+ };
16
+ export const queryByExample = {
17
+ toJsonPointerMap: exampleToJsonPointerMap
18
+ };
@@ -0,0 +1,80 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {isObject, resolvePointer, toNumberIfNumber} from './util.js';
5
+
6
+ /**
7
+ * Returns whether a credential matches against a JSON pointer map.
8
+ *
9
+ * A JSON pointer map must be created from a QueryByExample `example`, a DCQL
10
+ * `credential` query value, or a Presentation Exchange input descriptor
11
+ * by calling the respective utility APIs in this library. It is more efficient
12
+ * to produce this JSON pointer map just once when looking for matches in a
13
+ * list of more than one credential.
14
+ *
15
+ * @param {object} options - The options.
16
+ * @param {object} options.credential - The credential to try to match.
17
+ * @param {Map} options.map - The JSON pointer map.
18
+ * @param {object} options.options - Match options, such as:
19
+ * [coerceNumbers=true] - String/numbers will be coerced.
20
+ *
21
+ * @returns {boolean} `true` if the credential matches, `false` if not.
22
+ */
23
+ export function credentialMatches({
24
+ credential, map, options = {coerceNumbers: true}
25
+ } = {}) {
26
+ // credential must be an object to match
27
+ if(!isObject(credential)) {
28
+ return false;
29
+ }
30
+ return _match({cursor: credential, matchValue: map, options});
31
+ }
32
+
33
+ function _match({cursor, matchValue, options}) {
34
+ // handle wildcard matching
35
+ if(_isWildcard(matchValue)) {
36
+ return true;
37
+ }
38
+
39
+ if(matchValue instanceof Set) {
40
+ // some element in the set must match `cursor`
41
+ return [...matchValue].some(e => _match({cursor, matchValue: e, options}));
42
+ }
43
+
44
+ if(matchValue instanceof Map) {
45
+ // all pointers and values in the map must match `cursor`
46
+ return [...matchValue.entries()].every(([pointer, matchValue]) => {
47
+ const value = resolvePointer(cursor, pointer);
48
+ if(value === undefined) {
49
+ // no value at `pointer`; no match
50
+ return false;
51
+ }
52
+ // handles case where `value` is an empty array + wildcard `matchValue`
53
+ if(_isWildcard(matchValue)) {
54
+ return true;
55
+ }
56
+ // normalize value to an array for matching
57
+ const values = Array.isArray(value) ? value : [value];
58
+ return values.some(v => _match({cursor: v, matchValue, options}));
59
+ });
60
+ }
61
+
62
+ // primitive comparison
63
+ if(cursor === matchValue) {
64
+ return true;
65
+ }
66
+
67
+ // string/number coercion
68
+ if(options.coerceNumbers) {
69
+ const cursorNumber = toNumberIfNumber(cursor);
70
+ const matchNumber = toNumberIfNumber(matchValue);
71
+ return cursorNumber !== undefined && cursorNumber === matchNumber;
72
+ }
73
+
74
+ return false;
75
+ }
76
+
77
+ function _isWildcard(value) {
78
+ // empty string, Map, or Set
79
+ return value === '' || value?.size === 0;
80
+ }