@digitalbazaar/oid4-client 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/OID4Client.js +6 -74
- package/lib/convert/index.js +349 -0
- package/lib/index.js +9 -4
- package/lib/oid4vci/credentialOffer.js +138 -0
- package/lib/oid4vci/discovery.js +126 -0
- package/lib/oid4vci/proofs.js +50 -0
- package/lib/{authorizationRequest.js → oid4vp/authorizationRequest.js} +4 -11
- package/lib/{authorizationResponse.js → oid4vp/authorizationResponse.js} +4 -4
- package/lib/{oid4vp.js → oid4vp/index.js} +2 -6
- package/lib/{verifier.js → oid4vp/verifier.js} +3 -16
- package/lib/{x509.js → oid4vp/x509.js} +1 -1
- package/lib/query/dcql.js +244 -0
- package/lib/query/index.js +18 -0
- package/lib/query/match.js +80 -0
- package/lib/query/presentationExchange.js +318 -0
- package/lib/query/queryByExample.js +8 -0
- package/lib/query/util.js +105 -0
- package/lib/util.js +22 -230
- package/package.json +2 -2
- package/lib/convert.js +0 -430
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2023-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {resolvePointer, toJsonPointerMap} from './util.js';
|
|
5
|
+
import {exampleToJsonPointerMap} from './queryByExample.js';
|
|
6
|
+
import {JSONPath} from 'jsonpath-plus';
|
|
7
|
+
import jsonpointer from 'json-pointer';
|
|
8
|
+
|
|
9
|
+
const VALUE_TYPES = new Set(['string', 'number', 'boolean']);
|
|
10
|
+
|
|
11
|
+
export function presentationDefinitionToVprGroups({
|
|
12
|
+
presentation_definition, strict = false
|
|
13
|
+
} = {}) {
|
|
14
|
+
const {input_descriptors: inputDescriptors} = presentation_definition;
|
|
15
|
+
|
|
16
|
+
// only one group (no "OR" with presentation exchange), every input
|
|
17
|
+
// input descriptor converts to a `QueryByExample` query in the same group
|
|
18
|
+
const queries = inputDescriptors.map(inputDescriptor => {
|
|
19
|
+
return {
|
|
20
|
+
type: 'QueryByExample',
|
|
21
|
+
credentialQuery: _toQueryByExampleQuery({inputDescriptor, strict})
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// output group map with a single group with all `QueryByExample` queries
|
|
26
|
+
const group = new Map([['QueryByExample', queries]]);
|
|
27
|
+
const groupMap = new Map([[undefined, group]]);
|
|
28
|
+
return groupMap;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function inputDescriptorToJsonPointerMap({inputDescriptor} = {}) {
|
|
32
|
+
const queryByExample = _toQueryByExampleQuery({inputDescriptor});
|
|
33
|
+
return exampleToJsonPointerMap(queryByExample);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function vprGroupsToPresentationDefinition({
|
|
37
|
+
groupMap, prefixJwtVcPath
|
|
38
|
+
} = {}) {
|
|
39
|
+
const input_descriptors = [];
|
|
40
|
+
const presentationDefinition = {
|
|
41
|
+
id: crypto.randomUUID(),
|
|
42
|
+
input_descriptors
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// note: same group ID is logical "AND" and different group ID is "OR"
|
|
46
|
+
const groups = [...groupMap.values()];
|
|
47
|
+
for(const queries of groups) {
|
|
48
|
+
// only `QueryByExample` is convertible
|
|
49
|
+
const queryByExamples = queries.get('QueryByExample');
|
|
50
|
+
if(!queryByExamples) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// for each `QueryByExample`, add another input descriptor (for every
|
|
55
|
+
// "credentialQuery" within it
|
|
56
|
+
for(const queryByExample of queryByExamples) {
|
|
57
|
+
// should only be one `credentialQuery` but handle each one as a new
|
|
58
|
+
// set of input descriptors
|
|
59
|
+
const all = Array.isArray(queryByExample.credentialQuery) ?
|
|
60
|
+
queryByExample.credentialQuery : [queryByExample.credentialQuery];
|
|
61
|
+
for(const credentialQuery of all) {
|
|
62
|
+
input_descriptors.push(_fromQueryByExampleQuery({
|
|
63
|
+
credentialQuery, prefixJwtVcPath
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return presentationDefinition;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// exported for backwards compatibility only
|
|
73
|
+
export function pathsToVerifiableCredentialPointers({paths} = {}) {
|
|
74
|
+
// get only the paths inside a verifiable credential
|
|
75
|
+
paths = Array.isArray(paths) ? paths : [paths];
|
|
76
|
+
paths = _getVerifiableCredentialPaths(paths);
|
|
77
|
+
// convert each JSON path to a JSON pointer
|
|
78
|
+
return paths.map(_jsonPathToJsonPointer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _filterToValue({filter, strict = false}) {
|
|
82
|
+
/* Each `filter` has a JSON Schema object. In recognition of the fact that
|
|
83
|
+
a query must be usable by common database engines (including perhaps
|
|
84
|
+
encrypted cloud databases) and of the fact that each JSON Schema object will
|
|
85
|
+
come from an untrusted source (and could have malicious regexes, etc.), only
|
|
86
|
+
simple JSON Schema types are supported:
|
|
87
|
+
|
|
88
|
+
simple type filters (`string`/`number`/`boolean`/`object`): with `const` or
|
|
89
|
+
`enum`, `format` is not supported and `pattern` has partial support as it
|
|
90
|
+
will be treated as a simple string not a regex; regex is a DoS attack
|
|
91
|
+
vector
|
|
92
|
+
|
|
93
|
+
`array`: with `contains` where uses a simple type filter
|
|
94
|
+
|
|
95
|
+
`allOf`: supported only with the above schemas present in it.
|
|
96
|
+
|
|
97
|
+
*/
|
|
98
|
+
let value;
|
|
99
|
+
|
|
100
|
+
const {type} = filter;
|
|
101
|
+
if(type === 'array') {
|
|
102
|
+
if(filter.contains) {
|
|
103
|
+
if(Array.isArray(filter.contains)) {
|
|
104
|
+
return filter.contains.map(filter => _filterToValue({filter, strict}));
|
|
105
|
+
}
|
|
106
|
+
return _filterToValue({filter: filter.contains, strict});
|
|
107
|
+
}
|
|
108
|
+
if(Array.isArray(filter.allOf) && filter.allOf.every(f => f.contains)) {
|
|
109
|
+
return filter.allOf.map(
|
|
110
|
+
f => _filterToValue({filter: f.contains, strict}));
|
|
111
|
+
}
|
|
112
|
+
if(strict) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Unsupported filter; array filters must use "allOf" and/or ' +
|
|
115
|
+
'"contains" with a simple type filter.');
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if(VALUE_TYPES.has(type) || type === 'object' || type === undefined) {
|
|
120
|
+
if(filter.const !== undefined) {
|
|
121
|
+
value = filter.const;
|
|
122
|
+
} else if(filter.pattern) {
|
|
123
|
+
value = filter.pattern;
|
|
124
|
+
} else if(filter.enum) {
|
|
125
|
+
value = filter.enum.slice();
|
|
126
|
+
} else if(filter.type === 'object') {
|
|
127
|
+
value = {};
|
|
128
|
+
} else if(strict && type === 'string') {
|
|
129
|
+
throw new Error(
|
|
130
|
+
'Unsupported filter; string filters must use "const" or "pattern".');
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
if(strict) {
|
|
135
|
+
throw new Error(`Unsupported filter type "${type}".`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// exported for testing purposes only
|
|
140
|
+
export function _fromQueryByExampleQuery({credentialQuery, prefixJwtVcPath}) {
|
|
141
|
+
// determine `prefixJwtVcPath` default:
|
|
142
|
+
// if `credentialQuery` specifies `acceptedEnvelopes: ['application/jwt']`,
|
|
143
|
+
// then default `prefixJwtVcPath` to `true`
|
|
144
|
+
if(prefixJwtVcPath === undefined &&
|
|
145
|
+
(Array.isArray(credentialQuery.acceptedEnvelopes) &&
|
|
146
|
+
credentialQuery.acceptedEnvelopes.includes?.('application/jwt'))) {
|
|
147
|
+
prefixJwtVcPath = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fields = [];
|
|
151
|
+
const inputDescriptor = {
|
|
152
|
+
id: crypto.randomUUID(),
|
|
153
|
+
constraints: {fields}
|
|
154
|
+
};
|
|
155
|
+
if(credentialQuery?.reason) {
|
|
156
|
+
inputDescriptor.purpose = credentialQuery?.reason;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// convert `example` into json pointers
|
|
160
|
+
const {example = {}} = credentialQuery || {};
|
|
161
|
+
const pointers = toJsonPointerMap({obj: example});
|
|
162
|
+
|
|
163
|
+
// walk pointers and produce fields
|
|
164
|
+
for(const [pointer, value] of pointers) {
|
|
165
|
+
const path = jsonpointer.parse(pointer);
|
|
166
|
+
|
|
167
|
+
const field = {
|
|
168
|
+
path: [
|
|
169
|
+
JSONPath.toPathString(['$', ...path])
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
// include 'vc' path for queries against JWT payloads instead of VCs
|
|
173
|
+
if(prefixJwtVcPath) {
|
|
174
|
+
field.path.push(JSONPath.toPathString(['$', 'vc', ...path]));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if(value instanceof Set) {
|
|
178
|
+
field.filter = {
|
|
179
|
+
type: 'array',
|
|
180
|
+
allOf: [...value].map(value => ({
|
|
181
|
+
contains: _primitiveValueToFilter(value)
|
|
182
|
+
}))
|
|
183
|
+
};
|
|
184
|
+
} else {
|
|
185
|
+
field.filter = _primitiveValueToFilter(value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fields.push(field);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return inputDescriptor;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _getVerifiableCredentialPaths(paths) {
|
|
195
|
+
// remove any paths that start with what would be present in a
|
|
196
|
+
// presentation submission and adjust any paths that would be part of a
|
|
197
|
+
// JWT-secured VC, such that only actual VC paths remain
|
|
198
|
+
const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
|
|
199
|
+
return [...new Set(removed.map(p => {
|
|
200
|
+
if(_isJWTPath(p)) {
|
|
201
|
+
return '$' + p.slice('$.vc'.length);
|
|
202
|
+
}
|
|
203
|
+
if(_isSquareJWTPath(p)) {
|
|
204
|
+
return '$' + p.slice('$[\'vc\']'.length);
|
|
205
|
+
}
|
|
206
|
+
return p;
|
|
207
|
+
}))];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _isPresentationSubmissionPath(path) {
|
|
211
|
+
return path.startsWith('$.verifiableCredential[') ||
|
|
212
|
+
path.startsWith('$.vp.') ||
|
|
213
|
+
path.startsWith('$[\'verifiableCredential') || path.startsWith('$[\'vp');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _isJWTPath(path) {
|
|
217
|
+
return path.startsWith('$.vc.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _isSquareJWTPath(path) {
|
|
221
|
+
return path.startsWith('$[\'vc\']');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _jsonPathToJsonPointer(jsonPath) {
|
|
225
|
+
return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
|
|
229
|
+
// every input descriptor must have an `id`
|
|
230
|
+
if(typeof inputDescriptor?.id !== 'string') {
|
|
231
|
+
throw new TypeError('Input descriptor "id" must be a string.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const example = {};
|
|
235
|
+
const credentialQuery = {example};
|
|
236
|
+
if(inputDescriptor.purpose) {
|
|
237
|
+
credentialQuery.reason = inputDescriptor.purpose;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Note: Each input descriptor object is currently mapped to a single example
|
|
241
|
+
query. If multiple possible path values appear for a single field, these will
|
|
242
|
+
be mapped to multiple properties in the example which may or may not be what
|
|
243
|
+
is intended. This behavior could be changed in a future revision if it
|
|
244
|
+
becomes clear there is a better approach. */
|
|
245
|
+
|
|
246
|
+
const fields = inputDescriptor.constraints?.fields || [];
|
|
247
|
+
for(const field of fields) {
|
|
248
|
+
const {path, filter, optional} = field;
|
|
249
|
+
// skip optional fields
|
|
250
|
+
if(optional === true) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// each field must have a `path` (which can be a string or an array)
|
|
256
|
+
if(!(Array.isArray(path) || typeof path === 'string')) {
|
|
257
|
+
throw new TypeError(
|
|
258
|
+
'Input descriptor field "path" must be a string or array.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// process any filter
|
|
262
|
+
let value = '';
|
|
263
|
+
if(filter !== undefined) {
|
|
264
|
+
value = _filterToValue({filter, strict});
|
|
265
|
+
}
|
|
266
|
+
// no value understood, skip field
|
|
267
|
+
if(value === undefined) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// normalize value to array
|
|
271
|
+
const originalValue = value;
|
|
272
|
+
if(!Array.isArray(value)) {
|
|
273
|
+
value = [value];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// get JSON pointers for every path inside a verifiable credential
|
|
277
|
+
const pointers = pathsToVerifiableCredentialPointers({paths: path});
|
|
278
|
+
|
|
279
|
+
// add values at each path, converting to an array / appending as needed
|
|
280
|
+
for(const pointer of pointers) {
|
|
281
|
+
const existing = resolvePointer(example, pointer);
|
|
282
|
+
if(existing === undefined) {
|
|
283
|
+
jsonpointer.set(example, pointer, originalValue);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if(Array.isArray(existing)) {
|
|
288
|
+
if(!existing.includes(value)) {
|
|
289
|
+
existing.push(...value);
|
|
290
|
+
}
|
|
291
|
+
} else if(existing !== value) {
|
|
292
|
+
jsonpointer.set(example, pointer, [existing, ...value]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch(cause) {
|
|
296
|
+
const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
|
|
297
|
+
const error = new Error(
|
|
298
|
+
`Could not process input descriptor field: "${id}".`, {cause});
|
|
299
|
+
error.field = field;
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return credentialQuery;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _primitiveValueToFilter(value) {
|
|
308
|
+
const filter = {
|
|
309
|
+
type: typeof value
|
|
310
|
+
};
|
|
311
|
+
if(VALUE_TYPES.has(filter.type)) {
|
|
312
|
+
filter.const = value;
|
|
313
|
+
} else {
|
|
314
|
+
// default to `object`
|
|
315
|
+
filter.type = 'object';
|
|
316
|
+
}
|
|
317
|
+
return filter;
|
|
318
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import {assert} from '../util.js';
|
|
5
|
+
import jsonpointer from 'json-pointer';
|
|
6
|
+
|
|
7
|
+
export function fromJsonPointerMap({map} = {}) {
|
|
8
|
+
assert(map, 'map', Map);
|
|
9
|
+
return _fromPointers({map});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isNumber(x) {
|
|
13
|
+
return typeof toNumberIfNumber(x) === 'number';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isObject(x) {
|
|
17
|
+
return x && typeof x === 'object' && !Array.isArray(x);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolvePointer(obj, pointer) {
|
|
21
|
+
if(pointer === '/') {
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return jsonpointer.get(obj, pointer);
|
|
26
|
+
} catch(e) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// produces a map of deep pointers to primitives and sets; the values in each
|
|
32
|
+
// set share the same pointer value and if any value in the set is an object,
|
|
33
|
+
// it becomes a new map of deep pointers from that starting place; the pointer
|
|
34
|
+
// value for an empty objects will be an empty map
|
|
35
|
+
export function toJsonPointerMap({obj, flat = false} = {}) {
|
|
36
|
+
assert(obj, 'obj', 'object');
|
|
37
|
+
return _toPointers({cursor: obj, map: new Map(), flat});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toNumberIfNumber(x) {
|
|
41
|
+
if(typeof x === 'number') {
|
|
42
|
+
return x;
|
|
43
|
+
}
|
|
44
|
+
const num = parseInt(x, 10);
|
|
45
|
+
if(!isNaN(num)) {
|
|
46
|
+
return num;
|
|
47
|
+
}
|
|
48
|
+
return x;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function _fromPointers({map} = {}) {
|
|
52
|
+
const result = {};
|
|
53
|
+
|
|
54
|
+
for(const [pointer, value] of map) {
|
|
55
|
+
// convert any non-primitive values
|
|
56
|
+
let val = value;
|
|
57
|
+
if(value instanceof Map) {
|
|
58
|
+
val = _fromPointers({map: value});
|
|
59
|
+
} else if(value instanceof Set) {
|
|
60
|
+
val = [...value].map(e => e instanceof Map ? _fromPointers({map: e}) : e);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// if root pointer is used, `value` is result
|
|
64
|
+
if(pointer === '/') {
|
|
65
|
+
return val;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
jsonpointer.set(result, pointer, val);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _toPointers({
|
|
75
|
+
cursor, map, tokens = [], pointer = '/', flat = false
|
|
76
|
+
}) {
|
|
77
|
+
if(!flat && Array.isArray(cursor)) {
|
|
78
|
+
const set = new Set();
|
|
79
|
+
// when `map` is not set, case is array of arrays; return a new map
|
|
80
|
+
const result = map ? set : (map = new Map());
|
|
81
|
+
map.set(pointer, set);
|
|
82
|
+
for(const element of cursor) {
|
|
83
|
+
// reset map, tokens, and pointer for array elements
|
|
84
|
+
set.add(_toPointers({cursor: element, flat}));
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
if(cursor !== null && typeof cursor === 'object') {
|
|
89
|
+
map = map ?? new Map();
|
|
90
|
+
const entries = Object.entries(cursor);
|
|
91
|
+
if(entries.length === 0) {
|
|
92
|
+
// ensure empty object / array case is represented
|
|
93
|
+
map.set(pointer, Array.isArray(cursor) ? new Set() : new Map());
|
|
94
|
+
}
|
|
95
|
+
for(const [token, value] of entries) {
|
|
96
|
+
tokens.push(String(token));
|
|
97
|
+
pointer = jsonpointer.compile(tokens);
|
|
98
|
+
_toPointers({cursor: value, map, tokens, pointer, flat});
|
|
99
|
+
tokens.pop();
|
|
100
|
+
}
|
|
101
|
+
return map;
|
|
102
|
+
}
|
|
103
|
+
map?.set(pointer, cursor);
|
|
104
|
+
return cursor;
|
|
105
|
+
}
|