@digitalbazaar/oid4-client 2.0.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022-2023, Digital Bazaar, Inc.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # oid4-client
2
+ An OID4 client
@@ -0,0 +1,385 @@
1
+ /*!
2
+ * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {discoverIssuer, generateDIDProofJWT} from './util.js';
5
+ import {httpClient} from '@digitalbazaar/http-client';
6
+
7
+ const GRANT_TYPES = new Map([
8
+ ['preAuthorizedCode', 'urn:ietf:params:oauth:grant-type:pre-authorized_code']
9
+ ]);
10
+ const HEADERS = {accept: 'application/json'};
11
+
12
+ export class OID4Client {
13
+ constructor({accessToken = null, agent, issuerConfig, offer} = {}) {
14
+ this.accessToken = accessToken;
15
+ this.agent = agent;
16
+ this.issuerConfig = issuerConfig;
17
+ this.offer = offer;
18
+ }
19
+
20
+ async requestCredential({
21
+ credentialDefinition, did, didProofSigner, agent
22
+ } = {}) {
23
+ const {issuerConfig, offer} = this;
24
+ let requests;
25
+ if(credentialDefinition === undefined) {
26
+ if(!offer) {
27
+ throw new TypeError('"credentialDefinition" must be an object.');
28
+ }
29
+ requests = _createCredentialRequestsFromOffer({issuerConfig, offer});
30
+ if(requests.length > 1) {
31
+ throw new Error(
32
+ 'More than one credential is offered; ' +
33
+ 'use "requestCredentials()" instead.');
34
+ }
35
+ } else {
36
+ requests = [{
37
+ format: 'ldp_vc',
38
+ credential_definition: credentialDefinition
39
+ }];
40
+ }
41
+ return this.requestCredentials({requests, did, didProofSigner, agent});
42
+ }
43
+
44
+ async requestCredentials({
45
+ requests, did, didProofSigner, agent, alwaysUseBatchEndpoint = false
46
+ } = {}) {
47
+ const {issuerConfig, offer} = this;
48
+ if(requests === undefined && offer) {
49
+ requests = _createCredentialRequestsFromOffer({issuerConfig, offer});
50
+ } else if(!(Array.isArray(requests) && requests.length > 0)) {
51
+ throw new TypeError('"requests" must be an array of length >= 1.');
52
+ }
53
+ requests.forEach(_assertRequest);
54
+ // set default `format`
55
+ requests = requests.map(r => ({format: 'ldp_vc', ...r}));
56
+
57
+ try {
58
+ /* First send credential request(s) to DS without DID proof JWT, e.g.:
59
+
60
+ POST /credential HTTP/1.1
61
+ Host: server.example.com
62
+ Content-Type: application/json
63
+ Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
64
+
65
+ {
66
+ "format": "ldp_vc",
67
+ "credential_definition": {...},
68
+ // only present on retry after server requests it
69
+ "proof": {
70
+ "proof_type": "jwt",
71
+ "jwt": "eyJraW..."
72
+ }
73
+ }
74
+
75
+ OR (if multiple `requests` were given)
76
+
77
+ POST /batch_credential HTTP/1.1
78
+ Host: server.example.com
79
+ Content-Type: application/json
80
+ Authorization: BEARER czZCaGRSa3F0MzpnWDFmQmF0M2JW
81
+
82
+ {
83
+ "credential_requests": [{
84
+ "format": "ldp_vc",
85
+ "credential_definition": {...},
86
+ // only present on retry after server requests it
87
+ "proof": {
88
+ "proof_type": "jwt",
89
+ "jwt": "eyJraW..."
90
+ }
91
+ }, {
92
+ ...
93
+ }]
94
+ }
95
+ */
96
+ let url;
97
+ let json;
98
+ if(requests.length > 1 || alwaysUseBatchEndpoint) {
99
+ ({batch_credential_endpoint: url} = this.issuerConfig);
100
+ json = {credential_requests: requests};
101
+ } else {
102
+ ({credential_endpoint: url} = this.issuerConfig);
103
+ json = {...requests[0]};
104
+ }
105
+
106
+ let result;
107
+ const headers = {
108
+ ...HEADERS,
109
+ authorization: `Bearer ${this.accessToken}`
110
+ };
111
+ for(let retries = 0; retries <= 1; ++retries) {
112
+ try {
113
+ const response = await httpClient.post(url, {agent, headers, json});
114
+ result = response.data;
115
+ if(!result) {
116
+ const error = new Error('Credential response format is not JSON.');
117
+ error.name = 'DataError';
118
+ throw error;
119
+ }
120
+ break;
121
+ } catch(cause) {
122
+ if(!_isMissingProofError(cause)) {
123
+ // non-specific error case
124
+ throw cause;
125
+ }
126
+
127
+ // if `didProofSigner` is not provided, throw error
128
+ if(!(did && didProofSigner)) {
129
+ const {data: details} = cause;
130
+ const error = new Error('DID authentication is required.');
131
+ error.name = 'NotAllowedError';
132
+ error.cause = cause;
133
+ error.details = details;
134
+ throw error;
135
+ }
136
+
137
+ // validate that `result` has
138
+ const {data: {c_nonce: nonce}} = cause;
139
+ if(!(nonce && typeof nonce === 'string')) {
140
+ const error = new Error('No DID proof challenge specified.');
141
+ error.name = 'DataError';
142
+ throw error;
143
+ }
144
+
145
+ // generate a DID proof JWT
146
+ const {issuer: aud} = this.issuerConfig;
147
+ const jwt = await generateDIDProofJWT({
148
+ signer: didProofSigner,
149
+ nonce,
150
+ // the entity identified by the DID is issuing this JWT
151
+ iss: did,
152
+ // audience MUST be the target issuer per the OID4VC spec
153
+ aud
154
+ });
155
+
156
+ // add proof to body to be posted and loop to retry
157
+ const proof = {proof_type: 'jwt', jwt};
158
+ if(json.credential_requests) {
159
+ json.credential_requests = json.credential_requests.map(
160
+ cr => ({...cr, proof}));
161
+ } else {
162
+ json.proof = proof;
163
+ }
164
+ }
165
+ }
166
+
167
+ // wallet / client receives credential:
168
+ /* Note: The credential is not wrapped here in a VP in the current spec:
169
+
170
+ HTTP/1.1 200 OK
171
+ Content-Type: application/json
172
+ Cache-Control: no-store
173
+
174
+ {
175
+ "format": "ldp_vc"
176
+ "credential" : {...}
177
+ }
178
+
179
+ OR (if multiple `requests` were given)
180
+
181
+ {
182
+ "credential_responses": [{
183
+ "format": "ldp_vc",
184
+ "credential": {...}
185
+ }]
186
+ }
187
+ */
188
+ return result;
189
+ } catch(cause) {
190
+ const error = new Error('Could not receive credentials.');
191
+ error.name = 'OperationError';
192
+ error.cause = cause;
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ // create a client from a credential offer
198
+ static async fromCredentialOffer({offer, agent} = {}) {
199
+ // validate offer
200
+ const {credential_issuer, credentials, grants = {}} = offer;
201
+ let parsedIssuer;
202
+ try {
203
+ parsedIssuer = new URL(credential_issuer);
204
+ if(parsedIssuer.protocol !== 'https:') {
205
+ throw new Error('Only "https" credential issuer URLs are supported.');
206
+ }
207
+ } catch(e) {
208
+ const err = new Error('"offer.credential_issuer" is not valid.');
209
+ err.cause = e;
210
+ throw err;
211
+ }
212
+ if(!(Array.isArray(credentials) && credentials.length > 0 &&
213
+ credentials.every(c => c && typeof c === 'object'))) {
214
+ throw new Error('"offer.credentials" is not valid.');
215
+ }
216
+ const grant = grants[GRANT_TYPES.get('preAuthorizedCode')];
217
+ if(!grant) {
218
+ // FIXME: implement `authorization_code` grant type as well
219
+ throw new Error('Only "pre-authorized_code" grant type is implemented.');
220
+ }
221
+ const {
222
+ 'pre-authorized_code': preAuthorizedCode,
223
+ user_pin_required: userPinRequired
224
+ } = grant;
225
+ if(!preAuthorizedCode) {
226
+ throw new Error('"offer.grant" is missing "pre-authorized_code".');
227
+ }
228
+ if(userPinRequired) {
229
+ throw new Error('User pin is not implemented.');
230
+ }
231
+
232
+ try {
233
+ // discover issuer info
234
+ const issuerConfigUrl =
235
+ `${parsedIssuer.origin}/.well-known/oauth-authorization-server` +
236
+ parsedIssuer.pathname;
237
+ const issuerConfig = await discoverIssuer({issuerConfigUrl, agent});
238
+
239
+ /* First get access token from AS (Authorization Server), e.g.:
240
+
241
+ POST /token HTTP/1.1
242
+ Host: server.example.com
243
+ Content-Type: application/x-www-form-urlencoded
244
+ grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code
245
+ &pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA
246
+ &user_pin=493536
247
+
248
+ Note a bad response would look like:
249
+
250
+ /*
251
+ HTTP/1.1 400 Bad Request
252
+ Content-Type: application/json
253
+ Cache-Control: no-store
254
+ {
255
+ "error": "invalid_request"
256
+ }
257
+ */
258
+ const body = new URLSearchParams();
259
+ body.set('grant_type', GRANT_TYPES.get('preAuthorizedCode'));
260
+ body.set('pre-authorized_code', preAuthorizedCode);
261
+ const {token_endpoint} = issuerConfig;
262
+ const response = await httpClient.post(token_endpoint, {
263
+ agent, body, headers: HEADERS
264
+ });
265
+ const {data: result} = response;
266
+ if(!result) {
267
+ const error = new Error(
268
+ 'Could not get access token; response is not JSON.');
269
+ error.name = 'DataError';
270
+ throw error;
271
+ }
272
+
273
+ /* Validate response body (Note: Do not check or use `c_nonce*` here
274
+ because it conflates AS with DS (Delivery Server)), e.g.:
275
+
276
+ HTTP/1.1 200 OK
277
+ Content-Type: application/json
278
+ Cache-Control: no-store
279
+
280
+ {
281
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ",
282
+ "token_type": "bearer",
283
+ "expires_in": 86400
284
+ }
285
+ */
286
+ const {access_token: accessToken, token_type} = result;
287
+ if(!(accessToken && typeof accessToken === 'string')) {
288
+ const error = new Error(
289
+ 'Invalid access token response; "access_token" must be a string.');
290
+ error.name = 'DataError';
291
+ throw error;
292
+ }
293
+ if(token_type !== 'bearer') {
294
+ const error = new Error(
295
+ 'Invalid access token response; "token_type" must be a "bearer".');
296
+ error.name = 'DataError';
297
+ throw error;
298
+ }
299
+
300
+ // create client w/access token
301
+ return new OID4Client({accessToken, agent, issuerConfig, offer});
302
+ } catch(cause) {
303
+ const error = new Error('Could not create OID4 client.');
304
+ error.name = 'OperationError';
305
+ error.cause = cause;
306
+ throw error;
307
+ }
308
+ }
309
+ }
310
+
311
+ function _assertRequest(request) {
312
+ if(!(request && typeof request === 'object')) {
313
+ throw new TypeError('"request" must be an object.');
314
+ }
315
+ const {credential_definition, format} = request;
316
+ if(format !== undefined && format !== 'ldp_vc') {
317
+ throw new TypeError('Credential request "format" must be "ldp_vc".');
318
+ }
319
+ if(!(credential_definition && typeof credential_definition === 'object')) {
320
+ throw new TypeError(
321
+ 'Credential request "credential_definition" must be an object.');
322
+ }
323
+ const {'@context': context, type: type} = credential_definition;
324
+ if(!(Array.isArray(context) && context.length > 0)) {
325
+ throw new TypeError(
326
+ 'Credential definition "@context" must be an array of length >= 1.');
327
+ }
328
+ if(!(Array.isArray(type) && type.length > 0)) {
329
+ throw new TypeError(
330
+ 'Credential definition "type" must be an array of length >= 2.');
331
+ }
332
+ }
333
+
334
+ function _isMissingProofError(error) {
335
+ /* If DID authn is required, delivery server sends, e.g.:
336
+
337
+ HTTP/1.1 400 Bad Request
338
+ Content-Type: application/json
339
+ Cache-Control: no-store
340
+
341
+ {
342
+ "error": "invalid_or_missing_proof"
343
+ "error_description":
344
+ "Credential issuer requires proof element in Credential Request"
345
+ "c_nonce": "8YE9hCnyV2",
346
+ "c_nonce_expires_in": 86400
347
+ }
348
+ */
349
+ return error.status === 400 &&
350
+ error?.data?.error === 'invalid_or_missing_proof';
351
+ }
352
+
353
+ function _createCredentialRequestFromId({id, issuerConfig}) {
354
+ const {credentials_supported: supported = []} = issuerConfig;
355
+ const meta = supported.find(d => d.id === id);
356
+ if(!meta) {
357
+ throw new Error(`No supported credential "${id}" found.`);
358
+ }
359
+ const {format, credential_definition} = meta;
360
+ if(typeof format !== 'string') {
361
+ throw new Error(
362
+ `Invalid supported credential "${id}"; "format" not specified.`);
363
+ }
364
+ if(format !== 'ldp_vc') {
365
+ throw new Error(`Unsupported "format" "${format}".`);
366
+ }
367
+ if(!(Array.isArray(credential_definition?.['@context']) &&
368
+ Array.isArray(credential_definition?.types))) {
369
+ throw new Error(
370
+ `Invalid supported credential "${id}"; "credential_definition" not ` +
371
+ 'fully specified.');
372
+ }
373
+ return {format, credential_definition};
374
+ }
375
+
376
+ function _createCredentialRequestsFromOffer({issuerConfig, offer}) {
377
+ // build requests from `offer`
378
+ return offer.credentials.map(c => {
379
+ if(typeof c === 'string') {
380
+ // use issuer config metadata to dereference string
381
+ return _createCredentialRequestFromId({id: c, issuerConfig});
382
+ }
383
+ return c;
384
+ });
385
+ }
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /*!
2
+ * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ export * from './util.js';
5
+ export {OID4Client} from './OID4Client.js';
package/lib/util.js ADDED
@@ -0,0 +1,183 @@
1
+ /*!
2
+ * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as base64url from 'base64url-universal';
5
+ import {httpClient} from '@digitalbazaar/http-client';
6
+
7
+ const TEXT_ENCODER = new TextEncoder();
8
+ const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
9
+ const WELL_KNOWN_REGEX = /\/\.well-known\/([^\/]+)/;
10
+
11
+ export async function discoverIssuer({issuerConfigUrl, agent} = {}) {
12
+ try {
13
+ if(!(issuerConfigUrl && typeof issuerConfigUrl === 'string')) {
14
+ throw new TypeError('"issuerConfigUrl" must be a string.');
15
+ }
16
+
17
+ // allow these params to be passed / configured
18
+ const fetchOptions = {
19
+ // max size for issuer config related responses (in bytes, ~4 KiB)
20
+ size: 4096,
21
+ // timeout in ms for fetching an issuer config
22
+ timeout: 5000,
23
+ agent
24
+ };
25
+
26
+ const response = await httpClient.get(issuerConfigUrl, fetchOptions);
27
+ if(!response.data) {
28
+ const error = new Error('Issuer configuration format is not JSON.');
29
+ error.name = 'DataError';
30
+ throw error;
31
+ }
32
+
33
+ const {data: config} = response;
34
+ const {issuer, token_endpoint} = config;
35
+
36
+ // validate `issuer`
37
+ if(!(typeof issuer === 'string' && issuer.startsWith('https://'))) {
38
+ const error = new Error('"issuer" is not an HTTPS URL.');
39
+ error.name = 'DataError';
40
+ throw error;
41
+ }
42
+
43
+ /* Validate `issuer` value against `issuerConfigUrl` (per RFC 8414):
44
+
45
+ The `origin` and `path` element must be parsed from `issuer` and checked
46
+ against `issuerConfigUrl` like so:
47
+
48
+ For issuer `<origin>` (no path), `issuerConfigUrl` must match:
49
+ `<origin>/.well-known/<any-path-segment>`
50
+
51
+ For issuer `<origin><path>`, `issuerConfigUrl` must be:
52
+ `<origin>/.well-known/<any-path-segment><path>` */
53
+ const {pathname: wellKnownPath} = new URL(issuerConfigUrl);
54
+ const anyPathSegment = wellKnownPath.match(WELL_KNOWN_REGEX)[1];
55
+ const {origin, pathname} = new URL(issuer);
56
+ let expectedConfigUrl = `${origin}/.well-known/${anyPathSegment}`;
57
+ if(pathname !== '/') {
58
+ expectedConfigUrl += pathname;
59
+ }
60
+ if(issuerConfigUrl !== expectedConfigUrl) {
61
+ const error = new Error('"issuer" does not match configuration URL.');
62
+ error.name = 'DataError';
63
+ throw error;
64
+ }
65
+
66
+ // ensure `token_endpoint` is valid
67
+ if(!(token_endpoint && typeof token_endpoint === 'string')) {
68
+ const error = new TypeError('"token_endpoint" must be a string.');
69
+ error.name = 'DataError';
70
+ throw error;
71
+ }
72
+
73
+ return config;
74
+ } catch(cause) {
75
+ const error = new Error('Could not get OAuth2 issuer configuration.');
76
+ error.name = 'OperationError';
77
+ error.cause = cause;
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ export async function generateDIDProofJWT({
83
+ signer, nonce, iss, aud, exp, nbf
84
+ } = {}) {
85
+ /* Example:
86
+ {
87
+ "alg": "ES256",
88
+ "kid":"did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1"
89
+ }.
90
+ {
91
+ "iss": "s6BhdRkqt3",
92
+ "aud": "https://server.example.com",
93
+ "iat": 1659145924,
94
+ "nonce": "tZignsnFbp"
95
+ }
96
+ */
97
+
98
+ if(exp === undefined) {
99
+ // default to 5 minute expiration time
100
+ exp = Math.floor(Date.now() / 1000) + 60 * 5;
101
+ }
102
+ if(nbf === undefined) {
103
+ // default to now
104
+ nbf = Math.floor(Date.now() / 1000);
105
+ }
106
+
107
+ const {id: kid} = signer;
108
+ const alg = _curveToAlg(signer.algorithm);
109
+ const payload = {nonce, iss, aud, exp, nbf};
110
+ const protectedHeader = {alg, kid};
111
+
112
+ return signJWT({payload, protectedHeader, signer});
113
+ }
114
+
115
+ export function parseCredentialOfferUrl({url} = {}) {
116
+ if(!(url && typeof url === 'string')) {
117
+ throw new TypeError('"url" must be a string.');
118
+ }
119
+
120
+ /* Parse URL, e.g.:
121
+
122
+ 'openid-credential-offer://?' +
123
+ 'credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2F' +
124
+ 'localhost%3A18443%2Fexchangers%2Fz19t8xb568tNRD1zVm9R5diXR%2F' +
125
+ 'exchanges%2Fz1ADs3ur2s9tm6JUW6CnTiyn3%22%2C%22credentials' +
126
+ '%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22credential_definition' +
127
+ '%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
128
+ 'credentials%2Fv1%22%2C%22https%3A%2F%2Fwww.w3.org%2F2018%2F' +
129
+ 'credentials%2Fexamples%2Fv1%22%5D%2C%22type%22%3A%5B%22' +
130
+ 'VerifiableCredential%22%2C%22UniversityDegreeCredential' +
131
+ '%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams' +
132
+ '%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22' +
133
+ 'pre-authorized_code%22%3A%22z1AEvnk2cqeRM1Mfv75vzHSUo%22%7D%7D%7D';
134
+ */
135
+ const {protocol, searchParams} = new URL(url);
136
+ if(protocol !== 'openid-credential-offer:') {
137
+ throw new SyntaxError(
138
+ '"url" must express a URL with the ' +
139
+ '"openid-credential-offer" protocol.');
140
+ }
141
+ return JSON.parse(searchParams.get('credential_offer'));
142
+ }
143
+
144
+ export async function signJWT({payload, protectedHeader, signer} = {}) {
145
+ // encode payload and protected header
146
+ const b64Payload = base64url.encode(JSON.stringify(payload));
147
+ const b64ProtectedHeader = base64url.encode(JSON.stringify(protectedHeader));
148
+ payload = TEXT_ENCODER.encode(b64Payload);
149
+ protectedHeader = TEXT_ENCODER.encode(b64ProtectedHeader);
150
+
151
+ // concatenate
152
+ const data = new Uint8Array(
153
+ protectedHeader.length + ENCODED_PERIOD.length + payload.length);
154
+ data.set(protectedHeader);
155
+ data.set(ENCODED_PERIOD, protectedHeader.length);
156
+ data.set(payload, protectedHeader.length + ENCODED_PERIOD.length);
157
+
158
+ // sign
159
+ const signature = await signer.sign({data});
160
+
161
+ // create JWS
162
+ const jws = {
163
+ signature: base64url.encode(signature),
164
+ payload: b64Payload,
165
+ protected: b64ProtectedHeader
166
+ };
167
+
168
+ // create compact JWT
169
+ return `${jws.protected}.${jws.payload}.${jws.signature}`;
170
+ }
171
+
172
+ function _curveToAlg(crv) {
173
+ if(crv === 'Ed25519' || crv === 'Ed448') {
174
+ return 'EdDSA';
175
+ }
176
+ if(crv?.startsWith('P-')) {
177
+ return `ES${crv.slice(2)}`;
178
+ }
179
+ if(crv === 'secp256k1') {
180
+ return 'ES256K';
181
+ }
182
+ return crv;
183
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@digitalbazaar/oid4-client",
3
+ "version": "2.0.0",
4
+ "description": "An OID4 (VC + VP) client",
5
+ "homepage": "https://github.com/digitalbazaar/oid4-client",
6
+ "author": {
7
+ "name": "Digital Bazaar, Inc.",
8
+ "email": "support@digitalbazaar.com",
9
+ "url": "https://digitalbazaar.com/"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/digitalbazaar/oid4-client"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/digitalbazaar/oid4-client/issues",
17
+ "email": "support@digitalbazaar.com"
18
+ },
19
+ "license": "BSD-3-Clause",
20
+ "type": "module",
21
+ "exports": "./lib/index.js",
22
+ "files": [
23
+ "lib/**/*.js"
24
+ ],
25
+ "dependencies": {
26
+ "@digitalbazaar/http-client": "^3.2.0",
27
+ "base64url-universal": "^2.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "c8": "^7.11.3",
31
+ "chai": "^4.3.6",
32
+ "cross-env": "^7.0.3",
33
+ "eslint": "^8.41.0",
34
+ "eslint-config-digitalbazaar": "^5.0.1",
35
+ "eslint-plugin-jsdoc": "^45.0.0",
36
+ "eslint-plugin-unicorn": "^42.0.0",
37
+ "jsdoc": "^4.0.2",
38
+ "jsdoc-to-markdown": "^8.0.0",
39
+ "karma": "^6.3.20",
40
+ "karma-chai": "^0.1.0",
41
+ "karma-chrome-launcher": "^3.1.1",
42
+ "karma-mocha": "^2.0.1",
43
+ "karma-mocha-reporter": "^2.2.5",
44
+ "karma-sourcemap-loader": "^0.3.8",
45
+ "karma-webpack": "^5.0.0",
46
+ "mocha": "^10.0.0",
47
+ "mocha-lcov-reporter": "^1.3.0",
48
+ "webpack": "^5.73.0"
49
+ },
50
+ "c8": {
51
+ "reporter": [
52
+ "lcov",
53
+ "text-summary",
54
+ "text"
55
+ ]
56
+ },
57
+ "engines": {
58
+ "node": ">=16"
59
+ },
60
+ "keywords": [
61
+ "OID4",
62
+ "OID4VCI",
63
+ "OID4VC",
64
+ "OID4VP",
65
+ "OIDC4VCI"
66
+ ],
67
+ "scripts": {
68
+ "test": "npm run test-node",
69
+ "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 10000 -r tests/node.js tests/**/*.spec.js",
70
+ "test-karma": "karma start tests/karma.conf.cjs",
71
+ "coverage": "cross-env NODE_ENV=test c8 npm run test-node",
72
+ "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node",
73
+ "coverage-report": "c8 report",
74
+ "generate-readme": "jsdoc2md -t readme-template.hbs lib/*.js > README.md",
75
+ "lint": "eslint ."
76
+ }
77
+ }