@digitalbazaar/oid4-client 5.1.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 CHANGED
@@ -1,8 +1,10 @@
1
1
  /*!
2
- * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {generateDIDProofJWT, robustDiscoverIssuer} from './util.js';
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 = _createCredentialRequestsFromOffer({
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 = _createCredentialRequestsFromOffer({
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
- generateDIDProofJWT,
8
+ robustDiscoverIssuer
9
+ } from './oid4vci/discovery.js';
10
+ export {
8
11
  getCredentialOffer,
9
- parseCredentialOfferUrl,
10
- robustDiscoverIssuer,
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
+ }