@bedrock/vc-delivery 6.4.1 → 6.6.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/exchanges.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as database from '@bedrock/mongodb';
@@ -35,6 +35,8 @@ const LAST_ERROR_UPDATE_CONSTRAINTS = {
35
35
  updateTimeLimit: 1000
36
36
  };
37
37
 
38
+ const MONGODB_ILLEGAL_KEY_CHAR_REGEX = /[%$.]/;
39
+
38
40
  bedrock.events.on('bedrock-mongodb.ready', async () => {
39
41
  await database.openCollections([COLLECTION_NAME]);
40
42
 
@@ -110,14 +112,14 @@ export async function insert({workflowId, exchange}) {
110
112
  // backwards compatibility: enable existing systems to find record
111
113
  localExchangerId: localWorkflowId,
112
114
  meta,
113
- exchange
115
+ exchange: _encodeVariables({exchange})
114
116
  };
115
117
 
116
118
  // insert the exchange and get the updated record
117
119
  try {
118
120
  const collection = database.collections[COLLECTION_NAME];
119
- const result = await collection.insertOne(record);
120
- return result.ops[0];
121
+ await collection.insertOne(record);
122
+ return record;
121
123
  } catch(e) {
122
124
  if(!database.isDuplicateError(e)) {
123
125
  throw e;
@@ -199,6 +201,8 @@ export async function get({
199
201
  });
200
202
  }
201
203
 
204
+ record.exchange = _decodeVariables({exchange: record.exchange});
205
+
202
206
  // backwards compatibility; initialize `sequence`
203
207
  if(record.exchange.sequence === undefined) {
204
208
  const query = {
@@ -237,6 +241,9 @@ export async function update({workflowId, exchange, explain = false} = {}) {
237
241
  assert.object(exchange, 'exchange');
238
242
  const {id} = exchange;
239
243
 
244
+ // encode variable content for storage in mongoDB
245
+ exchange = _encodeVariables({exchange});
246
+
240
247
  // build update
241
248
  const update = _buildUpdate({exchange, complete: false});
242
249
 
@@ -566,13 +573,13 @@ function _buildUpdate({exchange, complete}) {
566
573
  $set: {'exchange.state': exchange.state, 'meta.updated': now},
567
574
  $unset: {}
568
575
  };
569
- if(complete) {
570
- // exchange complete, only update results
576
+ if(complete && typeof exchange.variables !== 'string') {
577
+ // exchange complete and variables not encoded, so only update results
571
578
  if(exchange.variables?.results) {
572
579
  update.$set['exchange.variables.results'] = exchange.variables.results;
573
580
  }
574
581
  } else {
575
- // exchange not complete, update all variables
582
+ // exchange not complete or variables are encoded, so update all variables
576
583
  if(exchange.variables) {
577
584
  update.$set['exchange.variables'] = exchange.variables;
578
585
  }
@@ -600,6 +607,41 @@ function _buildUpdate({exchange, complete}) {
600
607
  return update;
601
608
  }
602
609
 
610
+ function _encodeVariables({exchange}) {
611
+ // if any JSON object any variable uses a character that is not legal in
612
+ // a JSON key in mongoDB then stringify all the variables
613
+ if(_hasIllegalMongoDBKeyChar(exchange.variables)) {
614
+ return {...exchange, variables: JSON.stringify(exchange.variables)};
615
+ }
616
+ return exchange;
617
+ }
618
+
619
+ function _decodeVariables({exchange}) {
620
+ if(typeof exchange.variables === 'string') {
621
+ return {...exchange, variables: JSON.parse(exchange.variables)};
622
+ }
623
+ return exchange;
624
+ }
625
+
626
+ function _hasIllegalMongoDBKeyChar(value) {
627
+ if(Array.isArray(value)) {
628
+ for(const e of value) {
629
+ if(_hasIllegalMongoDBKeyChar(e)) {
630
+ return true;
631
+ }
632
+ }
633
+ } else if(value && typeof value === 'object') {
634
+ const keys = Object.keys(value);
635
+ for(const key of keys) {
636
+ if(MONGODB_ILLEGAL_KEY_CHAR_REGEX.test(key) ||
637
+ _hasIllegalMongoDBKeyChar(value[key])) {
638
+ return true;
639
+ }
640
+ }
641
+ }
642
+ return false;
643
+ }
644
+
603
645
  /**
604
646
  * An object containing information on the query plan.
605
647
  *
package/lib/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as vcjwt from './vcjwt.js';
@@ -18,6 +18,16 @@ const ALLOWED_ERROR_KEYS = [
18
18
  'status'
19
19
  ];
20
20
 
21
+ const JWT_FORMAT_ALIASES = new Set([
22
+ 'application/jwt',
23
+ 'application/vc+jwt',
24
+ 'application/vp+jwt',
25
+ 'jwt_vp',
26
+ 'jwt_vp_json',
27
+ 'jwt_vc_json-ld',
28
+ 'jwt_vc_json'
29
+ ]);
30
+
21
31
  export async function evaluateTemplate({
22
32
  workflow, exchange, typedTemplate, variables
23
33
  } = {}) {
@@ -243,7 +253,7 @@ function _getEnvelope({envelope, format}) {
243
253
  const isString = typeof envelope === 'string';
244
254
  if(isString) {
245
255
  // supported formats
246
- if(format === 'application/jwt' || format === 'jwt_vc_json-ld') {
256
+ if(JWT_FORMAT_ALIASES.has(format)) {
247
257
  format = 'application/jwt';
248
258
  }
249
259
  } else {
@@ -289,9 +289,20 @@ function _createClientMetaData() {
289
289
  // return default supported `vp_formats`
290
290
  return {
291
291
  vp_formats: {
292
+ // support both aliases `jwt_vp` and `jwt_vp_json`
292
293
  jwt_vp: {
293
294
  alg: ['EdDSA', 'Ed25519', 'ES256', 'ES384']
294
295
  },
296
+ jwt_vp_json: {
297
+ alg: ['EdDSA', 'Ed25519', 'ES256', 'ES384']
298
+ },
299
+ di_vp: {
300
+ proof_type: [
301
+ 'ecdsa-rdfc-2019',
302
+ 'eddsa-rdfc-2022',
303
+ 'Ed25519Signature2020'
304
+ ]
305
+ },
295
306
  ldp_vp: {
296
307
  proof_type: [
297
308
  'ecdsa-rdfc-2019',
@@ -308,7 +319,7 @@ async function _parseAuthorizationResponse({req}) {
308
319
  const {vp_token, presentation_submission} = req.body;
309
320
 
310
321
  // JSON parse and validate `vp_token` and `presentation_submission`
311
- let presentation = _jsonParse(vp_token, 'vp_token');
322
+ let presentation = _jsonParse(vp_token, 'vp_token', true);
312
323
  const presentationSubmission = _jsonParse(
313
324
  presentation_submission, 'presentation_submission');
314
325
  _validate(VALIDATORS.presentationSubmission, presentationSubmission);
@@ -319,8 +330,8 @@ async function _parseAuthorizationResponse({req}) {
319
330
  envelope: raw, presentation: contents, format
320
331
  } = await unenvelopePresentation({
321
332
  envelopedPresentation: presentation,
322
- // FIXME: check presentationSubmission for VP format
323
- format: 'jwt_vc_json-ld'
333
+ // FIXME: check `presentationSubmission` for VP format
334
+ format: 'application/jwt'
324
335
  });
325
336
  _validate(VALIDATORS.presentation, contents);
326
337
  presentation = {
@@ -336,10 +347,15 @@ async function _parseAuthorizationResponse({req}) {
336
347
  return {presentation, envelope, presentationSubmission};
337
348
  }
338
349
 
339
- function _jsonParse(x, name) {
350
+ function _jsonParse(x, name, allowJWT = false) {
340
351
  try {
341
352
  return JSON.parse(x);
342
353
  } catch(cause) {
354
+ // presume the string is a non-JSON encoded JWT and let subsequent
355
+ // checking handle it (`ey` is base64url-encoded `{`)
356
+ if(allowJWT && x?.startsWith('ey')) {
357
+ return x;
358
+ }
343
359
  throw new BedrockError(`Could not parse "${name}".`, {
344
360
  name: 'DataError',
345
361
  details: {httpStatusCode: 400, public: true},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "6.4.1",
3
+ "version": "6.6.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -181,7 +181,7 @@ const expectedCredentialRequest = {
181
181
  credential_definition: credentialDefinition,
182
182
  format: {
183
183
  type: 'string',
184
- enum: ['ldp_vc', 'jwt_vc_json-ld']
184
+ enum: ['di_vc', 'ldp_vc', 'jwt_vc_json-ld', 'jwt_vc_json']
185
185
  }
186
186
  }
187
187
  };
@@ -509,7 +509,7 @@ const openIdCredentialRequest = {
509
509
  credential_definition: credentialDefinition,
510
510
  format: {
511
511
  type: 'string',
512
- enum: ['ldp_vc', 'jwt_vc_json-ld']
512
+ enum: ['di_vc', 'ldp_vc', 'jwt_vc_json-ld', 'jwt_vc_json']
513
513
  },
514
514
  did: {
515
515
  type: 'string'
@@ -631,7 +631,21 @@ export function openIdAuthorizationResponseBody() {
631
631
  presentation_submission: {
632
632
  type: 'string'
633
633
  },
634
- // is a JSON string in the x-www-form-urlencoded body
634
+ // is a JSON-encoded string or object in the x-www-form-urlencoded body
635
+ /* Note: This can also be a simple base64url string for
636
+ backwards/forwards compatibility. While submitting VPs directly as
637
+ JSON objects has never changed in the OID4* specs, submitting VPs that
638
+ are wrapped in some envelope that is expressed as a string (e.g., a JWT)
639
+ has changed back and forth throughout the draft history. Sometimes these
640
+ vp_tokens are required to be JSON-encoded strings other times non-JSON
641
+ strings, i.e., no "extra/JSON quotes" around the string value inside the
642
+ x-www-form-urlencoded field value delimiting quotes. For example,
643
+ both of these:
644
+
645
+ `...&vp_token="non-string JSON"`
646
+ `...&vp_token="\"JSON string\""`
647
+
648
+ are accepted for these reasons. */
635
649
  vp_token: {
636
650
  type: 'string'
637
651
  },