@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,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
- if(x !== undefined && typeof x !== type) {
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
- return error;
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 async function generateDIDProofJWT({
155
- signer, nonce, iss, aud, exp, nbf
156
- } = {}) {
157
- /* Example:
158
- {
159
- "alg": "ES256",
160
- "kid":"did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1"
161
- }.
162
- {
163
- "iss": "s6BhdRkqt3",
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.0.0",
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.13",
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",