@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.
@@ -0,0 +1,244 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {
5
+ fromJsonPointerMap, isNumber, toJsonPointerMap, toNumberIfNumber
6
+ } from './util.js';
7
+ import {exampleToJsonPointerMap} from './queryByExample.js';
8
+ import jsonpointer from 'json-pointer';
9
+
10
+ const MDOC_MDL = 'org.iso.18013.5.1.mDL';
11
+
12
+ export function dcqlQueryToVprGroups({dcql_query} = {}) {
13
+ const {credentials} = dcql_query;
14
+ let {credential_sets: credentialSets} = dcql_query;
15
+ if(!credentialSets) {
16
+ credentialSets = [{
17
+ options: [credentials.map(q => q.id)]
18
+ }];
19
+ }
20
+
21
+ // convert `credentials` into a map based on `ID`
22
+ const credentialQueryMap = new Map(credentials.map(
23
+ query => [query.id, query]));
24
+
25
+ // output group map
26
+ const groupMap = new Map();
27
+ for(const credentialSet of credentialSets) {
28
+ for(const option of credentialSet.options) {
29
+ const groupId = crypto.randomUUID();
30
+ const queries = option.map(id => {
31
+ const dcqlCredentialQuery = credentialQueryMap.get(id);
32
+ return {
33
+ type: 'QueryByExample',
34
+ group: groupId,
35
+ credentialQuery: _toQueryByExampleQuery({dcqlCredentialQuery})
36
+ };
37
+ });
38
+ groupMap.set(groupId, new Map([['QueryByExample', queries]]));
39
+ }
40
+ }
41
+ return groupMap;
42
+ }
43
+
44
+ export function dcqlCredentialQueryToJsonPointerMap({
45
+ dcqlCredentialQuery
46
+ } = {}) {
47
+ const queryByExample = _toQueryByExampleQuery({dcqlCredentialQuery});
48
+ return exampleToJsonPointerMap(queryByExample);
49
+ }
50
+
51
+ export function vprGroupsToDcqlQuery({groupMap, options = {}} = {}) {
52
+ // if there is any DCQL query, return it as-is
53
+ const groups = [...groupMap.values()];
54
+ let dcqlQuery = groups
55
+ .filter(g => g.has('DigitalCredentialQueryLanguage'))
56
+ .map(g => g.get('DigitalCredentialQueryLanguage'))
57
+ .at(-1);
58
+ if(dcqlQuery) {
59
+ return dcqlQuery;
60
+ }
61
+
62
+ // convert all `QueryByExample` queries to a single DCQL query
63
+ const {nullyifyArrayIndices = false} = options;
64
+ dcqlQuery = {};
65
+ const credentials = [];
66
+ const credentialSets = [{options: []}];
67
+
68
+ // note: same group ID is logical "AND" and different group ID is "OR"
69
+ for(const queries of groups) {
70
+ // only `QueryByExample` is convertible
71
+ const queryByExamples = queries.get('QueryByExample');
72
+ if(!(queryByExamples?.length > 0)) {
73
+ continue;
74
+ }
75
+
76
+ // for each `QueryByExample`, add another option
77
+ const option = [];
78
+ for(const queryByExample of queryByExamples) {
79
+ // should only be one `credentialQuery` but handle each one as a new
80
+ // DCQL credential query
81
+ const all = Array.isArray(queryByExample.credentialQuery) ?
82
+ queryByExample.credentialQuery : [queryByExample.credentialQuery];
83
+ for(const credentialQuery of all) {
84
+ const result = _fromQueryByExampleQuery({
85
+ credentialQuery, nullyifyArrayIndices
86
+ });
87
+ credentials.push(result);
88
+ // DCQL credential set "option" includes all queries in the "AND" group
89
+ option.push(result.id);
90
+ }
91
+ }
92
+
93
+ // add "option" as another "OR" in the DCQL "options"
94
+ credentialSets[0].options.push(option);
95
+ }
96
+
97
+ if(credentials.length > 0) {
98
+ dcqlQuery.credentials = credentials;
99
+ }
100
+ if(credentialSets.length > 0) {
101
+ dcqlQuery.credential_sets = credentialSets;
102
+ }
103
+
104
+ return dcqlQuery;
105
+ }
106
+
107
+ // exported for testing purposes only
108
+ export function _fromQueryByExampleQuery({
109
+ credentialQuery, nullyifyArrayIndices = false
110
+ }) {
111
+ const result = {
112
+ id: crypto.randomUUID(),
113
+ format: 'ldp_vc',
114
+ meta: {
115
+ type_values: ['https://www.w3.org/2018/credentials#VerifiableCredential']
116
+ }
117
+ };
118
+ if(credentialQuery?.reason) {
119
+ result.meta.reason = credentialQuery.reason;
120
+ }
121
+
122
+ const {example = {}} = credentialQuery ?? {};
123
+
124
+ // determine credential format
125
+ if(Array.isArray(credentialQuery.acceptedEnvelopes)) {
126
+ const set = new Set(credentialQuery.acceptedEnvelopes);
127
+ if(set.has('application/jwt')) {
128
+ result.format = 'jwt_vc_json';
129
+ } else if(set.has('application/mdl')) {
130
+ result.format = 'mso_mdoc';
131
+ result.meta = {doctype_value: MDOC_MDL};
132
+ } else if(set.has('application/dc+sd-jwt')) {
133
+ result.format = 'dc+sd-jwt';
134
+ result.meta = {vct_values: []};
135
+ if(Array.isArray(example?.type)) {
136
+ result.meta.vct_values.push(...example.type);
137
+ } else if(typeof example.type === 'string') {
138
+ result.meta.vct_values.push(example.type);
139
+ }
140
+ }
141
+ }
142
+
143
+ // convert `example` into json pointers and walk to produce DCQL claim paths
144
+ const pointers = toJsonPointerMap({obj: example, flat: true});
145
+ const pathsMap = new Map();
146
+ for(const [pointer, value] of pointers) {
147
+ // parse path into DCQL path w/ numbers for array indexes
148
+ let path = jsonpointer.parse(pointer).map(toNumberIfNumber);
149
+
150
+ // special process non-`@context` paths to convert some array indexes
151
+ // to DCQL `null` (which means "any" index)
152
+ if(path[0] !== '@context') {
153
+ if(nullyifyArrayIndices) {
154
+ // brute force convert every array index to `null` by request
155
+ path = path.map(p => isNumber(p) ? null : p);
156
+ } else if(isNumber(path.at(-1)) && !isNumber(path.at(-2))) {
157
+ // when a pointer terminates at an array element it means candidate
158
+ // matching values are expressed in the `example`, so make sure to
159
+ // share the path for all candidates
160
+ path[path.length - 1] = null;
161
+ }
162
+ }
163
+
164
+ // compile processed path back to a key to consolidate `values`
165
+ const key = jsonpointer.compile(path.map(p => p === null ? 'null' : p));
166
+
167
+ // create shared entry for path and candidate matching values
168
+ let entry = pathsMap.get(key);
169
+ if(!entry) {
170
+ entry = {path, valueSet: new Set()};
171
+ pathsMap.set(key, entry);
172
+ }
173
+
174
+ // add any non-QueryByExample-wildcard as a DCQL candidate match value
175
+ if(!(value === '' || value instanceof Map || value instanceof Set)) {
176
+ entry.valueSet.add(value);
177
+ }
178
+ }
179
+
180
+ // produce DCQL `claims`
181
+ const claims = [...pathsMap.values()].map(({path, valueSet}) => {
182
+ const claim = {path};
183
+ if(valueSet.size > 0) {
184
+ claim.values = [...valueSet];
185
+ }
186
+ return claim;
187
+ });
188
+
189
+ if(claims.length > 0) {
190
+ result.claims = claims;
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ // exported for testing purposes only
197
+ export function _toQueryByExampleQuery({dcqlCredentialQuery}) {
198
+ // convert DCQL credential query to pointers
199
+ const pointers = new Map();
200
+ const {format, meta, claims = []} = dcqlCredentialQuery;
201
+ for(const claim of claims) {
202
+ const {values} = claim;
203
+
204
+ // a trailing `null` in a path means `values` should be treated as a set
205
+ // of candidates inside an array value at path-1
206
+ const path = claim.path?.at(-1) === null ?
207
+ claim.path.slice(0, -1) : claim.path;
208
+
209
+ // convert `null` path tokens to an index; assume the use of `null` will
210
+ // not be combined with any other index
211
+ const pointer = jsonpointer.compile(path.map(p => p === null ? '0' : p));
212
+ if(!values) {
213
+ pointers.set(pointer, '');
214
+ } else if(values.length === 1 && claim.path.at(-1) !== null) {
215
+ // convert a single choice for a non-array value to a primitive
216
+ pointers.set(pointer, values[0]);
217
+ } else {
218
+ pointers.set(pointer, new Set(values));
219
+ }
220
+ }
221
+
222
+ const credentialQuery = {};
223
+ if(meta?.reason) {
224
+ credentialQuery.reason = meta.reason;
225
+ }
226
+
227
+ // convert pointers to example object
228
+ credentialQuery.example = fromJsonPointerMap({map: pointers});
229
+
230
+ if(format === 'jwt_vc_json') {
231
+ credentialQuery.acceptedEnvelopes = ['application/jwt'];
232
+ } else if(format === 'mso_mdoc') {
233
+ if(meta?.doctype_value === MDOC_MDL) {
234
+ credentialQuery.acceptedEnvelopes = ['application/mdl'];
235
+ } else {
236
+ credentialQuery.acceptedEnvelopes = ['application/mdoc'];
237
+ }
238
+ } else if(format === 'dc+sd-jwt') {
239
+ // FIXME: consider adding `vct_values` as params
240
+ credentialQuery.acceptedEnvelopes = ['application/dc+sd-jwt'];
241
+ }
242
+
243
+ return credentialQuery;
244
+ }
@@ -0,0 +1,18 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {dcqlCredentialQueryToJsonPointerMap} from './dcql.js';
5
+ import {exampleToJsonPointerMap} from './queryByExample.js';
6
+ import {inputDescriptorToJsonPointerMap} from './presentationExchange.js';
7
+
8
+ export {credentialMatches} from './match.js';
9
+
10
+ export const dcql = {
11
+ toJsonPointerMap: dcqlCredentialQueryToJsonPointerMap
12
+ };
13
+ export const presentationExchange = {
14
+ toJsonPointerMap: inputDescriptorToJsonPointerMap
15
+ };
16
+ export const queryByExample = {
17
+ toJsonPointerMap: exampleToJsonPointerMap
18
+ };
@@ -0,0 +1,80 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {isObject, resolvePointer, toNumberIfNumber} from './util.js';
5
+
6
+ /**
7
+ * Returns whether a credential matches against a JSON pointer map.
8
+ *
9
+ * A JSON pointer map must be created from a QueryByExample `example`, a DCQL
10
+ * `credential` query value, or a Presentation Exchange input descriptor
11
+ * by calling the respective utility APIs in this library. It is more efficient
12
+ * to produce this JSON pointer map just once when looking for matches in a
13
+ * list of more than one credential.
14
+ *
15
+ * @param {object} options - The options.
16
+ * @param {object} options.credential - The credential to try to match.
17
+ * @param {Map} options.map - The JSON pointer map.
18
+ * @param {object} options.options - Match options, such as:
19
+ * [coerceNumbers=true] - String/numbers will be coerced.
20
+ *
21
+ * @returns {boolean} `true` if the credential matches, `false` if not.
22
+ */
23
+ export function credentialMatches({
24
+ credential, map, options = {coerceNumbers: true}
25
+ } = {}) {
26
+ // credential must be an object to match
27
+ if(!isObject(credential)) {
28
+ return false;
29
+ }
30
+ return _match({cursor: credential, matchValue: map, options});
31
+ }
32
+
33
+ function _match({cursor, matchValue, options}) {
34
+ // handle wildcard matching
35
+ if(_isWildcard(matchValue)) {
36
+ return true;
37
+ }
38
+
39
+ if(matchValue instanceof Set) {
40
+ // some element in the set must match `cursor`
41
+ return [...matchValue].some(e => _match({cursor, matchValue: e, options}));
42
+ }
43
+
44
+ if(matchValue instanceof Map) {
45
+ // all pointers and values in the map must match `cursor`
46
+ return [...matchValue.entries()].every(([pointer, matchValue]) => {
47
+ const value = resolvePointer(cursor, pointer);
48
+ if(value === undefined) {
49
+ // no value at `pointer`; no match
50
+ return false;
51
+ }
52
+ // handles case where `value` is an empty array + wildcard `matchValue`
53
+ if(_isWildcard(matchValue)) {
54
+ return true;
55
+ }
56
+ // normalize value to an array for matching
57
+ const values = Array.isArray(value) ? value : [value];
58
+ return values.some(v => _match({cursor: v, matchValue, options}));
59
+ });
60
+ }
61
+
62
+ // primitive comparison
63
+ if(cursor === matchValue) {
64
+ return true;
65
+ }
66
+
67
+ // string/number coercion
68
+ if(options.coerceNumbers) {
69
+ const cursorNumber = toNumberIfNumber(cursor);
70
+ const matchNumber = toNumberIfNumber(matchValue);
71
+ return cursorNumber !== undefined && cursorNumber === matchNumber;
72
+ }
73
+
74
+ return false;
75
+ }
76
+
77
+ function _isWildcard(value) {
78
+ // empty string, Map, or Set
79
+ return value === '' || value?.size === 0;
80
+ }
@@ -0,0 +1,328 @@
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
+ // only a single `QueryByExample` is supported at this time; use last one
40
+ const queryByExample = [...groupMap.values()]
41
+ .filter(g => g.has('QueryByExample'))
42
+ .map(g => g.get('QueryByExample'))
43
+ .at(-1);
44
+ if(!queryByExample) {
45
+ // no presentation definition
46
+ return;
47
+ }
48
+
49
+ const input_descriptors = [];
50
+ const presentationDefinition = {
51
+ id: crypto.randomUUID(),
52
+ input_descriptors
53
+ };
54
+
55
+ // note: same group ID is logical "AND" and different group ID is "OR"
56
+ const groups = [...groupMap.values()];
57
+ for(const queries of groups) {
58
+ // only `QueryByExample` is convertible
59
+ const queryByExamples = queries.get('QueryByExample');
60
+ if(!(queryByExamples?.length > 0)) {
61
+ continue;
62
+ }
63
+
64
+ // for each `QueryByExample`, add another input descriptor (for every
65
+ // "credentialQuery" within it
66
+ for(const queryByExample of queryByExamples) {
67
+ // should only be one `credentialQuery` but handle each one as a new
68
+ // set of input descriptors
69
+ const all = Array.isArray(queryByExample.credentialQuery) ?
70
+ queryByExample.credentialQuery : [queryByExample.credentialQuery];
71
+ for(const credentialQuery of all) {
72
+ input_descriptors.push(_fromQueryByExampleQuery({
73
+ credentialQuery, prefixJwtVcPath
74
+ }));
75
+ }
76
+ }
77
+ }
78
+
79
+ return presentationDefinition;
80
+ }
81
+
82
+ // exported for backwards compatibility only
83
+ export function pathsToVerifiableCredentialPointers({paths} = {}) {
84
+ // get only the paths inside a verifiable credential
85
+ paths = Array.isArray(paths) ? paths : [paths];
86
+ paths = _getVerifiableCredentialPaths(paths);
87
+ // convert each JSON path to a JSON pointer
88
+ return paths.map(_jsonPathToJsonPointer);
89
+ }
90
+
91
+ function _filterToValue({filter, strict = false}) {
92
+ /* Each `filter` has a JSON Schema object. In recognition of the fact that
93
+ a query must be usable by common database engines (including perhaps
94
+ encrypted cloud databases) and of the fact that each JSON Schema object will
95
+ come from an untrusted source (and could have malicious regexes, etc.), only
96
+ simple JSON Schema types are supported:
97
+
98
+ simple type filters (`string`/`number`/`boolean`/`object`): with `const` or
99
+ `enum`, `format` is not supported and `pattern` has partial support as it
100
+ will be treated as a simple string not a regex; regex is a DoS attack
101
+ vector
102
+
103
+ `array`: with `contains` where uses a simple type filter
104
+
105
+ `allOf`: supported only with the above schemas present in it.
106
+
107
+ */
108
+ let value;
109
+
110
+ const {type} = filter;
111
+ if(type === 'array') {
112
+ if(filter.contains) {
113
+ if(Array.isArray(filter.contains)) {
114
+ return filter.contains.map(filter => _filterToValue({filter, strict}));
115
+ }
116
+ return _filterToValue({filter: filter.contains, strict});
117
+ }
118
+ if(Array.isArray(filter.allOf) && filter.allOf.every(f => f.contains)) {
119
+ return filter.allOf.map(
120
+ f => _filterToValue({filter: f.contains, strict}));
121
+ }
122
+ if(strict) {
123
+ throw new Error(
124
+ 'Unsupported filter; array filters must use "allOf" and/or ' +
125
+ '"contains" with a simple type filter.');
126
+ }
127
+ return value;
128
+ }
129
+ if(VALUE_TYPES.has(type) || type === 'object' || type === undefined) {
130
+ if(filter.const !== undefined) {
131
+ value = filter.const;
132
+ } else if(filter.pattern) {
133
+ value = filter.pattern;
134
+ } else if(filter.enum) {
135
+ value = filter.enum.slice();
136
+ } else if(filter.type === 'object') {
137
+ value = {};
138
+ } else if(strict && type === 'string') {
139
+ throw new Error(
140
+ 'Unsupported filter; string filters must use "const" or "pattern".');
141
+ }
142
+ return value;
143
+ }
144
+ if(strict) {
145
+ throw new Error(`Unsupported filter type "${type}".`);
146
+ }
147
+ }
148
+
149
+ // exported for testing purposes only
150
+ export function _fromQueryByExampleQuery({credentialQuery, prefixJwtVcPath}) {
151
+ // determine `prefixJwtVcPath` default:
152
+ // if `credentialQuery` specifies `acceptedEnvelopes: ['application/jwt']`,
153
+ // then default `prefixJwtVcPath` to `true`
154
+ if(prefixJwtVcPath === undefined &&
155
+ (Array.isArray(credentialQuery.acceptedEnvelopes) &&
156
+ credentialQuery.acceptedEnvelopes.includes?.('application/jwt'))) {
157
+ prefixJwtVcPath = true;
158
+ }
159
+
160
+ const fields = [];
161
+ const inputDescriptor = {
162
+ id: crypto.randomUUID(),
163
+ constraints: {fields}
164
+ };
165
+ if(credentialQuery?.reason) {
166
+ inputDescriptor.purpose = credentialQuery?.reason;
167
+ }
168
+
169
+ // convert `example` into json pointers
170
+ const {example = {}} = credentialQuery || {};
171
+ const pointers = toJsonPointerMap({obj: example});
172
+
173
+ // walk pointers and produce fields
174
+ for(const [pointer, value] of pointers) {
175
+ const path = jsonpointer.parse(pointer);
176
+
177
+ const field = {
178
+ path: [
179
+ JSONPath.toPathString(['$', ...path])
180
+ ]
181
+ };
182
+ // include 'vc' path for queries against JWT payloads instead of VCs
183
+ if(prefixJwtVcPath) {
184
+ field.path.push(JSONPath.toPathString(['$', 'vc', ...path]));
185
+ }
186
+
187
+ if(value instanceof Set) {
188
+ field.filter = {
189
+ type: 'array',
190
+ allOf: [...value].map(value => ({
191
+ contains: _primitiveValueToFilter(value)
192
+ }))
193
+ };
194
+ } else {
195
+ field.filter = _primitiveValueToFilter(value);
196
+ }
197
+
198
+ fields.push(field);
199
+ }
200
+
201
+ return inputDescriptor;
202
+ }
203
+
204
+ function _getVerifiableCredentialPaths(paths) {
205
+ // remove any paths that start with what would be present in a
206
+ // presentation submission and adjust any paths that would be part of a
207
+ // JWT-secured VC, such that only actual VC paths remain
208
+ const removed = paths.filter(p => !_isPresentationSubmissionPath(p));
209
+ return [...new Set(removed.map(p => {
210
+ if(_isJWTPath(p)) {
211
+ return '$' + p.slice('$.vc'.length);
212
+ }
213
+ if(_isSquareJWTPath(p)) {
214
+ return '$' + p.slice('$[\'vc\']'.length);
215
+ }
216
+ return p;
217
+ }))];
218
+ }
219
+
220
+ function _isPresentationSubmissionPath(path) {
221
+ return path.startsWith('$.verifiableCredential[') ||
222
+ path.startsWith('$.vp.') ||
223
+ path.startsWith('$[\'verifiableCredential') || path.startsWith('$[\'vp');
224
+ }
225
+
226
+ function _isJWTPath(path) {
227
+ return path.startsWith('$.vc.');
228
+ }
229
+
230
+ function _isSquareJWTPath(path) {
231
+ return path.startsWith('$[\'vc\']');
232
+ }
233
+
234
+ function _jsonPathToJsonPointer(jsonPath) {
235
+ return JSONPath.toPointer(JSONPath.toPathArray(jsonPath));
236
+ }
237
+
238
+ function _toQueryByExampleQuery({inputDescriptor, strict = false}) {
239
+ // every input descriptor must have an `id`
240
+ if(typeof inputDescriptor?.id !== 'string') {
241
+ throw new TypeError('Input descriptor "id" must be a string.');
242
+ }
243
+
244
+ const example = {};
245
+ const credentialQuery = {example};
246
+ if(inputDescriptor.purpose) {
247
+ credentialQuery.reason = inputDescriptor.purpose;
248
+ }
249
+
250
+ /* Note: Each input descriptor object is currently mapped to a single example
251
+ query. If multiple possible path values appear for a single field, these will
252
+ be mapped to multiple properties in the example which may or may not be what
253
+ is intended. This behavior could be changed in a future revision if it
254
+ becomes clear there is a better approach. */
255
+
256
+ const fields = inputDescriptor.constraints?.fields || [];
257
+ for(const field of fields) {
258
+ const {path, filter, optional} = field;
259
+ // skip optional fields
260
+ if(optional === true) {
261
+ continue;
262
+ }
263
+
264
+ try {
265
+ // each field must have a `path` (which can be a string or an array)
266
+ if(!(Array.isArray(path) || typeof path === 'string')) {
267
+ throw new TypeError(
268
+ 'Input descriptor field "path" must be a string or array.');
269
+ }
270
+
271
+ // process any filter
272
+ let value = '';
273
+ if(filter !== undefined) {
274
+ value = _filterToValue({filter, strict});
275
+ }
276
+ // no value understood, skip field
277
+ if(value === undefined) {
278
+ continue;
279
+ }
280
+ // normalize value to array
281
+ const originalValue = value;
282
+ if(!Array.isArray(value)) {
283
+ value = [value];
284
+ }
285
+
286
+ // get JSON pointers for every path inside a verifiable credential
287
+ const pointers = pathsToVerifiableCredentialPointers({paths: path});
288
+
289
+ // add values at each path, converting to an array / appending as needed
290
+ for(const pointer of pointers) {
291
+ const existing = resolvePointer(example, pointer);
292
+ if(existing === undefined) {
293
+ jsonpointer.set(example, pointer, originalValue);
294
+ continue;
295
+ }
296
+
297
+ if(Array.isArray(existing)) {
298
+ if(!existing.includes(value)) {
299
+ existing.push(...value);
300
+ }
301
+ } else if(existing !== value) {
302
+ jsonpointer.set(example, pointer, [existing, ...value]);
303
+ }
304
+ }
305
+ } catch(cause) {
306
+ const id = field.id || (JSON.stringify(field).slice(0, 50) + ' ...');
307
+ const error = new Error(
308
+ `Could not process input descriptor field: "${id}".`, {cause});
309
+ error.field = field;
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ return credentialQuery;
315
+ }
316
+
317
+ function _primitiveValueToFilter(value) {
318
+ const filter = {
319
+ type: typeof value
320
+ };
321
+ if(VALUE_TYPES.has(filter.type)) {
322
+ filter.const = value;
323
+ } else {
324
+ // default to `object`
325
+ filter.type = 'object';
326
+ }
327
+ return filter;
328
+ }
@@ -0,0 +1,8 @@
1
+ /*!
2
+ * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {toJsonPointerMap} from './util.js';
5
+
6
+ export function exampleToJsonPointerMap({example} = {}) {
7
+ return toJsonPointerMap({obj: example, flat: false});
8
+ }