@digitalbazaar/oid4-client 5.0.0 → 5.2.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/lib/OID4Client.js +6 -74
- package/lib/convert/index.js +349 -0
- package/lib/index.js +9 -4
- package/lib/oid4vci/credentialOffer.js +138 -0
- package/lib/oid4vci/discovery.js +126 -0
- package/lib/oid4vci/proofs.js +50 -0
- package/lib/{authorizationRequest.js → oid4vp/authorizationRequest.js} +4 -11
- package/lib/{authorizationResponse.js → oid4vp/authorizationResponse.js} +85 -44
- package/lib/{oid4vp.js → oid4vp/index.js} +3 -6
- package/lib/oid4vp/verifier.js +102 -0
- package/lib/{x509.js → oid4vp/x509.js} +1 -1
- package/lib/query/dcql.js +244 -0
- package/lib/query/index.js +18 -0
- package/lib/query/match.js +80 -0
- package/lib/query/presentationExchange.js +328 -0
- package/lib/query/queryByExample.js +8 -0
- package/lib/query/util.js +105 -0
- package/lib/util.js +27 -232
- package/package.json +5 -3
- package/lib/convert.js +0 -430
package/lib/OID4Client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {createCredentialRequestsFromOffer} from './oid4vci/credentialOffer.js';
|
|
5
|
+
import {generateDIDProofJWT} from './oid4vci/proofs.js';
|
|
5
6
|
import {httpClient} from '@digitalbazaar/http-client';
|
|
7
|
+
import {robustDiscoverIssuer} from './oid4vci/discovery.js';
|
|
6
8
|
|
|
7
9
|
const GRANT_TYPES = new Map([
|
|
8
10
|
['preAuthorizedCode', 'urn:ietf:params:oauth:grant-type:pre-authorized_code']
|
|
@@ -66,7 +68,7 @@ export class OID4Client {
|
|
|
66
68
|
if(!offer) {
|
|
67
69
|
throw new TypeError('"credentialDefinition" must be an object.');
|
|
68
70
|
}
|
|
69
|
-
requests =
|
|
71
|
+
requests = createCredentialRequestsFromOffer({
|
|
70
72
|
issuerConfig, offer, format
|
|
71
73
|
});
|
|
72
74
|
if(requests.length > 1) {
|
|
@@ -97,7 +99,7 @@ export class OID4Client {
|
|
|
97
99
|
|
|
98
100
|
const {issuerConfig, offer} = this;
|
|
99
101
|
if(requests === undefined && offer) {
|
|
100
|
-
requests =
|
|
102
|
+
requests = createCredentialRequestsFromOffer({
|
|
101
103
|
issuerConfig, offer, format
|
|
102
104
|
});
|
|
103
105
|
} else if(!(Array.isArray(requests) && requests.length > 0)) {
|
|
@@ -464,73 +466,3 @@ function _isPresentationRequired(error) {
|
|
|
464
466
|
const errorType = error.data?.error;
|
|
465
467
|
return error.status === 400 && errorType === 'presentation_required';
|
|
466
468
|
}
|
|
467
|
-
|
|
468
|
-
function _createCredentialRequestsFromOffer({
|
|
469
|
-
issuerConfig, offer, format
|
|
470
|
-
}) {
|
|
471
|
-
// get any supported credential configurations from issuer config
|
|
472
|
-
const supported = _createSupportedCredentialsMap({issuerConfig});
|
|
473
|
-
|
|
474
|
-
// build requests from credentials identified in `offer` and remove any
|
|
475
|
-
// that do not match the given format
|
|
476
|
-
const credentials = offer.credential_configuration_ids ?? offer.credentials;
|
|
477
|
-
const requests = credentials.map(c => {
|
|
478
|
-
if(typeof c === 'string') {
|
|
479
|
-
// use supported credential config
|
|
480
|
-
return _getSupportedCredentialById({id: c, supported});
|
|
481
|
-
}
|
|
482
|
-
return c;
|
|
483
|
-
}).filter(r => r.format === format);
|
|
484
|
-
|
|
485
|
-
if(requests.length === 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`No supported credential(s) with format "${format}" found.`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return requests;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function _createSupportedCredentialsMap({issuerConfig}) {
|
|
494
|
-
const {
|
|
495
|
-
credential_configurations_supported,
|
|
496
|
-
credentials_supported
|
|
497
|
-
} = issuerConfig;
|
|
498
|
-
|
|
499
|
-
let supported;
|
|
500
|
-
if(credential_configurations_supported &&
|
|
501
|
-
typeof credential_configurations_supported === 'object') {
|
|
502
|
-
supported = new Map(Object.entries(
|
|
503
|
-
issuerConfig.credential_configurations_supported));
|
|
504
|
-
} else if(Array.isArray(credentials_supported)) {
|
|
505
|
-
// handle legacy `credentials_supported` array
|
|
506
|
-
supported = new Map();
|
|
507
|
-
for(const entry of issuerConfig.credentials_supported) {
|
|
508
|
-
supported.set(entry.id, entry);
|
|
509
|
-
}
|
|
510
|
-
} else {
|
|
511
|
-
// no supported credentials from issuer config
|
|
512
|
-
supported = new Map();
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return supported;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function _getSupportedCredentialById({id, supported}) {
|
|
519
|
-
const meta = supported.get(id);
|
|
520
|
-
if(!meta) {
|
|
521
|
-
throw new Error(`No supported credential "${id}" found.`);
|
|
522
|
-
}
|
|
523
|
-
const {format, credential_definition} = meta;
|
|
524
|
-
if(typeof format !== 'string') {
|
|
525
|
-
throw new Error(
|
|
526
|
-
`Invalid supported credential "${id}"; "format" not specified.`);
|
|
527
|
-
}
|
|
528
|
-
if(!(Array.isArray(credential_definition?.['@context']) &&
|
|
529
|
-
(Array.isArray(credential_definition?.types) ||
|
|
530
|
-
Array.isArray(credential_definition?.type)))) {
|
|
531
|
-
throw new Error(
|
|
532
|
-
`Invalid supported credential "${id}"; "credential_definition" not ` +
|
|
533
|
-
'fully specified.');
|
|
534
|
-
}
|
|
535
|
-
return {format, credential_definition};
|
|
536
|
-
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {dcqlQueryToVprGroups, vprGroupsToDcqlQuery} from '../query/dcql.js';
|
|
5
|
+
import {
|
|
6
|
+
presentationDefinitionToVprGroups,
|
|
7
|
+
vprGroupsToPresentationDefinition
|
|
8
|
+
} from '../query/presentationExchange.js';
|
|
9
|
+
import {
|
|
10
|
+
validate as validateAuthorizationRequest
|
|
11
|
+
} from '../oid4vp/authorizationRequest.js';
|
|
12
|
+
|
|
13
|
+
// backwards compatible exports
|
|
14
|
+
export {
|
|
15
|
+
pathsToVerifiableCredentialPointers
|
|
16
|
+
} from '../query/presentationExchange.js';
|
|
17
|
+
|
|
18
|
+
// currently supported VPR query types for conversion
|
|
19
|
+
const DID_AUTHENTICATION = 'DIDAuthentication';
|
|
20
|
+
const QUERY_BY_EXAMPLE = 'QueryByExample';
|
|
21
|
+
const DCQL = 'DigitalCredentialQueryLanguage';
|
|
22
|
+
const CONVERTIBLE_QUERY_TYPES = new Set([
|
|
23
|
+
QUERY_BY_EXAMPLE, DID_AUTHENTICATION, DCQL
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// converts a VPR to partial "authorization request"
|
|
27
|
+
export function fromVpr({
|
|
28
|
+
verifiablePresentationRequest,
|
|
29
|
+
strict = false,
|
|
30
|
+
queryFormats = {
|
|
31
|
+
// can replace `true` with options:
|
|
32
|
+
// e.g., dcql options: {nullifyArrayIndices: true}
|
|
33
|
+
dcql: true,
|
|
34
|
+
presentationExchange: true
|
|
35
|
+
},
|
|
36
|
+
// presentation exchange (deprecated) options:
|
|
37
|
+
prefixJwtVcPath,
|
|
38
|
+
// authorization request options (`false` for backwards compatibility, use
|
|
39
|
+
// `true` for OID4VP 1.0+)
|
|
40
|
+
useClientIdPrefix = false
|
|
41
|
+
} = {}) {
|
|
42
|
+
try {
|
|
43
|
+
if(!(queryFormats?.dcql || queryFormats?.presentationExchange)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'At least one of "queryFormats.dcql" or ' +
|
|
46
|
+
'"queryFormats.presentationExchange" is required.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// convert to query groups structure for processing
|
|
50
|
+
const groupMap = _vprQueryToGroups({verifiablePresentationRequest});
|
|
51
|
+
if(strict) {
|
|
52
|
+
_strictCheckVprGroups({groupMap, queryFormats});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// core authz request
|
|
56
|
+
const authorizationRequest = {
|
|
57
|
+
response_type: 'vp_token',
|
|
58
|
+
// default to `direct_post`; caller can override
|
|
59
|
+
response_mode: 'direct_post'
|
|
60
|
+
};
|
|
61
|
+
// include requested authn params
|
|
62
|
+
if(verifiablePresentationRequest.domain) {
|
|
63
|
+
// since a `domain` was provided, set these defaults:
|
|
64
|
+
authorizationRequest.client_id = verifiablePresentationRequest.domain;
|
|
65
|
+
authorizationRequest.response_uri = authorizationRequest.client_id;
|
|
66
|
+
if(useClientIdPrefix) {
|
|
67
|
+
authorizationRequest.client_id =
|
|
68
|
+
`redirect_uri:${authorizationRequest.client_id}`;
|
|
69
|
+
} else {
|
|
70
|
+
authorizationRequest.client_id_scheme = 'redirect_uri';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if(verifiablePresentationRequest.challenge) {
|
|
74
|
+
authorizationRequest.nonce = verifiablePresentationRequest.challenge;
|
|
75
|
+
}
|
|
76
|
+
// only a single `DIDAuthentication` query is supported at this time; use
|
|
77
|
+
// the last one
|
|
78
|
+
const didAuthnQuery = [...groupMap.values()]
|
|
79
|
+
.filter(g => g.has(DID_AUTHENTICATION))
|
|
80
|
+
.map(g => g.get(DID_AUTHENTICATION))
|
|
81
|
+
.at(-1);
|
|
82
|
+
if(didAuthnQuery) {
|
|
83
|
+
const [query] = didAuthnQuery;
|
|
84
|
+
const client_metadata = _fromDIDAuthenticationQuery({query, strict});
|
|
85
|
+
if(client_metadata) {
|
|
86
|
+
authorizationRequest.client_metadata = client_metadata;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// add credential queries
|
|
91
|
+
if(queryFormats?.dcql) {
|
|
92
|
+
const dcql_query = vprGroupsToDcqlQuery({
|
|
93
|
+
groupMap, options: queryFormats.dcql === true ? {} : queryFormats.dcql
|
|
94
|
+
});
|
|
95
|
+
if(dcql_query) {
|
|
96
|
+
authorizationRequest.dcql_query = dcql_query;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if(queryFormats?.presentationExchange) {
|
|
100
|
+
const presentation_definition = vprGroupsToPresentationDefinition({
|
|
101
|
+
groupMap, prefixJwtVcPath
|
|
102
|
+
});
|
|
103
|
+
if(presentation_definition) {
|
|
104
|
+
authorizationRequest.presentation_definition = presentation_definition;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return authorizationRequest;
|
|
109
|
+
} catch(cause) {
|
|
110
|
+
const error = new Error(
|
|
111
|
+
'Could not convert verifiable presentation request to ' +
|
|
112
|
+
'an OID4VP authorization request.', {cause});
|
|
113
|
+
error.name = 'OperationError';
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// converts an OID4VP authorization request (including its
|
|
119
|
+
// "presentation definition") to a VPR
|
|
120
|
+
export function toVpr({authorizationRequest, strict = false} = {}) {
|
|
121
|
+
try {
|
|
122
|
+
// ensure authorization request is valid
|
|
123
|
+
validateAuthorizationRequest({authorizationRequest});
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
client_id,
|
|
127
|
+
client_metadata,
|
|
128
|
+
dcql_query,
|
|
129
|
+
nonce,
|
|
130
|
+
presentation_definition,
|
|
131
|
+
response_uri
|
|
132
|
+
} = authorizationRequest;
|
|
133
|
+
|
|
134
|
+
// disallow unsupported `submission_requirements` in strict mode
|
|
135
|
+
if(strict && presentation_definition.submission_requirements) {
|
|
136
|
+
const error = new Error('"submission_requirements" is not supported.');
|
|
137
|
+
error.name = 'NotSupportedError';
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// generate base VPR from presentation definition
|
|
142
|
+
const verifiablePresentationRequest = {};
|
|
143
|
+
|
|
144
|
+
let didAuthnQuery;
|
|
145
|
+
if(client_metadata) {
|
|
146
|
+
didAuthnQuery = _toDIDAuthenticationQuery({client_metadata, strict});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// prefer conversion from DCQL query over presentation exchange
|
|
150
|
+
let groupMap;
|
|
151
|
+
if(dcql_query) {
|
|
152
|
+
groupMap = dcqlQueryToVprGroups({dcql_query});
|
|
153
|
+
} else if(presentation_definition) {
|
|
154
|
+
groupMap = presentationDefinitionToVprGroups({
|
|
155
|
+
presentation_definition, strict
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if(groupMap?.size > 0) {
|
|
159
|
+
verifiablePresentationRequest.query = _vprGroupsToQuery({groupMap});
|
|
160
|
+
|
|
161
|
+
// clone `DIDAuthentication` query for every query group
|
|
162
|
+
if(didAuthnQuery) {
|
|
163
|
+
for(const groupId of groupMap.keys()) {
|
|
164
|
+
verifiablePresentationRequest.query.push(groupId === undefined ?
|
|
165
|
+
didAuthnQuery : {
|
|
166
|
+
...structuredClone(didAuthnQuery),
|
|
167
|
+
group: groupId
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else if(didAuthnQuery) {
|
|
172
|
+
// add `DIDAuthentication` query once
|
|
173
|
+
verifiablePresentationRequest.query = [didAuthnQuery];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// map `response_uri` or `client_id` to `domain`
|
|
177
|
+
if(response_uri !== undefined || client_id !== undefined) {
|
|
178
|
+
verifiablePresentationRequest.domain = response_uri ?? client_id;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// map `nonce` to `challenge`
|
|
182
|
+
if(nonce !== undefined) {
|
|
183
|
+
verifiablePresentationRequest.challenge = nonce;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {verifiablePresentationRequest};
|
|
187
|
+
} catch(cause) {
|
|
188
|
+
const error = new Error(
|
|
189
|
+
'Could not convert OID4VP authorization request to ' +
|
|
190
|
+
'verifiable presentation request.', {cause});
|
|
191
|
+
error.name = 'OperationError';
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _strictCheckVprGroups({groupMap, queryFormats}) {
|
|
197
|
+
const groups = [...groupMap.values()];
|
|
198
|
+
let didAuthenticationCount = 0;
|
|
199
|
+
let queryByExampleCount = 0;
|
|
200
|
+
let dcqlCount = 0;
|
|
201
|
+
for(const group of groups) {
|
|
202
|
+
for(const type of group.keys()) {
|
|
203
|
+
if(!CONVERTIBLE_QUERY_TYPES.has(type)) {
|
|
204
|
+
const error = new Error(
|
|
205
|
+
'Query type not convertible at this time; supported query types ' +
|
|
206
|
+
`are: ${[...CONVERTIBLE_QUERY_TYPES].join(', ')}`);
|
|
207
|
+
error.name = 'NotSupportedError';
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
if(type === DID_AUTHENTICATION) {
|
|
211
|
+
didAuthenticationCount++;
|
|
212
|
+
} else if(type === QUERY_BY_EXAMPLE) {
|
|
213
|
+
queryByExampleCount++;
|
|
214
|
+
} else if(type === DCQL) {
|
|
215
|
+
dcqlCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// multiple DCQL queries are not supported; there should be a single group ID
|
|
221
|
+
// with a single DCQL query (or none at all)
|
|
222
|
+
if(dcqlCount > 1) {
|
|
223
|
+
const error = new Error(
|
|
224
|
+
`Multiple VPR "${DCQL}" queries are not supported.`);
|
|
225
|
+
error.name = 'NotSupportedError';
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// if presentation exchange output is expected, then only one
|
|
230
|
+
// `QueryByExample` is supported
|
|
231
|
+
if(queryFormats?.presentationExchange && queryByExampleCount > 1) {
|
|
232
|
+
const error = new Error(
|
|
233
|
+
`Multiple VPR "${QUERY_BY_EXAMPLE}" queries are not supported when ` +
|
|
234
|
+
'strictly converting to presentation exchange.');
|
|
235
|
+
error.name = 'NotSupportedError';
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// only one `DIDAuthentication` query is supported
|
|
240
|
+
if(didAuthenticationCount > 1) {
|
|
241
|
+
const error = new Error(
|
|
242
|
+
`Multiple VPR "${DID_AUTHENTICATION}" queries are not supported when ` +
|
|
243
|
+
'strictly converting to an OID4VP Authorization Request.');
|
|
244
|
+
error.name = 'NotSupportedError';
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// there must be at least one convertible type; DCQL is only acceptable
|
|
249
|
+
const convertibleCount = queryByExampleCount + didAuthenticationCount +
|
|
250
|
+
(queryFormats.dcql ? dcqlCount : 0);
|
|
251
|
+
|
|
252
|
+
// there must be at least one convertible type
|
|
253
|
+
if(convertibleCount === 0) {
|
|
254
|
+
const error = new Error(`No convertible query types found.`);
|
|
255
|
+
error.name = 'NotSupportedError';
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _fromDIDAuthenticationQuery({query, strict = false}) {
|
|
261
|
+
const cryptosuites = query.acceptedCryptosuites?.map(
|
|
262
|
+
({cryptosuite}) => cryptosuite);
|
|
263
|
+
if(!(cryptosuites?.length > 0)) {
|
|
264
|
+
if(strict) {
|
|
265
|
+
const error = new Error(
|
|
266
|
+
'"query.acceptedCryptosuites" must be a non-array with specified ' +
|
|
267
|
+
`cryptosuites to convert from a "${DID_AUTHENTICATION}" query.`);
|
|
268
|
+
error.name = 'NotSupportedError';
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const client_metadata = {
|
|
274
|
+
require_signed_request_object: false,
|
|
275
|
+
vp_formats: {
|
|
276
|
+
ldp_vp: {
|
|
277
|
+
proof_type: cryptosuites
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
vp_formats_supported: {
|
|
281
|
+
ldp_vc: {
|
|
282
|
+
proof_type_values: ['DataIntegrityProof']
|
|
283
|
+
},
|
|
284
|
+
cryptosuite_values: cryptosuites
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
// compatibility with legacy cryptosuite
|
|
288
|
+
if(cryptosuites.includes('Ed25519Signature2020')) {
|
|
289
|
+
client_metadata.vp_formats.supported.ldp_vc
|
|
290
|
+
.proof_type_values.push('Ed25519Signature2020');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return client_metadata;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _toDIDAuthenticationQuery({client_metadata, strict = false}) {
|
|
297
|
+
const {vp_formats_supported, vp_formats} = client_metadata;
|
|
298
|
+
const proofTypes = vp_formats_supported?.ldp_vc?.cryptosuite_values ??
|
|
299
|
+
vp_formats?.ldp_vp?.proof_type;
|
|
300
|
+
if(!Array.isArray(proofTypes)) {
|
|
301
|
+
if(strict) {
|
|
302
|
+
const error = new Error(
|
|
303
|
+
'"client_metadata.vp_formats.ldp_vp.proof_type" must be an array to ' +
|
|
304
|
+
`convert to "${DID_AUTHENTICATION}" query.`);
|
|
305
|
+
error.name = 'NotSupportedError';
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
type: DID_AUTHENTICATION,
|
|
312
|
+
acceptedCryptosuites: proofTypes.map(cryptosuite => ({cryptosuite}))
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function _vprGroupsToQuery({groupMap}) {
|
|
317
|
+
const query = [];
|
|
318
|
+
for(const group of groupMap.values()) {
|
|
319
|
+
query.push(...[...group.values()].flat());
|
|
320
|
+
}
|
|
321
|
+
return query;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _vprQueryToGroups({verifiablePresentationRequest}) {
|
|
325
|
+
// normalize queries into groups, each group ID defines a different "OR"
|
|
326
|
+
// condition, the same group ID defines an "AND" group; the group ID
|
|
327
|
+
// `undefined` is used when no group is present (every `undefined` group is
|
|
328
|
+
// the same "AND" group)
|
|
329
|
+
const groups = new Map();
|
|
330
|
+
let {query} = verifiablePresentationRequest;
|
|
331
|
+
if(!Array.isArray(query)) {
|
|
332
|
+
query = [query];
|
|
333
|
+
}
|
|
334
|
+
for(const q of query) {
|
|
335
|
+
// each group is a map of query type => queries
|
|
336
|
+
let group = groups.get(q?.group);
|
|
337
|
+
if(!group) {
|
|
338
|
+
group = new Map();
|
|
339
|
+
groups.set(q?.group, group);
|
|
340
|
+
}
|
|
341
|
+
const queries = group.get(q?.type);
|
|
342
|
+
if(queries) {
|
|
343
|
+
queries.push(q);
|
|
344
|
+
} else {
|
|
345
|
+
group.set(q?.type, [q]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return groups;
|
|
349
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
export * as oid4vp from './oid4vp.js';
|
|
4
|
+
export * as oid4vp from './oid4vp/index.js';
|
|
5
|
+
export * as query from './query/index.js';
|
|
5
6
|
export {
|
|
6
7
|
discoverIssuer,
|
|
7
|
-
|
|
8
|
+
robustDiscoverIssuer
|
|
9
|
+
} from './oid4vci/discovery.js';
|
|
10
|
+
export {
|
|
8
11
|
getCredentialOffer,
|
|
9
|
-
parseCredentialOfferUrl
|
|
10
|
-
|
|
12
|
+
parseCredentialOfferUrl
|
|
13
|
+
} from './oid4vci/credentialOffer.js';
|
|
14
|
+
export {
|
|
11
15
|
signJWT,
|
|
12
16
|
selectJwk
|
|
13
17
|
} from './util.js';
|
|
18
|
+
export {generateDIDProofJWT} from './oid4vci/proofs.js';
|
|
14
19
|
export {OID4Client} from './OID4Client.js';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {assert, fetchJSON} from '../util.js';
|
|
5
|
+
|
|
6
|
+
export function createCredentialRequestsFromOffer({
|
|
7
|
+
issuerConfig, offer, format
|
|
8
|
+
} = {}) {
|
|
9
|
+
// get any supported credential configurations from issuer config
|
|
10
|
+
const supported = _createSupportedCredentialsMap({issuerConfig});
|
|
11
|
+
|
|
12
|
+
// build requests from credentials identified in `offer` and remove any
|
|
13
|
+
// that do not match the given format
|
|
14
|
+
const credentials = offer.credential_configuration_ids ?? offer.credentials;
|
|
15
|
+
const requests = credentials.map(c => {
|
|
16
|
+
if(typeof c === 'string') {
|
|
17
|
+
// use supported credential config
|
|
18
|
+
return _getSupportedCredentialById({id: c, supported});
|
|
19
|
+
}
|
|
20
|
+
return c;
|
|
21
|
+
}).filter(r => r.format === format);
|
|
22
|
+
|
|
23
|
+
if(requests.length === 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`No supported credential(s) with format "${format}" found.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return requests;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getCredentialOffer({url, agent} = {}) {
|
|
32
|
+
const {protocol, searchParams} = new URL(url);
|
|
33
|
+
if(protocol !== 'openid-credential-offer:') {
|
|
34
|
+
throw new SyntaxError(
|
|
35
|
+
'"url" must express a URL with the ' +
|
|
36
|
+
'"openid-credential-offer" protocol.');
|
|
37
|
+
}
|
|
38
|
+
const offer = searchParams.get('credential_offer');
|
|
39
|
+
if(offer) {
|
|
40
|
+
return JSON.parse(offer);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// try to fetch offer from URL
|
|
44
|
+
const offerUrl = searchParams.get('credential_offer_uri');
|
|
45
|
+
if(!offerUrl) {
|
|
46
|
+
throw new SyntaxError(
|
|
47
|
+
'OID4VCI credential offer must have "credential_offer" or ' +
|
|
48
|
+
'"credential_offer_uri".');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if(!offerUrl.startsWith('https://')) {
|
|
52
|
+
const error = new Error(
|
|
53
|
+
`"credential_offer_uri" (${offerUrl}) must start with "https://".`);
|
|
54
|
+
error.name = 'NotSupportedError';
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await fetchJSON({url: offerUrl, agent});
|
|
59
|
+
if(!response.data) {
|
|
60
|
+
const error = new Error(
|
|
61
|
+
`Credential offer fetched from "${offerUrl}" is not JSON.`);
|
|
62
|
+
error.name = 'DataError';
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return response.data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseCredentialOfferUrl({url} = {}) {
|
|
69
|
+
assert(url, 'url', 'string');
|
|
70
|
+
|
|
71
|
+
/* Parse URL, e.g.:
|
|
72
|
+
|
|
73
|
+
'openid-credential-offer://?' +
|
|
74
|
+
'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
|
|
75
|
+
'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
|
|
76
|
+
'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
|
|
77
|
+
'%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
|
|
78
|
+
'%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
79
|
+
'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
80
|
+
'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
|
|
81
|
+
'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
|
|
82
|
+
'%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
|
|
83
|
+
'%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
|
|
84
|
+
'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
|
|
85
|
+
*/
|
|
86
|
+
const {protocol, searchParams} = new URL(url);
|
|
87
|
+
if(protocol !== 'openid-credential-offer:') {
|
|
88
|
+
throw new SyntaxError(
|
|
89
|
+
'"url" must express a URL with the ' +
|
|
90
|
+
'"openid-credential-offer" protocol.');
|
|
91
|
+
}
|
|
92
|
+
return JSON.parse(searchParams.get('credential_offer'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _createSupportedCredentialsMap({issuerConfig}) {
|
|
96
|
+
const {
|
|
97
|
+
credential_configurations_supported,
|
|
98
|
+
credentials_supported
|
|
99
|
+
} = issuerConfig;
|
|
100
|
+
|
|
101
|
+
let supported;
|
|
102
|
+
if(credential_configurations_supported &&
|
|
103
|
+
typeof credential_configurations_supported === 'object') {
|
|
104
|
+
supported = new Map(Object.entries(
|
|
105
|
+
issuerConfig.credential_configurations_supported));
|
|
106
|
+
} else if(Array.isArray(credentials_supported)) {
|
|
107
|
+
// handle legacy `credentials_supported` array
|
|
108
|
+
supported = new Map();
|
|
109
|
+
for(const entry of issuerConfig.credentials_supported) {
|
|
110
|
+
supported.set(entry.id, entry);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// no supported credentials from issuer config
|
|
114
|
+
supported = new Map();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return supported;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _getSupportedCredentialById({id, supported}) {
|
|
121
|
+
const meta = supported.get(id);
|
|
122
|
+
if(!meta) {
|
|
123
|
+
throw new Error(`No supported credential "${id}" found.`);
|
|
124
|
+
}
|
|
125
|
+
const {format, credential_definition} = meta;
|
|
126
|
+
if(typeof format !== 'string') {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid supported credential "${id}"; "format" not specified.`);
|
|
129
|
+
}
|
|
130
|
+
if(!(Array.isArray(credential_definition?.['@context']) &&
|
|
131
|
+
(Array.isArray(credential_definition?.types) ||
|
|
132
|
+
Array.isArray(credential_definition?.type)))) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Invalid supported credential "${id}"; "credential_definition" not ` +
|
|
135
|
+
'fully specified.');
|
|
136
|
+
}
|
|
137
|
+
return {format, credential_definition};
|
|
138
|
+
}
|