@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/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} +85 -44
- package/lib/{oid4vp.js → oid4vp/index.js} +3 -6
- package/lib/oid4vp/verifier.js +102 -0
- 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 +328 -0
- package/lib/query/queryByExample.js +8 -0
- package/lib/query/util.js +105 -0
- package/lib/util.js +27 -232
- package/package.json +5 -3
- package/lib/convert.js +0 -430
|
@@ -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
|
+
}
|
package/lib/util.js
CHANGED
|
@@ -6,14 +6,15 @@ import {httpClient} from '@digitalbazaar/http-client';
|
|
|
6
6
|
|
|
7
7
|
const TEXT_ENCODER = new TextEncoder();
|
|
8
8
|
const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
|
|
9
|
-
const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
|
|
10
9
|
|
|
11
10
|
export function assert(x, name, type, optional = false) {
|
|
12
11
|
const article = type === 'object' ? 'an' : 'a';
|
|
13
|
-
|
|
12
|
+
const xType = typeof type === 'string' ?
|
|
13
|
+
typeof x : (x instanceof type && type);
|
|
14
|
+
if(x !== undefined && xType !== type) {
|
|
14
15
|
throw new TypeError(
|
|
15
16
|
`${optional ? 'When present, ' : ''} ` +
|
|
16
|
-
`"${name}" must be ${article} ${type}.`);
|
|
17
|
+
`"${name}" must be ${article} ${type?.name ?? type}.`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -34,108 +35,16 @@ export function base64Encode(data) {
|
|
|
34
35
|
return data.toBase64();
|
|
35
36
|
}
|
|
36
37
|
// note: this is base64-no-pad; will only work with specific data lengths
|
|
37
|
-
base64url.encode(data).replace(/-/g, '+').replace(/_/g, '/');
|
|
38
|
+
return base64url.encode(data).replace(/-/g, '+').replace(/_/g, '/');
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export function createNamedError({message, name, cause} = {}) {
|
|
41
|
+
export function createNamedError({message, name, details, cause} = {}) {
|
|
41
42
|
const error = new Error(message, {cause});
|
|
42
43
|
error.name = name;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
|
|
47
|
-
try {
|
|
48
|
-
assert(issuerConfigUrl, 'issuerConfigUrl', 'string');
|
|
49
|
-
|
|
50
|
-
const response = await fetchJSON({url: issuerConfigUrl, agent});
|
|
51
|
-
if(!response.data) {
|
|
52
|
-
const error = new Error('Issuer configuration format is not JSON.');
|
|
53
|
-
error.name = 'DataError';
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
const {data: issuerMetaData} = response;
|
|
57
|
-
const {issuer, authorization_server} = issuerMetaData;
|
|
58
|
-
|
|
59
|
-
if(authorization_server && authorization_server !== issuer) {
|
|
60
|
-
// not yet implemented
|
|
61
|
-
throw new Error('Separate authorization server not yet implemented.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// validate `issuer`
|
|
65
|
-
if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
|
|
66
|
-
const error = new Error('"issuer" is not an HTTPS URL.');
|
|
67
|
-
error.name = 'DataError';
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ensure `credential_issuer` matches `issuer`, if present
|
|
72
|
-
const {credential_issuer} = issuerMetaData;
|
|
73
|
-
if(credential_issuer !== undefined && credential_issuer !== issuer) {
|
|
74
|
-
const error = new Error('"credential_issuer" must match "issuer".');
|
|
75
|
-
error.name = 'DataError';
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
|
|
80
|
-
|
|
81
|
-
The `origin` and `path` element must be parsed from `issuer` and checked
|
|
82
|
-
against `issuerConfigUrl` like so:
|
|
83
|
-
|
|
84
|
-
For issuer `<origin>` (no path), `issuerConfigUrl` must match:
|
|
85
|
-
`<origin>/.well-known/<any-path-segment>`
|
|
86
|
-
|
|
87
|
-
For issuer `<origin><path>`, `issuerConfigUrl` must be:
|
|
88
|
-
`<origin>/.well-known/<any-path-segment><path>` */
|
|
89
|
-
const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
|
|
90
|
-
const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
|
|
91
|
-
const {origin, pathname} = new URL(issuer);
|
|
92
|
-
let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
|
|
93
|
-
if(pathname !== '/') {
|
|
94
|
-
expectedConfigUrl += pathname;
|
|
95
|
-
}
|
|
96
|
-
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
97
|
-
// alternatively, against RFC 8414, but according to OID4VCI, make sure
|
|
98
|
-
// the issuer config URL matches:
|
|
99
|
-
// <origin><path>/.well-known/<any-path-segment>
|
|
100
|
-
expectedConfigUrl = origin;
|
|
101
|
-
if(pathname !== '/') {
|
|
102
|
-
expectedConfigUrl += pathname;
|
|
103
|
-
}
|
|
104
|
-
expectedConfigUrl += `/.well-known/${anyPathSegment}`;
|
|
105
|
-
if(issuerConfigUrl !== expectedConfigUrl) {
|
|
106
|
-
const error = new Error('"issuer" does not match configuration URL.');
|
|
107
|
-
error.name = 'DataError';
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// fetch AS meta data
|
|
113
|
-
const asMetaDataUrl =
|
|
114
|
-
`${origin}/.well-known/oauth-authorization-server${pathname}`;
|
|
115
|
-
const asMetaDataResponse = await fetchJSON({url: asMetaDataUrl, agent});
|
|
116
|
-
if(!asMetaDataResponse.data) {
|
|
117
|
-
const error = new Error('Authorization server meta data is not JSON.');
|
|
118
|
-
error.name = 'DataError';
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const {data: asMetaData} = response;
|
|
123
|
-
// merge AS meta data into total issuer config
|
|
124
|
-
const issuerConfig = {...issuerMetaData, ...asMetaData};
|
|
125
|
-
|
|
126
|
-
// ensure `token_endpoint` is valid
|
|
127
|
-
const {token_endpoint} = asMetaData;
|
|
128
|
-
assert(token_endpoint, 'token_endpoint', 'string');
|
|
129
|
-
|
|
130
|
-
// return merged config and separate issuer and AS configs
|
|
131
|
-
const metadata = {issuer: issuerMetaData, authorizationServer: asMetaData};
|
|
132
|
-
return {issuerConfig, metadata};
|
|
133
|
-
} catch(cause) {
|
|
134
|
-
const error = new Error('Could not get OpenID issuer configuration.');
|
|
135
|
-
error.name = 'OperationError';
|
|
136
|
-
error.cause = cause;
|
|
137
|
-
throw error;
|
|
44
|
+
if(details) {
|
|
45
|
+
error.details = details;
|
|
138
46
|
}
|
|
47
|
+
return error;
|
|
139
48
|
}
|
|
140
49
|
|
|
141
50
|
export function fetchJSON({url, agent} = {}) {
|
|
@@ -151,126 +60,17 @@ export function fetchJSON({url, agent} = {}) {
|
|
|
151
60
|
return httpClient.get(url, fetchOptions);
|
|
152
61
|
}
|
|
153
62
|
|
|
154
|
-
export
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"aud": "https://server.example.com",
|
|
165
|
-
"iat": 1659145924,
|
|
166
|
-
"nonce": "tZignsnFbp"
|
|
167
|
-
}
|
|
168
|
-
*/
|
|
169
|
-
|
|
170
|
-
if(exp === undefined) {
|
|
171
|
-
// default to 5 minute expiration time
|
|
172
|
-
exp = Math.floor(Date.now() / 1000) + 60 * 5;
|
|
173
|
-
}
|
|
174
|
-
if(nbf === undefined) {
|
|
175
|
-
// default to now
|
|
176
|
-
nbf = Math.floor(Date.now() / 1000);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const {id: kid} = signer;
|
|
180
|
-
const alg = _curveToAlg(signer.algorithm);
|
|
181
|
-
const payload = {nonce, iss, aud, exp, nbf};
|
|
182
|
-
const protectedHeader = {alg, kid};
|
|
183
|
-
|
|
184
|
-
return signJWT({payload, protectedHeader, signer});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function getCredentialOffer({url, agent} = {}) {
|
|
188
|
-
const {protocol, searchParams} = new URL(url);
|
|
189
|
-
if(protocol !== 'openid-credential-offer:') {
|
|
190
|
-
throw new SyntaxError(
|
|
191
|
-
'"url" must express a URL with the ' +
|
|
192
|
-
'"openid-credential-offer" protocol.');
|
|
193
|
-
}
|
|
194
|
-
const offer = searchParams.get('credential_offer');
|
|
195
|
-
if(offer) {
|
|
196
|
-
return JSON.parse(offer);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// try to fetch offer from URL
|
|
200
|
-
const offerUrl = searchParams.get('credential_offer_uri');
|
|
201
|
-
if(!offerUrl) {
|
|
202
|
-
throw new SyntaxError(
|
|
203
|
-
'OID4VCI credential offer must have "credential_offer" or ' +
|
|
204
|
-
'"credential_offer_uri".');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if(!offerUrl.startsWith('https://')) {
|
|
208
|
-
const error = new Error(
|
|
209
|
-
`"credential_offer_uri" (${offerUrl}) must start with "https://".`);
|
|
210
|
-
error.name = 'NotSupportedError';
|
|
211
|
-
throw error;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const response = await fetchJSON({url: offerUrl, agent});
|
|
215
|
-
if(!response.data) {
|
|
216
|
-
const error = new Error(
|
|
217
|
-
`Credential offer fetched from "${offerUrl}" is not JSON.`);
|
|
218
|
-
error.name = 'DataError';
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
return response.data;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function parseCredentialOfferUrl({url} = {}) {
|
|
225
|
-
assert(url, 'url', 'string');
|
|
226
|
-
|
|
227
|
-
/* Parse URL, e.g.:
|
|
228
|
-
|
|
229
|
-
'openid-credential-offer://?' +
|
|
230
|
-
'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
|
|
231
|
-
'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
|
|
232
|
-
'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
|
|
233
|
-
'%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
|
|
234
|
-
'%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
235
|
-
'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
|
|
236
|
-
'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
|
|
237
|
-
'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
|
|
238
|
-
'%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
|
|
239
|
-
'%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
|
|
240
|
-
'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
|
|
241
|
-
*/
|
|
242
|
-
const {protocol, searchParams} = new URL(url);
|
|
243
|
-
if(protocol !== 'openid-credential-offer:') {
|
|
244
|
-
throw new SyntaxError(
|
|
245
|
-
'"url" must express a URL with the ' +
|
|
246
|
-
'"openid-credential-offer" protocol.');
|
|
247
|
-
}
|
|
248
|
-
return JSON.parse(searchParams.get('credential_offer'));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export async function robustDiscoverIssuer({issuer, agent} = {}) {
|
|
252
|
-
// try issuer config URLs based on OID4VCI (first) and RFC 8414 (second)
|
|
253
|
-
const parsedIssuer = new URL(issuer);
|
|
254
|
-
const {origin} = parsedIssuer;
|
|
255
|
-
const path = parsedIssuer.pathname === '/' ? '' : parsedIssuer.pathname;
|
|
256
|
-
|
|
257
|
-
const issuerConfigUrls = [
|
|
258
|
-
// OID4VCI
|
|
259
|
-
`${origin}${path}/.well-known/openid-credential-issuer`,
|
|
260
|
-
// RFC 8414
|
|
261
|
-
`${origin}/.well-known/openid-credential-issuer${path}`
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
let error;
|
|
265
|
-
for(const issuerConfigUrl of issuerConfigUrls) {
|
|
266
|
-
try {
|
|
267
|
-
const config = await discoverIssuer({issuerConfigUrl, agent});
|
|
268
|
-
return config;
|
|
269
|
-
} catch(e) {
|
|
270
|
-
error = e;
|
|
271
|
-
}
|
|
63
|
+
export function parseJSON(x, name) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(x);
|
|
66
|
+
} catch(cause) {
|
|
67
|
+
throw createNamedError({
|
|
68
|
+
message: `Could not parse "${name}".`,
|
|
69
|
+
name: 'DataError',
|
|
70
|
+
details: {httpStatusCode: 400, public: true},
|
|
71
|
+
cause
|
|
72
|
+
});
|
|
272
73
|
}
|
|
273
|
-
throw error;
|
|
274
74
|
}
|
|
275
75
|
|
|
276
76
|
export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
|
|
@@ -316,6 +116,14 @@ export function selectJwk({keys, kid, alg, kty, crv, use} = {}) {
|
|
|
316
116
|
});
|
|
317
117
|
}
|
|
318
118
|
|
|
119
|
+
export async function sha256(data) {
|
|
120
|
+
if(typeof data === 'string') {
|
|
121
|
+
data = new TextEncoder().encode(data);
|
|
122
|
+
}
|
|
123
|
+
const algorithm = {name: 'SHA-256'};
|
|
124
|
+
return new Uint8Array(await crypto.subtle.digest(algorithm, data));
|
|
125
|
+
}
|
|
126
|
+
|
|
319
127
|
export async function signJWT({payload, protectedHeader, signer} = {}) {
|
|
320
128
|
// encode payload and protected header
|
|
321
129
|
const b64Payload = base64url.encode(JSON.stringify(payload));
|
|
@@ -343,16 +151,3 @@ export async function signJWT({payload, protectedHeader, signer} = {}) {
|
|
|
343
151
|
// create compact JWT
|
|
344
152
|
return `${jws.protected}.${jws.payload}.${jws.signature}`;
|
|
345
153
|
}
|
|
346
|
-
|
|
347
|
-
function _curveToAlg(crv) {
|
|
348
|
-
if(crv === 'Ed25519' || crv === 'Ed448') {
|
|
349
|
-
return 'EdDSA';
|
|
350
|
-
}
|
|
351
|
-
if(crv?.startsWith('P-')) {
|
|
352
|
-
return `ES${crv.slice(2)}`;
|
|
353
|
-
}
|
|
354
|
-
if(crv === 'secp256k1') {
|
|
355
|
-
return 'ES256K';
|
|
356
|
-
}
|
|
357
|
-
return crv;
|
|
358
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalbazaar/oid4-client",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "An OID4 (VC + VP) client",
|
|
5
5
|
"homepage": "https://github.com/digitalbazaar/oid4-client",
|
|
6
6
|
"author": {
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@digitalbazaar/http-client": "^4.0.0",
|
|
27
27
|
"base64url-universal": "^2.0.0",
|
|
28
|
-
"jose": "^6.0
|
|
28
|
+
"jose": "^6.1.0",
|
|
29
|
+
"json-pointer": "^0.6.2",
|
|
29
30
|
"jsonpath-plus": "^10.3.0",
|
|
30
|
-
"jsonpointer": "^5.0.1",
|
|
31
31
|
"pkijs": "^3.2.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@auth0/mdl": "^3.0.0",
|
|
35
|
+
"asn1js": "^3.0.6",
|
|
34
36
|
"c8": "^10.1.3",
|
|
35
37
|
"chai": "^4.3.6",
|
|
36
38
|
"cross-env": "^10.0.0",
|