@bedrock/vc-delivery 7.10.1 → 7.11.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/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as vcjwt from './vcjwt.js';
@@ -9,6 +9,7 @@ import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
9
9
  import {httpClient} from '@digitalbazaar/http-client';
10
10
  import {httpsAgent} from '@bedrock/https-agent';
11
11
  import jsonata from 'jsonata';
12
+ import jsonpointer from 'json-pointer';
12
13
  import {logger} from './logger.js';
13
14
  import {serializeError} from 'serialize-error';
14
15
  import {serviceAgents} from '@bedrock/service-agent';
@@ -141,12 +142,14 @@ export function getTemplateVariables({workflow, exchange} = {}) {
141
142
  workflow: {
142
143
  id: workflow.id
143
144
  },
145
+ exchange: {
146
+ id: exchange.id
147
+ },
148
+ localExchangeId: exchange.id,
149
+ exchangeId: `${workflow.id}/exchanges/${exchange.id}`,
144
150
  // backwards compatibility
145
151
  exchanger: {
146
152
  id: workflow.id
147
- },
148
- exchange: {
149
- id: exchange.id
150
153
  }
151
154
  };
152
155
  return variables;
@@ -293,6 +296,46 @@ export function createVerifyOptions({
293
296
  return options;
294
297
  }
295
298
 
299
+ export function resolvePointer(obj, pointer) {
300
+ if(pointer === '/') {
301
+ return obj;
302
+ }
303
+ try {
304
+ return jsonpointer.get(obj, pointer);
305
+ } catch(e) {
306
+ return undefined;
307
+ }
308
+ }
309
+
310
+ export function resolveVariableName({variables, name} = {}) {
311
+ if(!name.startsWith('/')) {
312
+ return variables[name];
313
+ }
314
+ if(name === '/') {
315
+ return variables;
316
+ }
317
+ try {
318
+ return jsonpointer.get(variables, name);
319
+ } catch(e) {
320
+ return undefined;
321
+ }
322
+ }
323
+
324
+ export function setVariable({variables, name, value} = {}) {
325
+ if(!name.startsWith('/')) {
326
+ variables[name] = value;
327
+ return;
328
+ }
329
+ if(name === '/') {
330
+ throw new BedrockError(
331
+ `Invalid variable name "${name}".`, {
332
+ name: 'NotSupportedError',
333
+ details: {httpStatusCode: 500, public: true}
334
+ });
335
+ }
336
+ jsonpointer.set(variables, name, value);
337
+ }
338
+
296
339
  export function stripStacktrace(error) {
297
340
  // serialize error and allow-list specific properties
298
341
  const serialized = serializeError(error);
package/lib/http.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Copyright (c) 2018-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2018-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from './exchanges.js';
5
+ import * as exchanges from './storage/exchanges.js';
6
6
  import * as inviteRequest from './inviteRequest/http.js';
7
7
  import * as oid4 from './oid4/http.js';
8
8
  import {createExchange, getProtocols, processExchange} from './vcapi.js';
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2025-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from '../exchanges.js';
5
+ import * as exchanges from '../storage/exchanges.js';
6
6
  import {emitExchangeUpdated, evaluateExchangeStep} from '../helpers.js';
7
7
  import {logger} from '../logger.js';
8
8
 
package/lib/issue.js CHANGED
@@ -6,25 +6,39 @@ import {
6
6
  evaluateTemplate,
7
7
  getTemplateVariables,
8
8
  getWorkflowIssuerInstances,
9
- getZcapClient
9
+ getZcapClient,
10
+ setVariable
10
11
  } from './helpers.js';
11
12
  import {createPresentation} from '@digitalbazaar/vc';
12
13
 
13
14
  const {util: {BedrockError}} = bedrock;
14
15
 
15
16
  export async function issue({
16
- workflow, exchange, step, format = 'application/vc'
17
+ workflow, exchange, step, format = 'application/vc',
18
+ // by default do not issue any VCs that are to be stored in the exchange;
19
+ // (`result` is NOT set)
20
+ filter = params => !params.result
17
21
  } = {}) {
18
22
  // eval all issue requests for current step in exchange
19
- const issueRequests = await _evalIssueRequests({workflow, exchange, step});
23
+ const issueRequests = await _evalIssueRequests({
24
+ workflow, exchange, step, filter
25
+ });
20
26
 
21
27
  // return early if there is no explicit VP in step nor nothing to issue
22
28
  if(!step?.verifiablePresentation && issueRequests.length === 0) {
23
- return {response: {}};
29
+ return {response: {}, exchangeChanged: false};
24
30
  }
25
31
 
26
32
  // run all issue requests
27
- const issuedVcs = await _issue({workflow, issueRequests, format});
33
+ const {
34
+ credentials: issuedVcs,
35
+ exchangeChanged
36
+ } = await _issue({workflow, exchange, issueRequests, format});
37
+
38
+ if(issuedVcs.length === 0 && !step?.verifiablePresentation) {
39
+ // no issued VCs/no VP to return in response
40
+ return {response: {}, exchangeChanged};
41
+ }
28
42
 
29
43
  // generate VP to return VCs; use any explicitly defined VP from the step
30
44
  // (which may include out-of-band issued VCs that are to be delivered)
@@ -43,30 +57,39 @@ export async function issue({
43
57
  }
44
58
  verifiablePresentation.verifiableCredential = vcs;
45
59
  }
46
- return {response: {verifiablePresentation}, format};
60
+ return {response: {verifiablePresentation}, format, exchangeChanged};
47
61
  }
48
62
 
49
- async function _evalIssueRequests({workflow, exchange, step}) {
63
+ async function _evalIssueRequests({workflow, exchange, step, filter}) {
50
64
  // evaluate all issue requests in parallel
51
- const requests = await _getIssueRequests({workflow, exchange, step});
52
- return Promise.all(requests.map(({typedTemplate, variables}) =>
53
- evaluateTemplate({workflow, exchange, typedTemplate, variables})));
65
+ let results = await _getIssueRequestParams({workflow, exchange, step});
66
+ results = results.filter(filter);
67
+ return Promise.all(results.map(async params => {
68
+ const {typedTemplate, variables} = params;
69
+ return {
70
+ params,
71
+ body: await evaluateTemplate({
72
+ workflow, exchange, typedTemplate, variables
73
+ })
74
+ };
75
+ }));
54
76
  }
55
77
 
56
- async function _getIssueRequests({workflow, exchange, step}) {
78
+ async function _getIssueRequestParams({workflow, exchange, step}) {
57
79
  // use any templates from workflow and variables from exchange to produce
58
80
  // credentials to be issued; issue via the configured issuer instance
59
81
  const {credentialTemplates = []} = workflow;
60
82
  if(!(credentialTemplates.length > 0)) {
61
- // no issue requests
83
+ // no issue request params
62
84
  return [];
63
85
  }
64
86
 
65
87
  if(!step ||
66
88
  (!step.issueRequests && Object.keys(workflow.steps).length === 1)) {
67
89
  // backwards-compatibility: deprecated workflows with no step or a single
68
- // step do not explicitly define `issueRequests` but instead use all
69
- // templates for issue requests
90
+ // step do not explicitly define `issueRequests` but instead consider each
91
+ // credential template as the `typedTemplate` parameter (and the only
92
+ // parameter) for an issue request
70
93
  return credentialTemplates.map(typedTemplate => ({typedTemplate}));
71
94
  }
72
95
 
@@ -103,7 +126,7 @@ async function _getIssueRequests({workflow, exchange, step}) {
103
126
  });
104
127
  }
105
128
  }
106
- return {
129
+ const params = {
107
130
  typedTemplate,
108
131
  variables: {
109
132
  // always include globals but allow local override
@@ -111,6 +134,10 @@ async function _getIssueRequests({workflow, exchange, step}) {
111
134
  ...vars
112
135
  }
113
136
  };
137
+ if(r.result) {
138
+ params.result = r.result;
139
+ }
140
+ return params;
114
141
  }));
115
142
  }
116
143
 
@@ -121,7 +148,7 @@ function _getIssueZcap({workflow, zcaps, format}) {
121
148
  return zcaps[issueRefId];
122
149
  }
123
150
 
124
- async function _issue({workflow, issueRequests, format} = {}) {
151
+ async function _issue({workflow, exchange, issueRequests, format} = {}) {
125
152
  // create zcap client for issuing VCs
126
153
  const {zcapClient, zcaps} = await getZcapClient({workflow});
127
154
 
@@ -137,19 +164,38 @@ async function _issue({workflow, issueRequests, format} = {}) {
137
164
  }
138
165
 
139
166
  // issue VCs in parallel
140
- return Promise.all(issueRequests.map(async issueRequest => {
141
- /* Note: Issue request formats can be any one of these:
167
+ let exchangeChanged = false;
168
+ const results = await Promise.all(issueRequests.map(async issueRequest => {
169
+ const {params, body} = issueRequest;
170
+
171
+ /* Note: Issue request body can be any one of these:
142
172
 
143
173
  1. `{credential, options?}`
144
174
  2. `credential`
145
175
 
146
- Normalize issue requests that use the full VC API issue request and those
147
- that return only the `credential` param directly. */
148
- const json = issueRequest.credential ?
149
- issueRequest : {credential: issueRequest};
176
+ Normalize all issue request bodies to full VC API issue request bodies. */
177
+ const json = !body?.credential ? {credential: body} : body;
150
178
  const {
151
179
  data: {verifiableCredential}
152
180
  } = await zcapClient.write({url, capability, json});
181
+
182
+ // if the issue request specifies a location for storing the credential,
183
+ // put it there and return `undefined`; otherwise, return the credential
184
+ if(params.result) {
185
+ exchangeChanged = true;
186
+ setVariable({
187
+ variables: exchange.variables,
188
+ name: params.result,
189
+ value: verifiableCredential
190
+ });
191
+ return;
192
+ }
193
+
153
194
  return verifiableCredential;
154
195
  }));
196
+
197
+ // filter out any undefined results, which are for results that were written
198
+ // to exchange variables and are not to be automatically returned in a
199
+ // presentation
200
+ return {credentials: results.filter(vc => vc), exchangeChanged};
155
201
  }
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from '../exchanges.js';
5
+ import * as exchanges from '../storage/exchanges.js';
6
6
  import {
7
7
  deepEqual, emitExchangeUpdated,
8
8
  evaluateExchangeStep, getWorkflowIssuerInstances
@@ -1,13 +1,15 @@
1
1
  /*!
2
- * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from '../exchanges.js';
5
+ import * as exchanges from '../storage/exchanges.js';
6
6
  import {
7
7
  buildPresentationFromResults,
8
8
  buildVerifyPresentationResults,
9
9
  emitExchangeUpdated,
10
- evaluateExchangeStep
10
+ evaluateExchangeStep,
11
+ resolveVariableName,
12
+ setVariable
11
13
  } from '../helpers.js';
12
14
  import {getClientBaseUrl, getClientProfile} from './clientProfiles.js';
13
15
  import {compile} from '@bedrock/validation';
@@ -362,8 +364,19 @@ async function _getStepAuthorizationRequest({
362
364
  _throwUnsupportedProtocol();
363
365
  }
364
366
 
365
- // create or get cached authorization request
366
- authorizationRequest = exchange.variables?.[authzReqVarName];
367
+ // create or get cached authorization request...
368
+
369
+ if(authzReqVarName === '/') {
370
+ // overwriting all variables is not permitted
371
+ throw new BedrockError(
372
+ `Invalid authorization request variable name "${authzReqVarName}".`, {
373
+ name: 'NotSupportedError',
374
+ details: {httpStatusCode: 500, public: true}
375
+ });
376
+ }
377
+ authorizationRequest = resolveVariableName({
378
+ variables: exchange.variables, name: authzReqVarName
379
+ });
367
380
  if(authorizationRequest) {
368
381
  return {authorizationRequest, exchangeChanged: false};
369
382
  }
@@ -393,7 +406,11 @@ async function _getStepAuthorizationRequest({
393
406
  if(!exchange.variables) {
394
407
  exchange.variables = {};
395
408
  }
396
- exchange.variables[authzReqVarName] = authorizationRequest;
409
+ setVariable({
410
+ variables: exchange.variables,
411
+ name: authzReqVarName,
412
+ value: authorizationRequest
413
+ });
397
414
 
398
415
  return {authorizationRequest, exchangeChanged: true};
399
416
  }
@@ -1,11 +1,11 @@
1
1
  /*!
2
- * Copyright (c) 2022-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as database from '@bedrock/mongodb';
6
- import {parseLocalId, stripStacktrace} from './helpers.js';
6
+ import {parseLocalId, stripStacktrace} from '../helpers.js';
7
7
  import assert from 'assert-plus';
8
- import {logger} from './logger.js';
8
+ import {logger} from '../logger.js';
9
9
  import {serializeError} from 'serialize-error';
10
10
 
11
11
  const {util: {BedrockError}} = bedrock;
@@ -18,11 +18,9 @@ If an exchange is marked as complete, any attempt to mark it complete again
18
18
  will result in an action, as specified in the exchange record, being taken
19
19
  such as auto-revocation or notification.
20
20
 
21
- Each pending exchange may include optionally encrypted VCs for pickup and / or
22
- VC templates and required variables (which may come from other VCs) that must
23
- be provided to populate those templates. If any templates are provided, then
24
- a capability to issue the VC must also be provided. If any VCs are to be
25
- provided during the exchange a capability to verify them must be provided. */
21
+ Each pending exchange is an instance of a workflow. A workflow may have
22
+ one or more steps, each that might issue, verify, or deliver VCs. Capabilities
23
+ must be provided to issue or verify VCs. */
26
24
 
27
25
  const COLLECTION_NAME = 'vc-exchange';
28
26
 
@@ -244,7 +242,7 @@ export async function update({workflowId, exchange, explain = false} = {}) {
244
242
  exchange = _encodeVariables({exchange});
245
243
 
246
244
  // build update
247
- const update = _buildUpdate({exchange, complete: false});
245
+ const update = _buildUpdate({exchange});
248
246
 
249
247
  const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
250
248
 
@@ -326,7 +324,7 @@ export async function complete({workflowId, exchange, explain = false} = {}) {
326
324
  const {id} = exchange;
327
325
 
328
326
  // build update
329
- const update = _buildUpdate({exchange, complete: true});
327
+ const update = _buildUpdate({exchange});
330
328
 
331
329
  const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
332
330
 
@@ -564,7 +562,7 @@ async function _markExchangeInvalid({record}) {
564
562
  }
565
563
  }
566
564
 
567
- function _buildUpdate({exchange, complete}) {
565
+ function _buildUpdate({exchange}) {
568
566
  // build update
569
567
  const now = Date.now();
570
568
  const update = {
@@ -572,21 +570,11 @@ function _buildUpdate({exchange, complete}) {
572
570
  $set: {
573
571
  'exchange.state': exchange.state,
574
572
  'exchange.secrets': exchange.secrets,
573
+ 'exchange.variables': exchange.variables,
575
574
  'meta.updated': now
576
575
  },
577
576
  $unset: {}
578
577
  };
579
- if(complete && typeof exchange.variables !== 'string') {
580
- // exchange complete and variables not encoded, so only update results
581
- if(exchange.variables?.results) {
582
- update.$set['exchange.variables.results'] = exchange.variables.results;
583
- }
584
- } else {
585
- // exchange not complete or variables are encoded, so update all variables
586
- if(exchange.variables) {
587
- update.$set['exchange.variables'] = exchange.variables;
588
- }
589
- }
590
578
  if(exchange.step !== undefined) {
591
579
  update.$set['exchange.step'] = exchange.step;
592
580
  }
package/lib/vcapi.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Copyright (c) 2018-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2018-2026 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
- import * as exchanges from './exchanges.js';
5
+ import * as exchanges from './storage/exchanges.js';
6
6
  import {createChallenge as _createChallenge, verify} from './verify.js';
7
7
  import {
8
8
  buildPresentationFromResults,
@@ -297,6 +297,9 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
297
297
  currentStep = step.nextStep;
298
298
  }
299
299
 
300
+ // issue any VCs that are to be stored in the exchange (`result` is set)
301
+ await issue({workflow, exchange, step, filter: params => params.result});
302
+
300
303
  // mark exchange complete
301
304
  exchange.state = 'complete';
302
305
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "7.10.1",
3
+ "version": "7.11.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -40,7 +40,7 @@
40
40
  "@digitalbazaar/ed25519-signature-2020": "^5.4.0",
41
41
  "@digitalbazaar/ezcap": "^4.1.0",
42
42
  "@digitalbazaar/http-client": "^4.2.0",
43
- "@digitalbazaar/oid4-client": "^5.4.1",
43
+ "@digitalbazaar/oid4-client": "^5.6.3",
44
44
  "@digitalbazaar/vc": "^7.2.0",
45
45
  "@digitalbazaar/webkms-client": "^14.2.0",
46
46
  "assert-plus": "^1.0.0",
@@ -48,6 +48,7 @@
48
48
  "body-parser": "^1.20.3",
49
49
  "cors": "^2.8.5",
50
50
  "jose": "^6.1.0",
51
+ "json-pointer": "^0.6.2",
51
52
  "jsonata": "^2.0.6",
52
53
  "serialize-error": "^12.0.0"
53
54
  },
@@ -396,6 +396,11 @@ const issueRequestParameters = {
396
396
  credentialTemplateIndex: {
397
397
  type: 'number'
398
398
  },
399
+ // optional specify where to store the issued VCs instead of automatically
400
+ // including it in a VP to be returned to the client
401
+ result: {
402
+ type: 'string'
403
+ },
399
404
  // optionally specify different variables
400
405
  variables: {
401
406
  oneOf: [{type: 'string'}, {type: 'object'}]
@@ -533,6 +538,7 @@ function step() {
533
538
  'nextStep',
534
539
  'openId',
535
540
  'presentationSchema',
541
+ 'redirectUrl',
536
542
  'verifiablePresentation',
537
543
  'verifiablePresentationRequest'
538
544
  ]
@@ -628,6 +634,9 @@ function step() {
628
634
  }
629
635
  }
630
636
  },
637
+ redirectUrl: {
638
+ type: 'string'
639
+ },
631
640
  stepTemplate: typedTemplate,
632
641
  // the base verifiable presentation to use in this step; any VCs that
633
642
  // are issued in this step (see: `issueRequests`) will be added to this