@digitalbazaar/oid4-client 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/convert.js DELETED
@@ -1,430 +0,0 @@
1
- /*!
2
- * Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
3
- */
4
- import {JSONPath} from 'jsonpath-plus';
5
- import jsonpointer from 'jsonpointer';
6
- import {
7
- validate as validateAuthorizationRequest
8
- } from './authorizationRequest.js';
9
-
10
- // converts a VPR to partial "authorization request"
11
- export function fromVpr({
12
- verifiablePresentationRequest, strict = false, prefixJwtVcPath
13
- } = {}) {
14
- try {
15
- let {query} = verifiablePresentationRequest;
16
- if(!Array.isArray(query)) {
17
- query = [query];
18
- }
19
-
20
- // convert any `QueryByExample` queries
21
- const queryByExample = query.filter(({type}) => type === 'QueryByExample');
22
- let credentialQuery = [];
23
- if(queryByExample.length > 0) {
24
- if(queryByExample.length > 1 && strict) {
25
- const error = new Error(
26
- 'Multiple "QueryByExample" VPR queries are not supported.');
27
- error.name = 'NotSupportedError';
28
- throw error;
29
- }
30
- ([{credentialQuery = []}] = queryByExample);
31
- if(!Array.isArray(credentialQuery)) {
32
- credentialQuery = [credentialQuery];
33
- }
34
- }
35
- const authorizationRequest = {
36
- response_type: 'vp_token',
37
- presentation_definition: {
38
- id: crypto.randomUUID(),
39
- input_descriptors: credentialQuery.map(q => _fromQueryByExampleQuery({
40
- credentialQuery: q,
41
- prefixJwtVcPath
42
- }))
43
- },
44
- // default to `direct_post`; caller can override
45
- response_mode: 'direct_post'
46
- };
47
-
48
- // convert any `DIDAuthentication` queries
49
- const didAuthnQuery = query.filter(
50
- ({type}) => type === 'DIDAuthentication');
51
- if(didAuthnQuery.length > 0) {
52
- if(didAuthnQuery.length > 1 && strict) {
53
- const error = new Error(
54
- 'Multiple "DIDAuthentication" VPR queries are not supported.');
55
- error.name = 'NotSupportedError';
56
- throw error;
57
- }
58
- const [query] = didAuthnQuery;
59
- const client_metadata = _fromDIDAuthenticationQuery({query, strict});
60
- authorizationRequest.client_metadata = client_metadata;
61
- }
62
-
63
- if(queryByExample.length === 0 && didAuthnQuery.length === 0 && strict) {
64
- const error = new Error(
65
- 'Only "DIDAuthentication" and "QueryByExample" VPR queries are ' +
66
- 'supported.');
67
- error.name = 'NotSupportedError';
68
- throw error;
69
- }
70
-
71
- // include requested authn params
72
- if(verifiablePresentationRequest.domain) {
73
- // since a `domain` was provided, set these defaults:
74
- authorizationRequest.client_id = verifiablePresentationRequest.domain;
75
- authorizationRequest.client_id_scheme = 'redirect_uri';
76
- authorizationRequest.response_uri = authorizationRequest.client_id;
77
- }
78
- if(verifiablePresentationRequest.challenge) {
79
- authorizationRequest.nonce = verifiablePresentationRequest.challenge;
80
- }
81
-
82
- return authorizationRequest;
83
- } catch(cause) {
84
- const error = new Error(
85
- 'Could not convert verifiable presentation request to ' +
86
- 'an OID4VP authorization request.', {cause});
87
- error.name = 'OperationError';
88
- throw error;
89
- }
90
- }
91
-
92
- export function pathsToVerifiableCredentialPointers({paths} = {}) {
93
- // get only the paths inside a verifiable credential
94
- paths = Array.isArray(paths) ? paths : [paths];
95
- paths = _getVerifiableCredentialPaths(paths);
96
- // convert each JSON path to a JSON pointer
97
- return paths.map(_jsonPathToJsonPointer);
98
- }
99
-
100
- // converts an OID4VP authorization request (including its
101
- // "presentation definition") to a VPR
102
- export async function toVpr({authorizationRequest, strict = false} = {}) {
103
- try {
104
- // ensure authorization request is valid
105
- validateAuthorizationRequest({authorizationRequest});
106
-
107
- const {
108
- client_id,
109
- client_metadata,
110
- nonce,
111
- presentation_definition,
112
- response_uri
113
- } = authorizationRequest;
114
-
115
- // disallow unsupported `submission_requirements` in strict mode
116
- if(strict && presentation_definition.submission_requirements) {
117
- const error = new Error('"submission_requirements" is not supported.');
118
- error.name = 'NotSupportedError';
119
- throw error;
120
- }
121
-
122
- // generate base VPR from presentation definition
123
- const verifiablePresentationRequest = {
124
- // map each `input_descriptors` value to a `QueryByExample` query
125
- query: [{
126
- type: 'QueryByExample',
127
- credentialQuery: presentation_definition.input_descriptors.map(
128
- inputDescriptor => _toQueryByExampleQuery({inputDescriptor, strict}))
129
- }]
130
- };
131
-
132
- // add `DIDAuthentication` query based on client_metadata
133
- if(client_metadata) {
134
- const query = _toDIDAuthenticationQuery({client_metadata, strict});
135
- if(query !== undefined) {
136
- verifiablePresentationRequest.query.unshift(query);
137
- }
138
- }
139
-
140
- // map `response_uri` or `client_id` to `domain`
141
- if(response_uri !== undefined || client_id !== undefined) {
142
- verifiablePresentationRequest.domain = response_uri ?? client_id;
143
- }
144
-
145
- // map `nonce` to `challenge`
146
- if(nonce !== undefined) {
147
- verifiablePresentationRequest.challenge = nonce;
148
- }
149
-
150
- return {verifiablePresentationRequest};
151
- } catch(cause) {
152
- const error = new Error(
153
- 'Could not convert OID4VP authorization request to ' +
154
- 'verifiable presentation request.', {cause});
155
- error.name = 'OperationError';
156
- throw error;
157
- }
158
- }
159
-
160
- function _filterToValue({filter, strict = false}) {
161
- /* Each `filter` has a JSON Schema object. In recognition of the fact that
162
- a query must be usable by common database engines (including perhaps
163
- encrypted cloud databases) and of the fact that each JSON Schema object will
164
- come from an untrusted source (and could have malicious regexes, etc.), only
165
- simple JSON Schema types are supported:
166
-
167
- `string`: with `const` or `enum`, `format` is not supported and `pattern` has
168
- partial support as it will be treated as a simple string not a regex; regex
169
- is a DoS attack vector
170
-
171
- `array`: with `contains` where uses a `string` filter
172
-
173
- `allOf`: supported only with the above schemas present in it.
174
-
175
- */
176
- let value;
177
-
178
- const {type} = filter;
179
- if(type === 'array') {
180
- if(filter.contains) {
181
- if(Array.isArray(filter.contains)) {
182
- return filter.contains.map(filter => _filterToValue({filter, strict}));
183
- }
184
- return _filterToValue({filter: filter.contains, strict});
185
- }
186
- if(Array.isArray(filter.allOf) && filter.allOf.every(f => f.contains)) {
187
- return filter.allOf.map(
188
- f => _filterToValue({filter: f.contains, strict}));
189
- }
190
- if(strict) {
191
- throw new Error(
192
- 'Unsupported filter; array filters must use "allOf" and/or ' +
193
- '"contains" with a string filter.');
194
- }
195
- return value;
196
- }
197
- if(type === 'string' || type === undefined) {
198
- if(filter.const !== undefined) {
199
- value = filter.const;
200
- } else if(filter.pattern) {
201
- value = filter.pattern;
202
- } else if(filter.enum) {
203
- value = filter.enum.slice();
204
- } else if(strict) {
205
- throw new Error(
206
- 'Unsupported filter; string filters must use "const" or "pattern".');
207
- }
208
- return value;
209
- }
210
- if(strict) {
211
- throw new Error(`Unsupported filter type "${type}".`);
212
- }
213
- }
214
-
215
- // exported for testing purposes only
216
- export function _fromQueryByExampleQuery({credentialQuery, prefixJwtVcPath}) {
217
- // determine `prefixJwtVcPath` default:
218
- // if `credentialQuery` specifies `acceptedEnvelopes: ['application/jwt']`,
219
- // then default `prefixJwtVcPath` to `true`
220
- if(prefixJwtVcPath === undefined &&
221
- (Array.isArray(credentialQuery.acceptedEnvelopes) &&
222
- credentialQuery.acceptedEnvelopes.includes?.('application/jwt'))) {
223
- prefixJwtVcPath = true;
224
- }
225
-
226
- const fields = [];
227
- const inputDescriptor = {
228
- id: crypto.randomUUID(),
229
- constraints: {fields}
230
- };
231
- if(credentialQuery?.reason) {
232
- inputDescriptor.purpose = credentialQuery?.reason;
233
- }
234
- // FIXME: current implementation only supports top-level string/array
235
- // properties and presumes strings
236
- const path = ['$'];
237
- const {example = {}} = credentialQuery || {};
238
- for(const key in example) {
239
- const value = example[key];
240
- path.push(key);
241
-
242
- const filter = {};
243
- if(Array.isArray(value)) {
244
- filter.type = 'array';
245
- filter.allOf = value.map(v => ({
246
- contains: {
247
- type: 'string',
248
- const: v
249
- }
250
- }));
251
- } else if(key === 'type') {
252
- // special provision for array/string for `type`
253
- filter.type = 'array',
254
- filter.contains = {
255
- type: 'string',
256
- const: value
257
- };
258
- } else {
259
- filter.type = 'string',
260
- filter.const = value;
261
- }
262
- const fieldsPath = [JSONPath.toPathString(path)];
263
- // include 'vc' path for queries against JWT payloads instead of VCs
264
- if(prefixJwtVcPath) {
265
- const vcPath = [...path];
266
- vcPath.splice(1, 0, 'vc');
267
- fieldsPath.push(JSONPath.toPathString(vcPath));
268
- }
269
- fields.push({
270
- path: fieldsPath,
271
- filter
272
- });
273
-
274
- path.pop();
275
- }
276
-
277
- return inputDescriptor;
278
- }
279
-
280
- function _getVerifiableCredentialPaths(paths) {
281
- // remove any paths that start with what would be present in a
282
- // presentation submission and adjust any paths that would be part of a
283
- // JWT-secured VC, such that only actual VC paths remain
284
- const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
285
- return [...new Set(removed.map(p => {
286
- if(_isJWTPath(p)) {
287
- return '$' + p.slice('$.vc'.length);
288
- }
289
- if(_isSquareJWTPath(p)) {
290
- return '$' + p.slice('$[\'vc\']'.length);
291
- }
292
- return p;
293
- }))];
294
- }
295
-
296
- function _isPresentationSubmissionPath(path) {
297
- return path.startsWith('$.verifiableCredential[') ||
298
- path.startsWith('$.vp.') ||
299
- path.startsWith('$[\'verifiableCredential') || path.startsWith('$[\'vp');
300
- }
301
-
302
- function _isJWTPath(path) {
303
- return path.startsWith('$.vc.');
304
- }
305
-
306
- function _isSquareJWTPath(path) {
307
- return path.startsWith('$[\'vc\']');
308
- }
309
-
310
- function _jsonPathToJsonPointer(jsonPath) {
311
- return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
312
- }
313
-
314
- function _fromDIDAuthenticationQuery({query, strict = false}) {
315
- const cryptosuites = query.acceptedCryptosuites?.map(
316
- ({cryptosuite}) => cryptosuite);
317
- if(!(cryptosuites && cryptosuites.length > 0)) {
318
- if(strict) {
319
- const error = new Error(
320
- '"query.acceptedCryptosuites" must be a non-array with specified ' +
321
- 'cryptosuites to convert from a DIDAuthentication query.');
322
- error.name = 'NotSupportedError';
323
- throw error;
324
- }
325
- return;
326
- }
327
- return {
328
- require_signed_request_object: false,
329
- vp_formats: {
330
- ldp_vp: {
331
- proof_type: cryptosuites
332
- }
333
- }
334
- };
335
- }
336
-
337
- function _toDIDAuthenticationQuery({client_metadata, strict = false}) {
338
- const {vp_formats} = client_metadata;
339
- const proofTypes = vp_formats?.ldp_vp?.proof_type;
340
- if(!Array.isArray(proofTypes)) {
341
- if(strict) {
342
- const error = new Error(
343
- '"client_metadata.vp_formats.ldp_vp.proof_type" must be an array to ' +
344
- 'convert to DIDAuthentication query.');
345
- error.name = 'NotSupportedError';
346
- throw error;
347
- }
348
- return;
349
- }
350
- return {
351
- type: 'DIDAuthentication',
352
- acceptedCryptosuites: proofTypes.map(cryptosuite => ({cryptosuite}))
353
- };
354
- }
355
-
356
- function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
357
- // every input descriptor must have an `id`
358
- if(typeof inputDescriptor?.id !== 'string') {
359
- throw new TypeError('Input descriptor "id" must be a string.');
360
- }
361
-
362
- const example = {};
363
- const credentialQuery = {example};
364
- if(inputDescriptor.purpose) {
365
- credentialQuery.reason = inputDescriptor.purpose;
366
- }
367
-
368
- /* Note: Each input descriptor object is currently mapped to a single example
369
- query. If multiple possible path values appear for a single field, these will
370
- be mapped to multiple properties in the example which may or may not be what
371
- is intended. This behavior could be changed in a future revision if it
372
- becomes clear there is a better approach. */
373
-
374
- const fields = inputDescriptor.constraints?.fields || [];
375
- for(const field of fields) {
376
- const {path, filter, optional} = field;
377
- // skip optional fields
378
- if(optional === true) {
379
- continue;
380
- }
381
-
382
- try {
383
- // each field must have a `path` (which can be a string or an array)
384
- if(!(Array.isArray(path) || typeof path === 'string')) {
385
- throw new TypeError(
386
- 'Input descriptor field "path" must be a string or array.');
387
- }
388
-
389
- // process any filter
390
- let value = '';
391
- if(filter !== undefined) {
392
- value = _filterToValue({filter, strict});
393
- }
394
- // no value understood, skip field
395
- if(value === undefined) {
396
- continue;
397
- }
398
- // normalize value to array
399
- if(!Array.isArray(value)) {
400
- value = [value];
401
- }
402
-
403
- // get JSON pointers for every path inside a verifiable credential
404
- const pointers = pathsToVerifiableCredentialPointers({paths: path});
405
-
406
- // add values at each path, converting to an array / appending as needed
407
- for(const pointer of pointers) {
408
- const existing = jsonpointer.get(example, pointer);
409
- if(existing === undefined) {
410
- jsonpointer.set(
411
- example, pointer, value.length > 1 ? value : value[0]);
412
- } else if(Array.isArray(existing)) {
413
- if(!existing.includes(value)) {
414
- existing.push(...value);
415
- }
416
- } else if(existing !== value) {
417
- jsonpointer.set(example, pointer, [existing, ...value]);
418
- }
419
- }
420
- } catch(cause) {
421
- const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
422
- const error = new Error(
423
- `Could not process input descriptor field: "${id}".`, {cause});
424
- error.field = field;
425
- throw error;
426
- }
427
- }
428
-
429
- return credentialQuery;
430
- }