@digitalbazaar/oid4-client 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/oid4vp.js CHANGED
@@ -1,840 +1,24 @@
1
1
  /*!
2
- * Copyright (c) 2023-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import {assert, assertOptional, fetchJSON} from './util.js';
5
- import {decodeJwt} from 'jose';
6
- import {httpClient} from '@digitalbazaar/http-client';
7
- import {JSONPath} from 'jsonpath-plus';
8
- import jsonpointer from 'jsonpointer';
9
- import {v4 as uuid} from 'uuid';
10
-
11
- // For examples of presentation request and responses, see:
4
+ export * as authzRequest from './authorizationRequest.js';
5
+ export * as authzResponse from './authorizationResponse.js';
6
+ export * as convert from './convert.js';
7
+
8
+ // backwards compatibility APIs
9
+ export {
10
+ get as getAuthorizationRequest
11
+ } from './authorizationRequest.js';
12
+ export {
13
+ createPresentationSubmission,
14
+ send as sendAuthorizationResponse
15
+ } from './authorizationResponse.js';
16
+ export {
17
+ fromVpr, toVpr,
18
+ // exported for testing purposes only
19
+ _fromQueryByExampleQuery
20
+ } from './convert.js';
21
+
22
+ // Note: for examples of presentation request and responses, see:
12
23
  // eslint-disable-next-line max-len
13
24
  // https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#appendix-A.1.2.2
14
-
15
- // get an authorization request from a verifier
16
- export async function getAuthorizationRequest({
17
- url, agent, documentLoader
18
- } = {}) {
19
- try {
20
- assert(url, 'url', 'string');
21
- assertOptional(documentLoader, 'documentLoader', 'function');
22
-
23
- let requestUrl = url;
24
- let expectedClientId;
25
- if(url.startsWith('openid4vp://')) {
26
- const {authorizationRequest} = _parseOID4VPUrl({url});
27
- if(authorizationRequest.request) {
28
- const error = new Error(
29
- 'JWT-Secured Authorization Request (JAR) not implemented.');
30
- error.name = 'NotSupportedError';
31
- throw error;
32
- }
33
- if(!authorizationRequest.request_uri) {
34
- // return direct request
35
- return {authorizationRequest, fetched: false};
36
- }
37
- requestUrl = authorizationRequest.request_uri;
38
- ({client_id: expectedClientId} = authorizationRequest);
39
- }
40
-
41
- // FIXME: every `fetchJSON` call needs to use a block list or other
42
- // protections to prevent a confused deputy attack where the `requestUrl`
43
- // accesses a location it should not, e.g., is on localhost and should
44
- // not be used in this way
45
- const response = await fetchJSON({url: requestUrl, agent});
46
-
47
- // parse payload from response data...
48
- const contentType = response.headers.get('content-type');
49
- const jwt = await response.text();
50
- // verify response is a JWT-secured authorization request
51
- if(!(contentType.includes('application/oauth-authz-req+jwt') &&
52
- typeof jwt === 'string')) {
53
- const error = new Error(
54
- 'Authorization request content-type must be ' +
55
- '"application/oauth-authz-req+jwt".');
56
- error.name = 'DataError';
57
- throw error;
58
- }
59
-
60
- // decode JWT *WITHOUT* verification
61
- const payload = decodeJwt(jwt);
62
-
63
- // validate payload (expected authorization request)
64
- const {
65
- client_id,
66
- client_id_scheme,
67
- client_metadata,
68
- client_metadata_uri,
69
- nonce,
70
- presentation_definition,
71
- presentation_definition_uri,
72
- response_mode,
73
- scope
74
- } = payload;
75
- assert(client_id, 'client_id', 'string');
76
- // ensure `client_id` matches expected client ID
77
- if(expectedClientId !== undefined && client_id !== expectedClientId) {
78
- const error = new Error(
79
- '"client_id" in fetched request does not match authorization ' +
80
- 'request URL parameter.');
81
- error.name = 'DataError';
82
- throw error;
83
- }
84
- assert(nonce, 'nonce', 'string');
85
- assertOptional(client_id_scheme, 'client_id_scheme', 'string');
86
- assertOptional(client_metadata, 'client_metadata', 'object');
87
- assertOptional(client_metadata_uri, 'client_metadata_uri', 'string');
88
- assertOptional(
89
- presentation_definition, 'presentation_definition', 'object');
90
- assertOptional(
91
- presentation_definition_uri, 'presentation_definition_uri', 'string');
92
- assertOptional(response_mode, 'response_mode', 'string');
93
- assertOptional(scope, 'scope', 'string');
94
- if(client_metadata && client_metadata_uri) {
95
- const error = new Error(
96
- 'Only one of "client_metadata" and ' +
97
- '"client_metadata_uri" must be present.');
98
- error.name = 'DataError';
99
- throw error;
100
- }
101
- if(presentation_definition && presentation_definition_uri) {
102
- const error = new Error(
103
- 'Only one of "presentation_definition" and ' +
104
- '"presentation_definition_uri" must be present.');
105
- error.name = 'DataError';
106
- throw error;
107
- }
108
- // Note: This implementation requires `response_mode` to be `direct_post`,
109
- // no other modes are supported.
110
- if(response_mode !== 'direct_post') {
111
- const error = new Error(
112
- 'Only "direct_post" response mode is supported.');
113
- error.name = 'NotSupportedError';
114
- throw error;
115
- }
116
-
117
- // build merged authorization request
118
- const authorizationRequest = {...payload};
119
-
120
- // get client meta data from URL if specified
121
- if(client_metadata_uri) {
122
- const response = await fetchJSON({url: client_metadata_uri, agent});
123
- if(!response.data) {
124
- const error = new Error('Client meta data format is not JSON.');
125
- error.name = 'DataError';
126
- throw error;
127
- }
128
- // FIXME: can `data` be a JWT and require verification as well?
129
- delete authorizationRequest.client_metadata_uri;
130
- authorizationRequest.client_metadata = response.data;
131
- }
132
-
133
- // get presentation definition from URL if not embedded
134
- if(presentation_definition_uri) {
135
- const response = await fetchJSON(
136
- {url: presentation_definition_uri, agent});
137
- if(!response.data) {
138
- const error = new Error('Presentation definition format is not JSON.');
139
- error.name = 'DataError';
140
- throw error;
141
- }
142
- // FIXME: can `data` be a JWT and require verification as well?
143
- delete authorizationRequest.presentation_definition_uri;
144
- authorizationRequest.presentation_definition = response.data;
145
- }
146
-
147
- // FIXME: validate `authorizationRequest.presentation_definition`
148
-
149
- // return merged authorization request and original response
150
- return {authorizationRequest, fetched: true, requestUrl, response, jwt};
151
- } catch(cause) {
152
- const error = new Error('Could not get authorization request.', {cause});
153
- error.name = 'OperationError';
154
- throw error;
155
- }
156
- }
157
-
158
- export async function sendAuthorizationResponse({
159
- verifiablePresentation,
160
- presentationSubmission,
161
- authorizationRequest,
162
- vpToken,
163
- agent
164
- } = {}) {
165
- try {
166
- // if no `presentationSubmission` provided, auto-generate one
167
- let generatedPresentationSubmission = false;
168
- if(!presentationSubmission) {
169
- ({presentationSubmission} = createPresentationSubmission({
170
- presentationDefinition: authorizationRequest.presentation_definition,
171
- verifiablePresentation
172
- }));
173
- generatedPresentationSubmission = true;
174
- }
175
-
176
- // send VP and presentation submission to complete exchange
177
- const body = new URLSearchParams();
178
- body.set('vp_token', vpToken ?? JSON.stringify(verifiablePresentation));
179
- body.set('presentation_submission', JSON.stringify(presentationSubmission));
180
- const response = await httpClient.post(authorizationRequest.response_uri, {
181
- agent, body, headers: {accept: 'application/json'},
182
- // FIXME: limit response size
183
- // timeout in ms for response
184
- timeout: 5000
185
- });
186
- // return response data as `result`
187
- const result = response.data || {};
188
- if(generatedPresentationSubmission) {
189
- // return any generated presentation submission
190
- return {result, presentationSubmission};
191
- }
192
- return {result};
193
- } catch(cause) {
194
- const error = new Error(
195
- 'Could not send OID4VP authorization response.', {cause});
196
- error.name = 'OperationError';
197
- throw error;
198
- }
199
- }
200
-
201
- // converts an OID4VP authorization request (including its
202
- // "presentation definition") to a VPR
203
- export async function toVpr({
204
- authorizationRequest, strict = false, agent
205
- } = {}) {
206
- try {
207
- const {
208
- client_id,
209
- client_metadata_uri,
210
- nonce,
211
- presentation_definition_uri,
212
- } = authorizationRequest;
213
- let {
214
- client_metadata,
215
- presentation_definition
216
- } = authorizationRequest;
217
- if(client_metadata && client_metadata_uri) {
218
- const error = new Error(
219
- 'Only one of "client_metadata" and ' +
220
- '"client_metadata_uri" must be present.');
221
- error.name = 'DataError';
222
- throw error;
223
- }
224
- if(presentation_definition && presentation_definition_uri) {
225
- const error = new Error(
226
- 'Only one of "presentation_definition" and ' +
227
- '"presentation_definition_uri" must be present.');
228
- error.name = 'DataError';
229
- throw error;
230
- }
231
-
232
- // apply constraints for currently supported subset of AR data
233
- if(client_metadata_uri) {
234
- const response = await fetchJSON({url: client_metadata_uri, agent});
235
- if(!response.data) {
236
- const error = new Error('Client metadata format is not JSON.');
237
- error.name = 'DataError';
238
- throw error;
239
- }
240
- client_metadata = response.data;
241
- }
242
- assertOptional(client_metadata, 'client_metadata', 'object');
243
- if(presentation_definition_uri) {
244
- const response = await fetchJSON(
245
- {url: presentation_definition_uri, agent});
246
- if(!response.data) {
247
- const error = new Error('Presentation definition format is not JSON.');
248
- error.name = 'DataError';
249
- throw error;
250
- }
251
- presentation_definition = response.data;
252
- }
253
- assert(presentation_definition, 'presentation_definition', 'object');
254
- assert(presentation_definition?.id, 'presentation_definition.id', 'string');
255
- if(presentation_definition.submission_requirements && strict) {
256
- const error = new Error('"submission_requirements" is not supported.');
257
- error.name = 'NotSupportedError';
258
- throw error;
259
- }
260
-
261
- // generate base VPR from presentation definition
262
- const verifiablePresentationRequest = {
263
- // map each `input_descriptors` value to a `QueryByExample` query
264
- query: [{
265
- type: 'QueryByExample',
266
- credentialQuery: presentation_definition.input_descriptors.map(
267
- inputDescriptor => _toQueryByExampleQuery({inputDescriptor, strict}))
268
- }]
269
- };
270
-
271
- // add `DIDAuthentication` query based on client_metadata
272
- if(client_metadata) {
273
- const query = _toDIDAuthenticationQuery({client_metadata, strict});
274
- if(query !== undefined) {
275
- verifiablePresentationRequest.query.unshift(query);
276
- }
277
- }
278
-
279
- // map `client_id` to `domain`
280
- if(client_id !== undefined) {
281
- verifiablePresentationRequest.domain = client_id;
282
- }
283
-
284
- // map `nonce` to `challenge`
285
- if(nonce !== undefined) {
286
- verifiablePresentationRequest.challenge = nonce;
287
- }
288
-
289
- return {verifiablePresentationRequest};
290
- } catch(cause) {
291
- const error = new Error(
292
- 'Could not convert OID4VP authorization request to ' +
293
- 'verifiable presentation request.', {cause});
294
- error.name = 'OperationError';
295
- throw error;
296
- }
297
- }
298
-
299
- // converts a VPR to partial "authorization request"
300
- export function fromVpr({
301
- verifiablePresentationRequest, strict = false, prefixJwtVcPath
302
- } = {}) {
303
- try {
304
- let {query} = verifiablePresentationRequest;
305
- if(!Array.isArray(query)) {
306
- query = [query];
307
- }
308
-
309
- // convert any `QueryByExample` queries
310
- const queryByExample = query.filter(({type}) => type === 'QueryByExample');
311
- let credentialQuery = [];
312
- if(queryByExample.length > 0) {
313
- if(queryByExample.length > 1 && strict) {
314
- const error = new Error(
315
- 'Multiple "QueryByExample" VPR queries are not supported.');
316
- error.name = 'NotSupportedError';
317
- throw error;
318
- }
319
- ([{credentialQuery = []}] = queryByExample);
320
- if(!Array.isArray(credentialQuery)) {
321
- credentialQuery = [credentialQuery];
322
- }
323
- }
324
- const authorizationRequest = {
325
- response_type: 'vp_token',
326
- presentation_definition: {
327
- id: uuid(),
328
- input_descriptors: credentialQuery.map(q => _fromQueryByExampleQuery({
329
- credentialQuery: q,
330
- prefixJwtVcPath
331
- }))
332
- },
333
- response_mode: 'direct_post'
334
- };
335
-
336
- // convert any `DIDAuthentication` queries
337
- const didAuthnQuery = query.filter(
338
- ({type}) => type === 'DIDAuthentication');
339
- if(didAuthnQuery.length > 0) {
340
- if(didAuthnQuery.length > 1 && strict) {
341
- const error = new Error(
342
- 'Multiple "DIDAuthentication" VPR queries are not supported.');
343
- error.name = 'NotSupportedError';
344
- throw error;
345
- }
346
- const [query] = didAuthnQuery;
347
- const client_metadata = _fromDIDAuthenticationQuery({query, strict});
348
- authorizationRequest.client_metadata = client_metadata;
349
- }
350
-
351
- if(queryByExample.length === 0 && didAuthnQuery.length === 0 && strict) {
352
- const error = new Error(
353
- 'Only "DIDAuthentication" and "QueryByExample" VPR queries are ' +
354
- 'supported.');
355
- error.name = 'NotSupportedError';
356
- throw error;
357
- }
358
-
359
- // include requested authn params
360
- if(verifiablePresentationRequest.domain) {
361
- // `authorizationRequest` uses `direct_post` so force client ID to
362
- // be the exchange response URL per "Note" here:
363
- // eslint-disable-next-line max-len
364
- // https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#section-6.2
365
- authorizationRequest.client_id = verifiablePresentationRequest.domain;
366
- authorizationRequest.client_id_scheme = 'redirect_uri';
367
- authorizationRequest.response_uri = authorizationRequest.client_id;
368
- }
369
- if(verifiablePresentationRequest.challenge) {
370
- authorizationRequest.nonce = verifiablePresentationRequest.challenge;
371
- }
372
-
373
- return authorizationRequest;
374
- } catch(cause) {
375
- const error = new Error(
376
- 'Could not convert verifiable presentation request to ' +
377
- 'an OID4VP authorization request.', {cause});
378
- error.name = 'OperationError';
379
- throw error;
380
- }
381
- }
382
-
383
- // creates a "presentation submission" from a presentation definition and VP
384
- export function createPresentationSubmission({
385
- presentationDefinition, verifiablePresentation
386
- } = {}) {
387
- const descriptor_map = [];
388
- const presentationSubmission = {
389
- id: uuid(),
390
- definition_id: presentationDefinition.id,
391
- descriptor_map
392
- };
393
-
394
- try {
395
- // walk through each input descriptor object and match it to a VC
396
- let {verifiableCredential: vcs} = verifiablePresentation;
397
- const single = !Array.isArray(vcs);
398
- if(single) {
399
- vcs = [vcs];
400
- }
401
- /* Note: It is conceivable that the same VC could match multiple input
402
- descriptors. In this simplistic implementation, the first VC that matches
403
- is used. This may result in VCs in the VP not being mapped to an input
404
- descriptor, but every input descriptor having a VC that matches (i.e., at
405
- least one VC will be shared across multiple input descriptors). If
406
- some other behavior is more desirable, this can be changed in a future
407
- version. */
408
- for(const inputDescriptor of presentationDefinition.input_descriptors) {
409
- // walk through each VC and try to match it to the input descriptor
410
- for(let i = 0; i < vcs.length; ++i) {
411
- const verifiableCredential = vcs[i];
412
- if(_matchesInputDescriptor({inputDescriptor, verifiableCredential})) {
413
- descriptor_map.push({
414
- id: inputDescriptor.id,
415
- path: '$',
416
- format: 'ldp_vp',
417
- path_nested: {
418
- format: 'ldp_vc',
419
- path: single ?
420
- '$.verifiableCredential' :
421
- '$.verifiableCredential[' + i + ']'
422
- }
423
- });
424
- break;
425
- }
426
- }
427
- }
428
- } catch(cause) {
429
- const error = new Error(
430
- 'Could not create presentation submission.', {cause});
431
- error.name = 'OperationError';
432
- throw error;
433
- }
434
-
435
- return {presentationSubmission};
436
- }
437
-
438
- function _filterToValue({filter, strict = false}) {
439
- /* Each `filter` has a JSON Schema object. In recognition of the fact that
440
- a query must be usable by common database engines (including perhaps
441
- encrypted cloud databases) and of the fact that each JSON Schema object will
442
- come from an untrusted source (and could have malicious regexes, etc.), only
443
- simple JSON Schema types are supported:
444
-
445
- `string`: with `const` or `enum`, `format` is not supported and `pattern` has
446
- partial support as it will be treated as a simple string not a regex; regex
447
- is a DoS attack vector
448
-
449
- `array`: with `contains` where uses a `string` filter
450
-
451
- `allOf`: supported only with the above schemas present in it.
452
-
453
- */
454
- let value;
455
-
456
- const {type} = filter;
457
- if(type === 'array') {
458
- if(filter.contains) {
459
- if(Array.isArray(filter.contains)) {
460
- return filter.contains.map(filter => _filterToValue({filter, strict}));
461
- }
462
- return _filterToValue({filter: filter.contains, strict});
463
- }
464
- if(Array.isArray(filter.allOf) && filter.allOf.every(f => f.contains)) {
465
- return filter.allOf.map(
466
- f => _filterToValue({filter: f.contains, strict}));
467
- }
468
- if(strict) {
469
- throw new Error(
470
- 'Unsupported filter; array filters must use "allOf" and/or ' +
471
- '"contains" with a string filter.');
472
- }
473
- return value;
474
- }
475
- if(type === 'string' || type === undefined) {
476
- if(filter.const !== undefined) {
477
- value = filter.const;
478
- } else if(filter.pattern) {
479
- value = filter.pattern;
480
- } else if(filter.enum) {
481
- value = filter.enum.slice();
482
- } else if(strict) {
483
- throw new Error(
484
- 'Unsupported filter; string filters must use "const" or "pattern".');
485
- }
486
- return value;
487
- }
488
- if(strict) {
489
- throw new Error(`Unsupported filter type "${type}".`);
490
- }
491
- }
492
-
493
- function _jsonPathToJsonPointer(jsonPath) {
494
- return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
495
- }
496
-
497
- function _matchesInputDescriptor({
498
- inputDescriptor, verifiableCredential, strict = false
499
- }) {
500
- // walk through each field ensuring there is a matching value
501
- const fields = inputDescriptor?.constraints?.fields || [];
502
- for(const field of fields) {
503
- const {path, filter, optional} = field;
504
- if(optional) {
505
- // skip field, it is optional
506
- continue;
507
- }
508
-
509
- try {
510
- // each field must have a `path` (which can be a string or an array)
511
- if(!(Array.isArray(path) || typeof path === 'string')) {
512
- throw new Error(
513
- 'Input descriptor field "path" must be a string or array.');
514
- }
515
-
516
- // process any filter
517
- let value = '';
518
- if(filter !== undefined) {
519
- value = _filterToValue({filter, strict});
520
- }
521
- // no value to match, presume no match
522
- if(value === undefined) {
523
- return false;
524
- }
525
- // normalize value to array
526
- if(!Array.isArray(value)) {
527
- value = [value];
528
- }
529
-
530
- // filter out erroneous paths
531
- let paths = Array.isArray(path) ? path : [path];
532
- paths = _adjustErroneousPaths(paths);
533
- // convert each JSON path to a JSON pointer
534
- const pointers = paths.map(_jsonPathToJsonPointer);
535
-
536
- // check for a value at at least one path
537
- for(const pointer of pointers) {
538
- const existing = jsonpointer.get(verifiableCredential, pointer);
539
- if(existing === undefined) {
540
- // VC does not match
541
- return false;
542
- }
543
- // look for at least one matching value in `existing`
544
- let match = false;
545
- for(const v of value) {
546
- if(Array.isArray(existing)) {
547
- if(existing.includes(v)) {
548
- match = true;
549
- break;
550
- }
551
- } else if(existing === v) {
552
- match = true;
553
- break;
554
- }
555
- }
556
- if(!match) {
557
- return false;
558
- }
559
- }
560
- } catch(cause) {
561
- const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
562
- const error = new Error(
563
- `Could not process input descriptor field: "${id}".`, {cause});
564
- error.field = field;
565
- throw error;
566
- }
567
- }
568
-
569
- return true;
570
- }
571
-
572
- // exported for testing purposes only
573
- export function _fromQueryByExampleQuery({credentialQuery, prefixJwtVcPath}) {
574
- // determine `prefixJwtVcPath` default:
575
- // if `credentialQuery` specifies `acceptedEnvelopes: ['application/jwt']`,
576
- // then default `prefixJwtVcPath` to `true`
577
- if(prefixJwtVcPath === undefined &&
578
- (Array.isArray(credentialQuery.acceptedEnvelopes) &&
579
- credentialQuery.acceptedEnvelopes.includes?.('application/jwt'))) {
580
- prefixJwtVcPath = true;
581
- }
582
-
583
- const fields = [];
584
- const inputDescriptor = {
585
- id: uuid(),
586
- constraints: {fields}
587
- };
588
- if(credentialQuery?.reason) {
589
- inputDescriptor.purpose = credentialQuery?.reason;
590
- }
591
- // FIXME: current implementation only supports top-level string/array
592
- // properties and presumes strings
593
- const path = ['$'];
594
- const {example = {}} = credentialQuery || {};
595
- for(const key in example) {
596
- const value = example[key];
597
- path.push(key);
598
-
599
- const filter = {};
600
- if(Array.isArray(value)) {
601
- filter.type = 'array';
602
- filter.allOf = value.map(v => ({
603
- contains: {
604
- type: 'string',
605
- const: v
606
- }
607
- }));
608
- } else if(key === 'type') {
609
- // special provision for array/string for `type`
610
- filter.type = 'array',
611
- filter.contains = {
612
- type: 'string',
613
- const: value
614
- };
615
- } else {
616
- filter.type = 'string',
617
- filter.const = value;
618
- }
619
- const fieldsPath = [JSONPath.toPathString(path)];
620
- // include 'vc' path for queries against JWT payloads instead of VCs
621
- if(prefixJwtVcPath) {
622
- const vcPath = [...path];
623
- vcPath.splice(1, 0, 'vc');
624
- fieldsPath.push(JSONPath.toPathString(vcPath));
625
- }
626
- fields.push({
627
- path: fieldsPath,
628
- filter
629
- });
630
-
631
- path.pop();
632
- }
633
-
634
- return inputDescriptor;
635
- }
636
-
637
- function _toDIDAuthenticationQuery({client_metadata, strict = false}) {
638
- const {vp_formats} = client_metadata;
639
- const proofTypes = vp_formats?.ldp_vp?.proof_type;
640
- if(!Array.isArray(proofTypes)) {
641
- if(strict) {
642
- const error = new Error(
643
- '"client_metadata.vp_formats.ldp_vp.proof_type" must be an array to ' +
644
- 'convert to DIDAuthentication query.');
645
- error.name = 'NotSupportedError';
646
- throw error;
647
- }
648
- return;
649
- }
650
- return {
651
- type: 'DIDAuthentication',
652
- acceptedCryptosuites: proofTypes.map(cryptosuite => ({cryptosuite}))
653
- };
654
- }
655
-
656
- function _fromDIDAuthenticationQuery({query, strict = false}) {
657
- const cryptosuites = query.acceptedCryptosuites?.map(
658
- ({cryptosuite}) => cryptosuite);
659
- if(!(cryptosuites && cryptosuites.length > 0)) {
660
- if(strict) {
661
- const error = new Error(
662
- '"query.acceptedCryptosuites" must be a non-array with specified ' +
663
- 'cryptosuites to convert from a DIDAuthentication query.');
664
- error.name = 'NotSupportedError';
665
- throw error;
666
- }
667
- return;
668
- }
669
- return {
670
- require_signed_request_object: false,
671
- vp_formats: {
672
- ldp_vp: {
673
- proof_type: cryptosuites
674
- }
675
- }
676
- };
677
- }
678
-
679
- function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
680
- // every input descriptor must have an `id`
681
- if(typeof inputDescriptor?.id !== 'string') {
682
- throw new TypeError('Input descriptor "id" must be a string.');
683
- }
684
-
685
- const example = {};
686
- const credentialQuery = {example};
687
- if(inputDescriptor.purpose) {
688
- credentialQuery.reason = inputDescriptor.purpose;
689
- }
690
-
691
- /* Note: Each input descriptor object is currently mapped to a single example
692
- query. If multiple possible path values appear for a single field, these will
693
- be mapped to multiple properties in the example which may or may not be what
694
- is intended. This behavior could be changed in a future revision if it
695
- becomes clear there is a better approach. */
696
-
697
- const fields = inputDescriptor.constraints?.fields || [];
698
- for(const field of fields) {
699
- const {path, filter, optional} = field;
700
- // skip optional fields
701
- if(optional === true) {
702
- continue;
703
- }
704
-
705
- try {
706
- // each field must have a `path` (which can be a string or an array)
707
- if(!(Array.isArray(path) || typeof path === 'string')) {
708
- throw new TypeError(
709
- 'Input descriptor field "path" must be a string or array.');
710
- }
711
-
712
- // process any filter
713
- let value = '';
714
- if(filter !== undefined) {
715
- value = _filterToValue({filter, strict});
716
- }
717
- // no value understood, skip field
718
- if(value === undefined) {
719
- continue;
720
- }
721
- // normalize value to array
722
- if(!Array.isArray(value)) {
723
- value = [value];
724
- }
725
-
726
- // filter out erroneous paths
727
- let paths = Array.isArray(path) ? path : [path];
728
- paths = _adjustErroneousPaths(paths);
729
- // convert each JSON path to a JSON pointer
730
- const pointers = paths.map(_jsonPathToJsonPointer);
731
-
732
- // add values at each path, converting to an array / appending as needed
733
- for(const pointer of pointers) {
734
- const existing = jsonpointer.get(example, pointer);
735
- if(existing === undefined) {
736
- jsonpointer.set(
737
- example, pointer, value.length > 1 ? value : value[0]);
738
- } else if(Array.isArray(existing)) {
739
- if(!existing.includes(value)) {
740
- existing.push(...value);
741
- }
742
- } else if(existing !== value) {
743
- jsonpointer.set(example, pointer, [existing, ...value]);
744
- }
745
- }
746
- } catch(cause) {
747
- const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
748
- const error = new Error(
749
- `Could not process input descriptor field: "${id}".`, {cause});
750
- error.field = field;
751
- throw error;
752
- }
753
- }
754
-
755
- return credentialQuery;
756
- }
757
-
758
- function _adjustErroneousPaths(paths) {
759
- // remove any paths that start with what would be present in a
760
- // presentation submission and adjust any paths that would be part of a
761
- // JWT-secured VC, such that only actual VC paths remain
762
- const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
763
- return [...new Set(removed.map(p => {
764
- if(_isJWTPath(p)) {
765
- return '$' + p.slice('$.vc'.length);
766
- }
767
- if(_isSquareJWTPath(p)) {
768
- return '$' + p.slice('$[\'vc\']'.length);
769
- }
770
- return p;
771
- }))];
772
- }
773
-
774
- function _parseOID4VPUrl({url}) {
775
- const {searchParams} = new URL(url);
776
- const request = _get(searchParams, 'request');
777
- const request_uri = _get(searchParams, 'request_uri');
778
- const response_type = _get(searchParams, 'response_type');
779
- const response_mode = _get(searchParams, 'response_mode');
780
- const presentation_definition = _get(
781
- searchParams, 'presentation_definition');
782
- const presentation_definition_uri = _get(
783
- searchParams, 'presentation_definition_uri');
784
- const client_id = _get(searchParams, 'client_id');
785
- const client_id_scheme = _get(searchParams, 'client_id_scheme');
786
- const client_metadata = _get(searchParams, 'client_metadata');
787
- const nonce = _get(searchParams, 'nonce');
788
- const response_uri = _get(searchParams, 'response_uri');
789
- const state = _get(searchParams, 'state');
790
- if(request && request_uri) {
791
- const error = new Error(
792
- 'Only one of "request" and "request_uri" may be present.');
793
- error.name = 'DataError';
794
- error.url = url;
795
- throw error;
796
- }
797
- if(!(request || request_uri)) {
798
- if(response_type !== 'vp_token') {
799
- throw new Error(`Unsupported "response_type", "${response_type}".`);
800
- }
801
- if(response_mode !== 'direct_post') {
802
- throw new Error(`Unsupported "response_type", "${response_type}".`);
803
- }
804
- }
805
- const authorizationRequest = {
806
- request,
807
- request_uri,
808
- response_type,
809
- response_mode,
810
- presentation_definition: presentation_definition &&
811
- JSON.parse(presentation_definition),
812
- presentation_definition_uri,
813
- client_id,
814
- client_id_scheme,
815
- client_metadata: client_metadata && JSON.parse(client_metadata),
816
- response_uri,
817
- nonce,
818
- state
819
- };
820
- return {authorizationRequest};
821
- }
822
-
823
- function _get(sp, name) {
824
- const value = sp.get(name);
825
- return value === null ? undefined : value;
826
- }
827
-
828
- function _isPresentationSubmissionPath(path) {
829
- return path.startsWith('$.verifiableCredential[') ||
830
- path.startsWith('$.vp.') ||
831
- path.startsWith('$[\'verifiableCredential') || path.startsWith('$[\'vp');
832
- }
833
-
834
- function _isJWTPath(path) {
835
- return path.startsWith('$.vc.');
836
- }
837
-
838
- function _isSquareJWTPath(path) {
839
- return path.startsWith('$[\'vc\']');
840
- }