@digitalbazaar/oid4-client 5.9.0 → 5.10.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 +467 -286
- package/lib/convert/index.js +16 -5
- package/lib/oid4vci/credentialOffer.js +102 -36
- package/lib/util.js +1 -1
- package/package.json +1 -1
package/lib/OID4Client.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createAuthorizationDetailsFromOffer, createCredentialRequestsFromOffer
|
|
6
|
+
} from './oid4vci/credentialOffer.js';
|
|
7
|
+
import {createNamedError} from './util.js';
|
|
5
8
|
import {generateDIDProofJWT} from './oid4vci/proofs.js';
|
|
6
9
|
import {httpClient} from '@digitalbazaar/http-client';
|
|
7
10
|
import {robustDiscoverIssuer} from './oid4vci/discovery.js';
|
|
@@ -12,12 +15,23 @@ const GRANT_TYPES = new Map([
|
|
|
12
15
|
const HEADERS = {accept: 'application/json'};
|
|
13
16
|
|
|
14
17
|
export class OID4Client {
|
|
15
|
-
constructor({
|
|
18
|
+
constructor({
|
|
19
|
+
accessToken = null, agent, authorizationDetails,
|
|
20
|
+
issuerConfig, metadata, offer,
|
|
21
|
+
// default to "compatible" versions where the client will attempt to
|
|
22
|
+
// detect and work with whatever version the server supports, for the
|
|
23
|
+
// versions presently supported by this library
|
|
24
|
+
oid4vciVersion = 'detect',
|
|
25
|
+
oid4vpVersion = 'detect'
|
|
26
|
+
} = {}) {
|
|
16
27
|
this.accessToken = accessToken;
|
|
17
28
|
this.agent = agent;
|
|
29
|
+
this.authorizationDetails = authorizationDetails;
|
|
18
30
|
this.metadata = metadata;
|
|
19
31
|
this.issuerConfig = issuerConfig;
|
|
20
32
|
this.offer = offer;
|
|
33
|
+
this.oid4vciVersion = oid4vciVersion;
|
|
34
|
+
this.oid4vpVersion = oid4vpVersion;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
async getNonce({agent, headers = HEADERS} = {}) {
|
|
@@ -26,57 +40,59 @@ export class OID4Client {
|
|
|
26
40
|
// get nonce endpoint
|
|
27
41
|
const {nonce_endpoint: url} = this.issuerConfig;
|
|
28
42
|
if(url === undefined) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
throw createNamedError({
|
|
44
|
+
message: 'Credential issuer has no "nonce_endpoint".',
|
|
45
|
+
name: 'DataError'
|
|
46
|
+
});
|
|
32
47
|
}
|
|
33
48
|
if(!url?.startsWith('https://')) {
|
|
34
|
-
|
|
35
|
-
`Nonce endpoint "${url}" does not start with "https://"
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
throw createNamedError({
|
|
50
|
+
message: `Nonce endpoint "${url}" does not start with "https://".`,
|
|
51
|
+
name: 'DataError'
|
|
52
|
+
});
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
// get nonce
|
|
41
56
|
response = await httpClient.post(url, {agent, headers});
|
|
42
57
|
if(!response.data) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
throw createNamedError({
|
|
59
|
+
message: 'Nonce response format is not JSON.',
|
|
60
|
+
name: 'DataError'
|
|
61
|
+
});
|
|
46
62
|
}
|
|
47
63
|
if(response.data.c_nonce === undefined) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
throw createNamedError({
|
|
65
|
+
message: 'Nonce not provided in response.',
|
|
66
|
+
name: 'DataError'
|
|
67
|
+
});
|
|
51
68
|
}
|
|
52
69
|
} catch(cause) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
throw createNamedError({
|
|
71
|
+
message: 'Could not get nonce.',
|
|
72
|
+
name: 'DataError',
|
|
73
|
+
cause
|
|
74
|
+
});
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
const {c_nonce: nonce} = response.data;
|
|
59
78
|
return {nonce, response};
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
// deprecated; always call `requestCredentials()` instead
|
|
62
82
|
async requestCredential({
|
|
63
83
|
credentialDefinition, did, didProofSigner, nonce, agent, format = 'ldp_vc'
|
|
64
84
|
} = {}) {
|
|
65
|
-
const {issuerConfig, offer} = this;
|
|
85
|
+
const {authorizationDetails, issuerConfig, offer, oid4vciVersion} = this;
|
|
66
86
|
let requests;
|
|
67
87
|
if(credentialDefinition === undefined) {
|
|
68
88
|
if(!offer) {
|
|
69
|
-
throw new TypeError('"
|
|
89
|
+
throw new TypeError('"offer" must be an object.');
|
|
70
90
|
}
|
|
71
91
|
requests = createCredentialRequestsFromOffer({
|
|
72
|
-
issuerConfig, offer, format
|
|
92
|
+
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
|
|
73
93
|
});
|
|
74
|
-
if(requests.length > 1) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
'More than one credential is offered; ' +
|
|
77
|
-
'use "requestCredentials()" instead.');
|
|
78
|
-
}
|
|
79
94
|
} else {
|
|
95
|
+
// OID4VCI Draft 13 only
|
|
80
96
|
requests = [{
|
|
81
97
|
format,
|
|
82
98
|
credential_definition: credentialDefinition
|
|
@@ -93,292 +109,124 @@ export class OID4Client {
|
|
|
93
109
|
} = {}) {
|
|
94
110
|
// if `nonce` is given, then `did` and `didProofSigner` must also be
|
|
95
111
|
if(nonce !== undefined && !(did && didProofSigner)) {
|
|
96
|
-
throw
|
|
97
|
-
|
|
112
|
+
throw createNamedError({
|
|
113
|
+
message:
|
|
114
|
+
'If "nonce" is given then "did" and "didProofSigner" are required.',
|
|
115
|
+
name: 'DataError'
|
|
116
|
+
});
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
const {issuerConfig, offer} = this;
|
|
119
|
+
const {authorizationDetails, issuerConfig, offer, oid4vciVersion} = this;
|
|
101
120
|
if(requests === undefined && offer) {
|
|
102
121
|
requests = createCredentialRequestsFromOffer({
|
|
103
|
-
issuerConfig, offer, format
|
|
122
|
+
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
|
|
104
123
|
});
|
|
105
124
|
} else if(!(Array.isArray(requests) && requests.length > 0)) {
|
|
106
125
|
throw new TypeError('"requests" must be an array of length >= 1.');
|
|
107
126
|
}
|
|
108
127
|
requests.forEach(_assertRequest);
|
|
109
|
-
// set default `format`
|
|
110
|
-
requests = requests.map(r => ({format, ...r}));
|
|
111
128
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`nonce` is given) e.g.:
|
|
115
|
-
|
|
116
|
-
POST /credential HTTP/1.1
|
|
117
|
-
Host: server.example.com
|
|
118
|
-
Content-Type: application/json
|
|
119
|
-
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
"format": "ldp_vc",
|
|
123
|
-
"credential_definition": {...},
|
|
124
|
-
// only present on retry after server requests it or if nonce is given
|
|
125
|
-
"proof": {
|
|
126
|
-
"proof_type": "jwt",
|
|
127
|
-
"jwt": "eyJraW..."
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
OR (if multiple `requests` were given)
|
|
132
|
-
|
|
133
|
-
POST /batch_credential HTTP/1.1
|
|
134
|
-
Host: server.example.com
|
|
135
|
-
Content-Type: application/json
|
|
136
|
-
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
"credential_requests": [{
|
|
140
|
-
"format": "ldp_vc",
|
|
141
|
-
"credential_definition": {...},
|
|
142
|
-
// only present on retry after server requests it
|
|
143
|
-
"proof": {
|
|
144
|
-
"proof_type": "jwt",
|
|
145
|
-
"jwt": "eyJraW..."
|
|
146
|
-
}
|
|
147
|
-
}, {
|
|
148
|
-
...
|
|
149
|
-
}]
|
|
150
|
-
}
|
|
151
|
-
*/
|
|
152
|
-
let url;
|
|
153
|
-
let json;
|
|
154
|
-
if(requests.length > 1 || alwaysUseBatchEndpoint) {
|
|
155
|
-
({batch_credential_endpoint: url} = this.issuerConfig);
|
|
156
|
-
json = {credential_requests: requests};
|
|
157
|
-
} else {
|
|
158
|
-
({credential_endpoint: url} = this.issuerConfig);
|
|
159
|
-
json = {...requests[0]};
|
|
160
|
-
}
|
|
129
|
+
// determine if OID4VCI 1.0+ is to be used
|
|
130
|
+
const version1Plus = requests.some(r => r.credential_identifier);
|
|
161
131
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
132
|
+
if(!version1Plus) {
|
|
133
|
+
// set default `format` for requests with `credential_definition`
|
|
134
|
+
// (OID4VCI Draft 13 only)
|
|
135
|
+
requests = requests.map(
|
|
136
|
+
r => r.credential_definition ? {format, ...r} : r);
|
|
137
|
+
}
|
|
166
138
|
|
|
139
|
+
try {
|
|
167
140
|
let result;
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
break;
|
|
182
|
-
} catch(cause) {
|
|
183
|
-
// presentation is required to continue issuance
|
|
184
|
-
if(_isPresentationRequired(cause)) {
|
|
185
|
-
const {data: details} = cause;
|
|
186
|
-
const error = new Error('Presentation is required.', {cause});
|
|
187
|
-
error.name = 'NotAllowedError';
|
|
188
|
-
error.details = details;
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if(!_isMissingProofError(cause)) {
|
|
193
|
-
// other non-specific error case
|
|
194
|
-
throw cause;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// if `didProofSigner` is not provided, throw error
|
|
198
|
-
if(!(did && didProofSigner)) {
|
|
199
|
-
const {data: details} = cause;
|
|
200
|
-
const error = new Error('DID authentication is required.', {cause});
|
|
201
|
-
error.name = 'NotAllowedError';
|
|
202
|
-
error.details = details;
|
|
203
|
-
throw error;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// validate that `result` has
|
|
207
|
-
let {data: {c_nonce: nonce}} = cause;
|
|
208
|
-
if(!(nonce && typeof nonce === 'string')) {
|
|
209
|
-
// try to get a nonce
|
|
210
|
-
({nonce} = await this.getNonce({agent}));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// add DID proof JWT to json
|
|
214
|
-
await _addDIDProofJWT({
|
|
215
|
-
issuerConfig, json, nonce, did, didProofSigner
|
|
141
|
+
const {accessToken} = this;
|
|
142
|
+
if(version1Plus) {
|
|
143
|
+
// OID4VCI 1.0+ ... make N-many requests in parallel, with each one
|
|
144
|
+
// adding a DID proof, if requested and N-many nonces might be required
|
|
145
|
+
// FIXME: use p-queue to manage work
|
|
146
|
+
const {credential_endpoint: url} = issuerConfig;
|
|
147
|
+
const results = await Promise.all(requests.map(async request => {
|
|
148
|
+
const json = {...request};
|
|
149
|
+
return _requestCredential({
|
|
150
|
+
accessToken, issuerConfig,
|
|
151
|
+
url, json, nonce, did, didProofSigner, agent
|
|
216
152
|
});
|
|
153
|
+
}));
|
|
154
|
+
// for backwards compatibility, return all results independently,
|
|
155
|
+
// combined, and singular (if applicable)
|
|
156
|
+
const credentials = results
|
|
157
|
+
.map(r => r?.credentials?.map(e => e.credential))
|
|
158
|
+
.flat();
|
|
159
|
+
result = {credential_responses: results, credentials};
|
|
160
|
+
// backwards compatibility with common draft 13 credential calls
|
|
161
|
+
if(credentials.length === 1) {
|
|
162
|
+
result.credential = credentials[0];
|
|
217
163
|
}
|
|
164
|
+
if(credentials.every(c => c?.['@context'])) {
|
|
165
|
+
result.format = 'ldp_vc';
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// draft 13...
|
|
169
|
+
let url;
|
|
170
|
+
let json;
|
|
171
|
+
if(requests.length > 1 || alwaysUseBatchEndpoint) {
|
|
172
|
+
({batch_credential_endpoint: url} = issuerConfig);
|
|
173
|
+
json = {credential_requests: requests};
|
|
174
|
+
} else {
|
|
175
|
+
({credential_endpoint: url} = issuerConfig);
|
|
176
|
+
json = {...requests[0]};
|
|
177
|
+
}
|
|
178
|
+
result = await _requestCredential({
|
|
179
|
+
accessToken, issuerConfig,
|
|
180
|
+
url, json, nonce, did, didProofSigner, agent
|
|
181
|
+
});
|
|
218
182
|
}
|
|
219
|
-
|
|
220
|
-
// wallet / client receives credential(s):
|
|
221
|
-
/* Note: The credential is not wrapped here in a VP in the current spec:
|
|
222
|
-
|
|
223
|
-
HTTP/1.1 200 OK
|
|
224
|
-
Content-Type: application/json
|
|
225
|
-
Cache-Control: no-store
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
"format": "ldp_vc",
|
|
229
|
-
"credential" : {...}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
OR (if multiple VCs *of the same type* were issued)
|
|
233
|
-
|
|
234
|
-
{
|
|
235
|
-
"format": "ldp_vc",
|
|
236
|
-
"credentials" : {...}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
OR (if multiple `requests` were given)
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
"credential_responses": [{
|
|
243
|
-
"format": "ldp_vc",
|
|
244
|
-
"credential": {...}
|
|
245
|
-
}]
|
|
246
|
-
}
|
|
247
|
-
*/
|
|
248
183
|
return result;
|
|
249
184
|
} catch(cause) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
185
|
+
throw createNamedError({
|
|
186
|
+
message: 'Could not receive credentials.',
|
|
187
|
+
name: 'OperationError',
|
|
188
|
+
cause
|
|
189
|
+
});
|
|
253
190
|
}
|
|
254
191
|
}
|
|
255
192
|
|
|
256
193
|
// create a client from a credential offer
|
|
257
|
-
static async fromCredentialOffer({
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
} = offer;
|
|
265
|
-
let parsedIssuer;
|
|
266
|
-
try {
|
|
267
|
-
parsedIssuer = new URL(credential_issuer);
|
|
268
|
-
if(parsedIssuer.protocol !== 'https:') {
|
|
269
|
-
throw new Error('Only "https" credential issuer URLs are supported.');
|
|
270
|
-
}
|
|
271
|
-
} catch(cause) {
|
|
272
|
-
throw new Error('"offer.credential_issuer" is not valid.', {cause});
|
|
273
|
-
}
|
|
274
|
-
if(credentials === undefined &&
|
|
275
|
-
credential_configuration_ids === undefined) {
|
|
276
|
-
throw new Error(
|
|
277
|
-
'Either "offer.credential_configuration_ids" or ' +
|
|
278
|
-
'"offer.credentials" is required.');
|
|
279
|
-
}
|
|
280
|
-
if(credential_configuration_ids !== undefined &&
|
|
281
|
-
!Array.isArray(credential_configuration_ids)) {
|
|
282
|
-
throw new Error('"offer.credential_configuration_ids" is not valid.');
|
|
283
|
-
}
|
|
284
|
-
if(credentials !== undefined &&
|
|
285
|
-
!(Array.isArray(credentials) && credentials.length > 0 &&
|
|
286
|
-
credentials.every(c => c && (
|
|
287
|
-
typeof c === 'object' || typeof c === 'string')))) {
|
|
288
|
-
throw new Error('"offer.credentials" is not valid.');
|
|
289
|
-
}
|
|
290
|
-
const grant = grants[GRANT_TYPES.get('preAuthorizedCode')];
|
|
291
|
-
if(!grant) {
|
|
292
|
-
// FIXME: implement `authorization_code` grant type as well
|
|
293
|
-
throw new Error('Only "pre-authorized_code" grant type is implemented.');
|
|
294
|
-
}
|
|
295
|
-
const {
|
|
296
|
-
'pre-authorized_code': preAuthorizedCode,
|
|
297
|
-
// FIXME: update to `tx_code` terminology
|
|
298
|
-
user_pin_required: userPinRequired
|
|
299
|
-
} = grant;
|
|
300
|
-
if(!preAuthorizedCode) {
|
|
301
|
-
throw new Error('"offer.grant" is missing "pre-authorized_code".');
|
|
302
|
-
}
|
|
303
|
-
if(userPinRequired) {
|
|
304
|
-
throw new Error('User pin is not implemented.');
|
|
305
|
-
}
|
|
194
|
+
static async fromCredentialOffer({
|
|
195
|
+
offer, supportedFormats = ['ldp_vc'],
|
|
196
|
+
oid4vciVersion = 'detect', oid4vpVersion = 'detect',
|
|
197
|
+
agent
|
|
198
|
+
} = {}) {
|
|
199
|
+
// parse offer
|
|
200
|
+
const {issuer, preAuthorizedCode} = _parseOffer({offer});
|
|
306
201
|
|
|
307
202
|
try {
|
|
308
203
|
// discover issuer info
|
|
309
204
|
const {issuerConfig, metadata} = await robustDiscoverIssuer({
|
|
310
|
-
issuer
|
|
205
|
+
issuer, agent
|
|
311
206
|
});
|
|
312
207
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
Note a bad response would look like:
|
|
323
|
-
|
|
324
|
-
/*
|
|
325
|
-
HTTP/1.1 400 Bad Request
|
|
326
|
-
Content-Type: application/json
|
|
327
|
-
Cache-Control: no-store
|
|
328
|
-
{
|
|
329
|
-
"error": "invalid_request"
|
|
330
|
-
}
|
|
331
|
-
*/
|
|
332
|
-
const body = new URLSearchParams();
|
|
333
|
-
body.set('grant_type', GRANT_TYPES.get('preAuthorizedCode'));
|
|
334
|
-
body.set('pre-authorized_code', preAuthorizedCode);
|
|
335
|
-
const {token_endpoint} = issuerConfig;
|
|
336
|
-
const response = await httpClient.post(token_endpoint, {
|
|
337
|
-
agent, body, headers: HEADERS
|
|
208
|
+
// get access token from AS (Authorization Server)
|
|
209
|
+
const {accessToken, authorizationDetails} = await _getAccessToken({
|
|
210
|
+
issuerConfig, preAuthorizedCode,
|
|
211
|
+
// request authz details if the server supports it
|
|
212
|
+
authorizationDetails: createAuthorizationDetailsFromOffer({
|
|
213
|
+
issuerConfig, offer, supportedFormats, oid4vciVersion
|
|
214
|
+
}),
|
|
215
|
+
agent
|
|
338
216
|
});
|
|
339
|
-
const {data: result} = response;
|
|
340
|
-
if(!result) {
|
|
341
|
-
const error = new Error(
|
|
342
|
-
'Could not get access token; response is not JSON.');
|
|
343
|
-
error.name = 'DataError';
|
|
344
|
-
throw error;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/* Validate response body (Note: Do not check or use `c_nonce*` here
|
|
348
|
-
because it conflates AS with DS (Delivery Server)), e.g.:
|
|
349
|
-
|
|
350
|
-
HTTP/1.1 200 OK
|
|
351
|
-
Content-Type: application/json
|
|
352
|
-
Cache-Control: no-store
|
|
353
|
-
|
|
354
|
-
{
|
|
355
|
-
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ",
|
|
356
|
-
"token_type": "bearer",
|
|
357
|
-
"expires_in": 86400
|
|
358
|
-
}
|
|
359
|
-
*/
|
|
360
|
-
const {access_token: accessToken, token_type} = result;
|
|
361
|
-
if(!(accessToken && typeof accessToken === 'string')) {
|
|
362
|
-
const error = new Error(
|
|
363
|
-
'Invalid access token response; "access_token" must be a string.');
|
|
364
|
-
error.name = 'DataError';
|
|
365
|
-
throw error;
|
|
366
|
-
}
|
|
367
|
-
if(token_type !== 'bearer') {
|
|
368
|
-
const error = new Error(
|
|
369
|
-
'Invalid access token response; "token_type" must be a "bearer".');
|
|
370
|
-
error.name = 'DataError';
|
|
371
|
-
throw error;
|
|
372
|
-
}
|
|
373
217
|
|
|
374
218
|
// create client w/access token
|
|
375
219
|
return new OID4Client({
|
|
376
|
-
accessToken, agent,
|
|
220
|
+
accessToken, agent, authorizationDetails,
|
|
221
|
+
issuerConfig, metadata, offer,
|
|
222
|
+
oid4vciVersion, oid4vpVersion
|
|
377
223
|
});
|
|
378
224
|
} catch(cause) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
225
|
+
throw createNamedError({
|
|
226
|
+
message: 'Could not create OID4 client.',
|
|
227
|
+
name: 'OperationError',
|
|
228
|
+
cause
|
|
229
|
+
});
|
|
382
230
|
}
|
|
383
231
|
}
|
|
384
232
|
}
|
|
@@ -400,33 +248,289 @@ async function _addDIDProofJWT({
|
|
|
400
248
|
// add proof to body to be posted and loop to retry
|
|
401
249
|
const proof = {proof_type: 'jwt', jwt};
|
|
402
250
|
if(json.credential_requests) {
|
|
251
|
+
// OID4VCI Draft 13 only
|
|
403
252
|
json.credential_requests = json.credential_requests.map(
|
|
404
253
|
cr => ({...cr, proof}));
|
|
405
|
-
} else {
|
|
254
|
+
} else if(json.credential_definition) {
|
|
255
|
+
// OID4VCI Draft 13 only
|
|
406
256
|
json.proof = proof;
|
|
257
|
+
} else {
|
|
258
|
+
// OID4VCI 1.0+
|
|
259
|
+
json.proofs = {
|
|
260
|
+
...proof,
|
|
261
|
+
jwt: [proof.jwt]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function _requestCredential({
|
|
267
|
+
accessToken, issuerConfig, url, json, nonce, did, didProofSigner, agent
|
|
268
|
+
}) {
|
|
269
|
+
/* First send credential request(s) to DS without DID proof JWT (unless
|
|
270
|
+
`nonce` is given) e.g.:
|
|
271
|
+
|
|
272
|
+
POST /credential HTTP/1.1
|
|
273
|
+
Host: server.example.com
|
|
274
|
+
Content-Type: application/json
|
|
275
|
+
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
276
|
+
|
|
277
|
+
{
|
|
278
|
+
"format": "ldp_vc",
|
|
279
|
+
"credential_definition": {...},
|
|
280
|
+
// only present on retry after server requests it or if nonce is given
|
|
281
|
+
"proof": {
|
|
282
|
+
"proof_type": "jwt",
|
|
283
|
+
"jwt": "eyJraW..."
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
OR
|
|
287
|
+
{
|
|
288
|
+
"credential_identifier": "foo",
|
|
289
|
+
...
|
|
290
|
+
}
|
|
291
|
+
OR
|
|
292
|
+
{
|
|
293
|
+
"credential_configuration_id": "bar",
|
|
294
|
+
...
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
OR (if multiple `requests` were given w/ Draft 13)
|
|
298
|
+
|
|
299
|
+
POST /batch_credential HTTP/1.1
|
|
300
|
+
Host: server.example.com
|
|
301
|
+
Content-Type: application/json
|
|
302
|
+
Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
|
303
|
+
|
|
304
|
+
{
|
|
305
|
+
"credential_requests": [{
|
|
306
|
+
"format": "ldp_vc",
|
|
307
|
+
"credential_definition": {...},
|
|
308
|
+
// only present on retry after server requests it
|
|
309
|
+
"proof": {
|
|
310
|
+
"proof_type": "jwt",
|
|
311
|
+
"jwt": "eyJraW..."
|
|
312
|
+
}
|
|
313
|
+
}, {
|
|
314
|
+
...
|
|
315
|
+
}]
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
OR (if multiple `requests` were given w/1.0+), N-many requests will be
|
|
319
|
+
repeated to `/credential`
|
|
320
|
+
|
|
321
|
+
*/
|
|
322
|
+
if(nonce !== undefined) {
|
|
323
|
+
// add DID proof JWT to json
|
|
324
|
+
await _addDIDProofJWT({issuerConfig, json, nonce, did, didProofSigner});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let result;
|
|
328
|
+
const headers = {
|
|
329
|
+
...HEADERS,
|
|
330
|
+
authorization: `Bearer ${accessToken}`
|
|
331
|
+
};
|
|
332
|
+
for(let retries = 0; retries <= 1; ++retries) {
|
|
333
|
+
try {
|
|
334
|
+
const response = await httpClient.post(url, {agent, headers, json});
|
|
335
|
+
result = response.data;
|
|
336
|
+
if(!result) {
|
|
337
|
+
throw createNamedError({
|
|
338
|
+
message: 'Credential response format is not JSON.',
|
|
339
|
+
name: 'DataError'
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
} catch(cause) {
|
|
344
|
+
// presentation is required to continue issuance
|
|
345
|
+
if(_isPresentationRequired(cause)) {
|
|
346
|
+
throw createNamedError({
|
|
347
|
+
message: 'Presentation is required.',
|
|
348
|
+
name: 'NotAllowedError',
|
|
349
|
+
cause,
|
|
350
|
+
details: cause.data
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if(!_isMissingProofError(cause)) {
|
|
355
|
+
// other non-specific error case
|
|
356
|
+
throw cause;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// if `didProofSigner` is not provided, throw error
|
|
360
|
+
if(!(did && didProofSigner)) {
|
|
361
|
+
throw createNamedError({
|
|
362
|
+
message: 'DID authentication is required.',
|
|
363
|
+
name: 'NotAllowedError',
|
|
364
|
+
cause,
|
|
365
|
+
details: cause.data
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// validate that `result` has a nonce
|
|
370
|
+
let {data: {c_nonce: nonce}} = cause;
|
|
371
|
+
if(!(nonce && typeof nonce === 'string')) {
|
|
372
|
+
// try to get a nonce
|
|
373
|
+
({nonce} = await this.getNonce({agent}));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// add DID proof JWT to json
|
|
377
|
+
await _addDIDProofJWT({
|
|
378
|
+
issuerConfig, json, nonce, did, didProofSigner
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// wallet / client receives credential(s):
|
|
384
|
+
/* Note: The credential is not wrapped here in a VP in the current spec:
|
|
385
|
+
|
|
386
|
+
HTTP/1.1 200 OK
|
|
387
|
+
Content-Type: application/json
|
|
388
|
+
Cache-Control: no-store
|
|
389
|
+
|
|
390
|
+
{
|
|
391
|
+
"format": "ldp_vc",
|
|
392
|
+
"credential" : {...}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
OR (if multiple VCs *of the same type* were issued)
|
|
396
|
+
|
|
397
|
+
{
|
|
398
|
+
"format": "ldp_vc",
|
|
399
|
+
"credentials" : {...}
|
|
407
400
|
}
|
|
401
|
+
|
|
402
|
+
OR (if multiple `requests` were given)
|
|
403
|
+
|
|
404
|
+
{
|
|
405
|
+
"credential_responses": [{
|
|
406
|
+
"format": "ldp_vc",
|
|
407
|
+
"credential": {...}
|
|
408
|
+
}]
|
|
409
|
+
}
|
|
410
|
+
*/
|
|
411
|
+
return result;
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
function _assertRequest(request) {
|
|
415
|
+
// all current versions of OID4VCI require `request` to be an object
|
|
411
416
|
if(!(request && typeof request === 'object')) {
|
|
412
417
|
throw new TypeError('"request" must be an object.');
|
|
413
418
|
}
|
|
419
|
+
|
|
420
|
+
// OID4VCI 1.0+ request format
|
|
421
|
+
if(request.credential_configuration_id) {
|
|
422
|
+
if(typeof request.credential_configuration_id !== 'string') {
|
|
423
|
+
throw new TypeError(
|
|
424
|
+
'Credential request "credential_configuration_id" must be a string.');
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// OID4VCI 1.0+ request format
|
|
430
|
+
if(request.credential_identifier) {
|
|
431
|
+
if(typeof request.credential_identifier !== 'string') {
|
|
432
|
+
throw new TypeError(
|
|
433
|
+
'Credential request "credential_identifier" must be a string.');
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// OID4VCI Draft 13 format
|
|
414
439
|
const {credential_definition} = request;
|
|
415
440
|
if(!(credential_definition && typeof credential_definition === 'object')) {
|
|
416
441
|
throw new TypeError(
|
|
417
442
|
'Credential request "credential_definition" must be an object.');
|
|
418
443
|
}
|
|
419
|
-
const {
|
|
420
|
-
if(!(Array.isArray(context) && context.length > 0)) {
|
|
421
|
-
throw new TypeError(
|
|
422
|
-
'Credential definition "@context" must be an array of length >= 1.');
|
|
423
|
-
}
|
|
444
|
+
const {type: type} = credential_definition;
|
|
424
445
|
if(!(Array.isArray(type) && type.length > 0)) {
|
|
425
446
|
throw new TypeError(
|
|
426
|
-
'Credential definition "type" must be an array of length
|
|
447
|
+
'Credential definition "type" must be an array of length > 0.');
|
|
427
448
|
}
|
|
428
449
|
}
|
|
429
450
|
|
|
451
|
+
async function _getAccessToken({
|
|
452
|
+
issuerConfig, preAuthorizedCode, authorizationDetails, agent
|
|
453
|
+
}) {
|
|
454
|
+
/* First get access token from AS (Authorization Server), e.g.:
|
|
455
|
+
|
|
456
|
+
POST /token HTTP/1.1
|
|
457
|
+
Host: server.example.com
|
|
458
|
+
Content-Type: application/x-www-form-urlencoded
|
|
459
|
+
grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
|
|
460
|
+
&pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
|
|
461
|
+
&user_pin=493536
|
|
462
|
+
&authorization_details=<URI-component-encoded JSON array>
|
|
463
|
+
|
|
464
|
+
Note a bad response would look like:
|
|
465
|
+
|
|
466
|
+
/*
|
|
467
|
+
HTTP/1.1 400 Bad Request
|
|
468
|
+
Content-Type: application/json
|
|
469
|
+
Cache-Control: no-store
|
|
470
|
+
{
|
|
471
|
+
"error": "invalid_request"
|
|
472
|
+
}
|
|
473
|
+
*/
|
|
474
|
+
const body = new URLSearchParams();
|
|
475
|
+
body.set('grant_type', GRANT_TYPES.get('preAuthorizedCode'));
|
|
476
|
+
body.set('pre-authorized_code', preAuthorizedCode);
|
|
477
|
+
if(authorizationDetails) {
|
|
478
|
+
body.set('authorization_details', JSON.stringify(authorizationDetails));
|
|
479
|
+
}
|
|
480
|
+
const {token_endpoint} = issuerConfig;
|
|
481
|
+
const response = await httpClient.post(token_endpoint, {
|
|
482
|
+
agent, body, headers: HEADERS
|
|
483
|
+
});
|
|
484
|
+
const {data: result} = response;
|
|
485
|
+
if(!result) {
|
|
486
|
+
throw createNamedError({
|
|
487
|
+
message: 'Could not get access token; response is not JSON.',
|
|
488
|
+
name: 'DataError'
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* Validate response body (Note: Do not check or use `c_nonce*` here
|
|
493
|
+
because it conflates AS with DS (Delivery Server)), e.g.:
|
|
494
|
+
|
|
495
|
+
HTTP/1.1 200 OK
|
|
496
|
+
Content-Type: application/json
|
|
497
|
+
Cache-Control: no-store
|
|
498
|
+
|
|
499
|
+
{
|
|
500
|
+
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ",
|
|
501
|
+
"token_type": "bearer",
|
|
502
|
+
"expires_in": 86400
|
|
503
|
+
}
|
|
504
|
+
*/
|
|
505
|
+
const {access_token: accessToken, token_type} = result;
|
|
506
|
+
({authorization_details: authorizationDetails} = result);
|
|
507
|
+
if(!(accessToken && typeof accessToken === 'string')) {
|
|
508
|
+
throw createNamedError({
|
|
509
|
+
message:
|
|
510
|
+
'Invalid access token response; "access_token" must be a string.',
|
|
511
|
+
name: 'DataError'
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if(token_type !== 'bearer') {
|
|
515
|
+
throw createNamedError({
|
|
516
|
+
message:
|
|
517
|
+
'Invalid access token response; "token_type" must be a "bearer".',
|
|
518
|
+
name: 'DataError'
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if(authorizationDetails !== undefined &&
|
|
522
|
+
!Array.isArray(authorizationDetails)) {
|
|
523
|
+
throw createNamedError({
|
|
524
|
+
message:
|
|
525
|
+
'Invalid access token response; ' +
|
|
526
|
+
'"authorization_details" must be an array.',
|
|
527
|
+
name: 'DataError'
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {accessToken, authorizationDetails};
|
|
532
|
+
}
|
|
533
|
+
|
|
430
534
|
function _isMissingProofError(error) {
|
|
431
535
|
/* If DID authn is required, delivery server sends, e.g.:
|
|
432
536
|
|
|
@@ -466,3 +570,80 @@ function _isPresentationRequired(error) {
|
|
|
466
570
|
const errorType = error.data?.error;
|
|
467
571
|
return error.status === 400 && errorType === 'presentation_required';
|
|
468
572
|
}
|
|
573
|
+
|
|
574
|
+
function _parseOffer({offer}) {
|
|
575
|
+
// relevant fields to validate
|
|
576
|
+
const {
|
|
577
|
+
credential_issuer,
|
|
578
|
+
credentials,
|
|
579
|
+
credential_configuration_ids,
|
|
580
|
+
grants = {}
|
|
581
|
+
} = offer;
|
|
582
|
+
|
|
583
|
+
// ensure issuer is a valid URL
|
|
584
|
+
let parsedIssuer;
|
|
585
|
+
try {
|
|
586
|
+
parsedIssuer = new URL(credential_issuer);
|
|
587
|
+
if(parsedIssuer.protocol !== 'https:') {
|
|
588
|
+
throw createNamedError({
|
|
589
|
+
message: 'Only "https" credential issuer URLs are supported.',
|
|
590
|
+
name: 'NotSupportedError'
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
} catch(cause) {
|
|
594
|
+
throw createNamedError({
|
|
595
|
+
message: '"offer.credential_issuer" is not valid.',
|
|
596
|
+
name: 'DataError',
|
|
597
|
+
cause
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// OID4VCI Draft 13 used `credentials`
|
|
602
|
+
// 1.0+ uses `credential_configuration_ids`
|
|
603
|
+
if(credentials === undefined && credential_configuration_ids === undefined) {
|
|
604
|
+
throw createNamedError({
|
|
605
|
+
message:
|
|
606
|
+
'Either "offer.credential_configuration_ids" or ' +
|
|
607
|
+
'"offer.credentials" is required.',
|
|
608
|
+
name: 'DataError'
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if(credential_configuration_ids !== undefined &&
|
|
612
|
+
!Array.isArray(credential_configuration_ids)) {
|
|
613
|
+
throw createNamedError({
|
|
614
|
+
message: '"offer.credential_configuration_ids" is not valid.',
|
|
615
|
+
name: 'DataError'
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if(credentials !== undefined &&
|
|
619
|
+
!(Array.isArray(credentials) && credentials.length > 0 &&
|
|
620
|
+
credentials.every(c => c && (
|
|
621
|
+
typeof c === 'object' || typeof c === 'string')))) {
|
|
622
|
+
throw createNamedError({
|
|
623
|
+
message: '"offer.credentials" is not valid.',
|
|
624
|
+
name: 'DataError'
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// validate grant
|
|
629
|
+
const grant = grants?.[GRANT_TYPES.get('preAuthorizedCode')];
|
|
630
|
+
if(!grant) {
|
|
631
|
+
throw createNamedError({
|
|
632
|
+
message: 'Only "pre-authorized_code" grant type is supported.',
|
|
633
|
+
name: 'NotSupportedError'
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const {
|
|
637
|
+
'pre-authorized_code': preAuthorizedCode
|
|
638
|
+
// note: `tx_code` is presently ignored/not supported; if required an
|
|
639
|
+
// error will be thrown by the appropriate software
|
|
640
|
+
} = grant;
|
|
641
|
+
if(!preAuthorizedCode) {
|
|
642
|
+
throw createNamedError({
|
|
643
|
+
message: '"offer.grant" is missing "pre-authorized_code".',
|
|
644
|
+
name: 'DataError'
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return {issuer: credential_issuer, preAuthorizedCode};
|
|
649
|
+
}
|
package/lib/convert/index.js
CHANGED
|
@@ -62,11 +62,18 @@ export function fromVpr({
|
|
|
62
62
|
if(verifiablePresentationRequest.domain) {
|
|
63
63
|
// since a `domain` was provided, set these defaults:
|
|
64
64
|
authorizationRequest.client_id = verifiablePresentationRequest.domain;
|
|
65
|
-
|
|
65
|
+
const usesRedirectUriPrefix =
|
|
66
|
+
authorizationRequest.client_id?.startsWith('redirect_uri:');
|
|
67
|
+
authorizationRequest.response_uri = usesRedirectUriPrefix ?
|
|
68
|
+
authorizationRequest.client_id.slice('redirect_uri:'.length) :
|
|
69
|
+
authorizationRequest.client_id;
|
|
66
70
|
if(useClientIdPrefix) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
if(!usesRedirectUriPrefix) {
|
|
72
|
+
authorizationRequest.client_id =
|
|
73
|
+
`redirect_uri:${authorizationRequest.client_id}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if(usesRedirectUriPrefix) {
|
|
70
77
|
authorizationRequest.client_id_scheme = 'redirect_uri';
|
|
71
78
|
}
|
|
72
79
|
}
|
|
@@ -137,6 +144,7 @@ export function toVpr({authorizationRequest, strict = false} = {}) {
|
|
|
137
144
|
|
|
138
145
|
const {
|
|
139
146
|
client_id,
|
|
147
|
+
client_id_scheme,
|
|
140
148
|
client_metadata,
|
|
141
149
|
dcql_query,
|
|
142
150
|
expected_origins,
|
|
@@ -188,11 +196,14 @@ export function toVpr({authorizationRequest, strict = false} = {}) {
|
|
|
188
196
|
verifiablePresentationRequest.query = [didAuthnQuery];
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
// map `expected_origins`
|
|
199
|
+
// map `expected_origins` / `client_id` / `response_uri` to `domain`
|
|
192
200
|
if(response_uri !== undefined || client_id !== undefined) {
|
|
193
201
|
if(response_mode?.startsWith('dc_api') && !response_uri &&
|
|
194
202
|
expected_origins?.length > 0) {
|
|
195
203
|
verifiablePresentationRequest.domain = expected_origins[0];
|
|
204
|
+
} else if(client_id_scheme === 'redirect_uri' ||
|
|
205
|
+
client_id?.startsWith('redirect_uri:')) {
|
|
206
|
+
verifiablePresentationRequest.domain = client_id ?? response_uri;
|
|
196
207
|
} else {
|
|
197
208
|
verifiablePresentationRequest.domain = response_uri;
|
|
198
209
|
}
|
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2026 Digital Bazaar, Inc.
|
|
3
3
|
*/
|
|
4
4
|
import {assert, fetchJSON} from '../util.js';
|
|
5
5
|
|
|
6
|
+
export function createAuthorizationDetailsFromOffer({
|
|
7
|
+
issuerConfig, offer, supportedFormats, oid4vciVersion
|
|
8
|
+
} = {}) {
|
|
9
|
+
if(oid4vciVersion === 'draft13' ||
|
|
10
|
+
!(Array.isArray(issuerConfig.authorization_details_types_supported) &&
|
|
11
|
+
issuerConfig.authorization_details_types_supported
|
|
12
|
+
.includes('openid_credential'))) {
|
|
13
|
+
// issuer does not support authz details
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// build authz details from configs that match `offer` and `supportedFormats`
|
|
18
|
+
const configs = getCredentialConfigurations({
|
|
19
|
+
issuerConfig, offer, supportedFormats
|
|
20
|
+
});
|
|
21
|
+
const authorizationDetails = configs.map(
|
|
22
|
+
c => ({
|
|
23
|
+
type: 'openid_credential',
|
|
24
|
+
credential_configuration_id: c.id
|
|
25
|
+
}));
|
|
26
|
+
// only return details if there are matching configuration IDs
|
|
27
|
+
return authorizationDetails.length > 0 ? authorizationDetails : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export function createCredentialRequestsFromOffer({
|
|
7
|
-
issuerConfig, offer, format
|
|
31
|
+
issuerConfig, offer, format, authorizationDetails, oid4vciVersion
|
|
8
32
|
} = {}) {
|
|
9
|
-
// get
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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);
|
|
33
|
+
// get credential configs that match `offer` and `format`
|
|
34
|
+
const matchingConfigurations = getCredentialConfigurations({
|
|
35
|
+
issuerConfig, offer, supportedFormats: [format]
|
|
36
|
+
});
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
// build requests...
|
|
39
|
+
let requests;
|
|
40
|
+
|
|
41
|
+
// the presence of `authorizationDetails` triggers OID4VCI 1.0+ format,
|
|
42
|
+
// which uses `credential_identifier` instead of
|
|
43
|
+
// Draft 13 `format` + `credential_definition`
|
|
44
|
+
if(authorizationDetails && oid4vciVersion !== 'draft13') {
|
|
45
|
+
// add a request for each `credential_identifier` mentioned in each
|
|
46
|
+
// matching configuration
|
|
47
|
+
requests = [];
|
|
48
|
+
const matchingIds = new Set(matchingConfigurations.map(({id}) => id));
|
|
49
|
+
for(const element of authorizationDetails) {
|
|
50
|
+
const {
|
|
51
|
+
type, credential_configuration_id, credential_identifiers = []
|
|
52
|
+
} = element;
|
|
53
|
+
if(type !== 'openid_credential') {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if(matchingIds.has(credential_configuration_id)) {
|
|
57
|
+
requests.push(
|
|
58
|
+
...credential_identifiers.map(id => ({credential_identifier: id})));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// OID4VCI Draft 13 request format
|
|
63
|
+
requests = matchingConfigurations.map(
|
|
64
|
+
({format, credential_definition}) => ({format, credential_definition}));
|
|
65
|
+
}
|
|
66
|
+
if(!(requests?.length > 0)) {
|
|
24
67
|
throw new Error(
|
|
25
68
|
`No supported credential(s) with format "${format}" found.`);
|
|
26
69
|
}
|
|
@@ -65,6 +108,49 @@ export async function getCredentialOffer({url, agent} = {}) {
|
|
|
65
108
|
return response.data;
|
|
66
109
|
}
|
|
67
110
|
|
|
111
|
+
export function getCredentialConfigurations({
|
|
112
|
+
issuerConfig, offer, credentialConfigurationIds, supportedFormats
|
|
113
|
+
} = {}) {
|
|
114
|
+
// get all supported credential configurations from issuer config
|
|
115
|
+
let supported = [
|
|
116
|
+
..._createSupportedCredentialsMap({issuerConfig}).entries()]
|
|
117
|
+
.map(([id, c]) => ({id, ...c}));
|
|
118
|
+
|
|
119
|
+
// compute `credentialConfigurationIds` from `offer` as necessary
|
|
120
|
+
if(offer && !credentialConfigurationIds) {
|
|
121
|
+
if(offer.credential_configuration_ids) {
|
|
122
|
+
credentialConfigurationIds = offer.credential_configuration_ids;
|
|
123
|
+
} else if(offer.credentials) {
|
|
124
|
+
// allow legacy `offer.credentials` that express IDs
|
|
125
|
+
credentialConfigurationIds = offer.credentials
|
|
126
|
+
.map(c => typeof c === 'string' ? c : undefined)
|
|
127
|
+
.filter(c => c !== undefined);
|
|
128
|
+
|
|
129
|
+
// if no IDs; handle degenerate case of objects expressed in
|
|
130
|
+
// `offer.credentials` for pre-draft 13, to be dropped in a future major
|
|
131
|
+
// release that also drops draft 13 support
|
|
132
|
+
if(credentialConfigurationIds.length === 0) {
|
|
133
|
+
supported = offer.credentials.filter(c => typeof c === 'object');
|
|
134
|
+
credentialConfigurationIds = undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// filter by IDs, if given
|
|
140
|
+
if(credentialConfigurationIds) {
|
|
141
|
+
const idSet = new Set(credentialConfigurationIds);
|
|
142
|
+
supported = supported.filter(c => idSet.has(c.id));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// filter by supported formats, if given
|
|
146
|
+
if(supportedFormats) {
|
|
147
|
+
const formatSet = new Set(supportedFormats);
|
|
148
|
+
supported = supported.filter(c => formatSet.has(c.format));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return supported;
|
|
152
|
+
}
|
|
153
|
+
|
|
68
154
|
export function parseCredentialOfferUrl({url} = {}) {
|
|
69
155
|
assert(url, 'url', 'string');
|
|
70
156
|
|
|
@@ -116,23 +202,3 @@ function _createSupportedCredentialsMap({issuerConfig}) {
|
|
|
116
202
|
|
|
117
203
|
return supported;
|
|
118
204
|
}
|
|
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
|
-
}
|
package/lib/util.js
CHANGED
|
@@ -39,7 +39,7 @@ export function base64Encode(data) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function createNamedError({message, name, details, cause} = {}) {
|
|
42
|
-
const error = new Error(message, {cause});
|
|
42
|
+
const error = cause ? new Error(message, {cause}) : new Error(message);
|
|
43
43
|
error.name = name;
|
|
44
44
|
if(details) {
|
|
45
45
|
error.details = details;
|