@bedrock/vc-delivery 7.11.1 → 7.11.3

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.
@@ -2,89 +2,39 @@
2
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 '../storage/exchanges.js';
6
- import {emitExchangeUpdated, evaluateExchangeStep} from '../helpers.js';
7
- import {logger} from '../logger.js';
5
+ import {ExchangeProcessor} from '../ExchangeProcessor.js';
8
6
 
9
7
  const {util: {BedrockError}} = bedrock;
10
8
 
11
9
  export async function processInviteResponse({req}) {
12
10
  const {config: workflow} = req.serviceObject;
13
11
  const exchangeRecord = await req.getExchange();
14
- let {meta: {updated: lastUpdated}} = exchangeRecord;
15
- const {exchange} = exchangeRecord;
16
- let step;
17
12
 
18
- try {
19
- // exchange step required for `inviteRequest`
20
- const currentStep = exchange.step;
21
- if(!currentStep) {
22
- _throwUnsupportedProtocol();
23
- }
24
-
25
- step = await evaluateExchangeStep({workflow, exchange});
26
-
27
- // step must have `inviteRequest` to perform protocol
28
- if(!step.inviteRequest) {
29
- _throwUnsupportedProtocol();
30
- }
13
+ // `inviteResponse` validated via HTTP body JSON schema already
14
+ const {body: inviteResponse} = req;
31
15
 
32
- // exchange must still be pending
33
- if(exchange.state !== 'pending') {
34
- throw new BedrockError(
35
- 'This exchange is already in progress.', {
36
- name: 'NotAllowedError',
37
- details: {httpStatusCode: 403, public: true}
38
- });
39
- }
40
-
41
- // `inviteResponse` validated via HTTP body JSON schema already
42
- const {body: inviteResponse} = req;
43
-
44
- // store invite response in variables associated with current step
45
- if(!exchange.variables.results) {
46
- exchange.variables.results = {};
47
- }
48
- const stepResult = {
49
- inviteRequest: {inviteResponse}
50
- };
51
- const prevState = exchange.state;
52
- exchange.variables.results[currentStep] = stepResult;
53
- try {
54
- // mark exchange complete
55
- exchange.state = 'complete';
56
- exchange.sequence++;
57
- await exchanges.complete({workflowId: workflow.id, exchange});
58
- await emitExchangeUpdated({workflow, exchange, step});
59
- lastUpdated = Date.now();
60
- } catch(e) {
61
- // revert exchange changes as it couldn't be written
62
- exchange.sequence--;
63
- exchange.state = prevState;
64
- delete exchange.variables.results[currentStep];
65
- throw e;
16
+ // process exchange to completion
17
+ const exchangeProcessor = new ExchangeProcessor({
18
+ workflow, exchangeRecord,
19
+ prepareStep({exchange, step}) {
20
+ // step must have `inviteRequest` to perform protocol
21
+ if(!step.inviteRequest) {
22
+ _throwUnsupportedProtocol();
23
+ }
24
+ // store invite response in variables associated with current step
25
+ exchange.variables.results[exchange.step] = {
26
+ ...exchange.variables.results[exchange.step],
27
+ inviteRequest: {inviteResponse}
28
+ };
66
29
  }
30
+ });
31
+ await exchangeProcessor.process();
67
32
 
68
- const result = {};
69
- if(inviteResponse.referenceId !== undefined) {
70
- result.referenceId = inviteResponse.referenceId;
71
- }
72
- return result;
73
- } catch(e) {
74
- if(e.name === 'InvalidStateError') {
75
- throw e;
76
- }
77
- // write last error if exchange hasn't been frequently updated
78
- const {id: workflowId} = workflow;
79
- const copy = {...exchange};
80
- copy.sequence++;
81
- copy.lastError = e;
82
- await exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
83
- .catch(error => logger.error(
84
- 'Could not set last exchange error: ' + error.message, {error}));
85
- await emitExchangeUpdated({workflow, exchange, step});
86
- throw e;
33
+ const result = {};
34
+ if(inviteResponse.referenceId !== undefined) {
35
+ result.referenceId = inviteResponse.referenceId;
87
36
  }
37
+ return result;
88
38
  }
89
39
 
90
40
  export function getInviteRequestProtocols({workflow, exchange, step}) {
package/lib/issue.js CHANGED
@@ -15,13 +15,16 @@ const {util: {BedrockError}} = bedrock;
15
15
 
16
16
  export async function issue({
17
17
  workflow, exchange, step, format = 'application/vc',
18
+ issueRequestsParams,
19
+ verifiablePresentation,
20
+ // FIXME: remove `filter`
18
21
  // by default do not issue any VCs that are to be stored in the exchange;
19
22
  // (`result` is NOT set)
20
23
  filter = params => !params.result
21
24
  } = {}) {
22
25
  // eval all issue requests for current step in exchange
23
26
  const issueRequests = await _evalIssueRequests({
24
- workflow, exchange, step, filter
27
+ workflow, exchange, step, issueRequestsParams, filter
25
28
  });
26
29
 
27
30
  // return early if there is no explicit VP in step nor nothing to issue
@@ -42,8 +45,10 @@ export async function issue({
42
45
 
43
46
  // generate VP to return VCs; use any explicitly defined VP from the step
44
47
  // (which may include out-of-band issued VCs that are to be delivered)
45
- const verifiablePresentation =
46
- structuredClone(step?.verifiablePresentation) ?? createPresentation();
48
+ verifiablePresentation =
49
+ verifiablePresentation ??
50
+ structuredClone(step?.verifiablePresentation) ??
51
+ createPresentation();
47
52
 
48
53
  // add issued VCs to VP
49
54
  if(issuedVcs.length > 0) {
@@ -60,31 +65,16 @@ export async function issue({
60
65
  return {response: {verifiablePresentation}, format, exchangeChanged};
61
66
  }
62
67
 
63
- async function _evalIssueRequests({workflow, exchange, step, filter}) {
64
- // evaluate all issue requests in parallel
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
- }));
76
- }
77
-
78
- async function _getIssueRequestParams({workflow, exchange, step}) {
68
+ export function getIssueRequestsParams({workflow, exchange, step}) {
79
69
  // use any templates from workflow and variables from exchange to produce
80
70
  // credentials to be issued; issue via the configured issuer instance
81
71
  const {credentialTemplates = []} = workflow;
82
72
  if(!(credentialTemplates.length > 0)) {
83
- // no issue request params
73
+ // no issue requests params
84
74
  return [];
85
75
  }
86
76
 
87
- if(!step ||
77
+ if(!workflow.steps ||
88
78
  (!step.issueRequests && Object.keys(workflow.steps).length === 1)) {
89
79
  // backwards-compatibility: deprecated workflows with no step or a single
90
80
  // step do not explicitly define `issueRequests` but instead consider each
@@ -93,9 +83,14 @@ async function _getIssueRequestParams({workflow, exchange, step}) {
93
83
  return credentialTemplates.map(typedTemplate => ({typedTemplate}));
94
84
  }
95
85
 
96
- // resolve all issue request params in parallel
86
+ if(!step.issueRequests) {
87
+ // no issue requests params
88
+ return [];
89
+ }
90
+
91
+ // resolve all issue requests params
97
92
  const variables = getTemplateVariables({workflow, exchange});
98
- return Promise.all(step.issueRequests.map(async r => {
93
+ return step.issueRequests.map(r => {
99
94
  // find the typed template to use
100
95
  let typedTemplate;
101
96
  if(r.credentialTemplateIndex !== undefined) {
@@ -138,6 +133,23 @@ async function _getIssueRequestParams({workflow, exchange, step}) {
138
133
  params.result = r.result;
139
134
  }
140
135
  return params;
136
+ });
137
+ }
138
+
139
+ async function _evalIssueRequests({
140
+ workflow, exchange, step, issueRequestsParams, filter
141
+ }) {
142
+ // evaluate all issue requests in parallel
143
+ const results = issueRequestsParams ??
144
+ getIssueRequestsParams({workflow, exchange, step}).filter(filter);
145
+ return Promise.all(results.map(async params => {
146
+ const {typedTemplate, variables} = params;
147
+ return {
148
+ params,
149
+ body: await evaluateTemplate({
150
+ workflow, exchange, typedTemplate, variables
151
+ })
152
+ };
141
153
  }));
142
154
  }
143
155
 
@@ -12,7 +12,7 @@ import {randomUUID} from 'node:crypto';
12
12
 
13
13
  const {util: {BedrockError}} = bedrock;
14
14
 
15
- const OID4VP_JWT_TYP = 'application/oauth-authz-req+jwt';
15
+ const OID4VP_JWT_TYP = 'oauth-authz-req+jwt';
16
16
  const TEXT_ENCODER = new TextEncoder();
17
17
 
18
18
  export async function create({
@@ -2,16 +2,12 @@
2
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 '../storage/exchanges.js';
6
- import {
7
- deepEqual, emitExchangeUpdated,
8
- evaluateExchangeStep, getWorkflowIssuerInstances
9
- } from '../helpers.js';
5
+ import {deepEqual, getWorkflowIssuerInstances} from '../helpers.js';
10
6
  import {importJWK, SignJWT} from 'jose';
11
7
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
12
- import {getAuthorizationRequest} from './oid4vp.js';
13
- import {issue} from '../issue.js';
14
- import {logger} from '../logger.js';
8
+ import {issue as defaultIssue} from '../issue.js';
9
+ import {ExchangeProcessor} from '../ExchangeProcessor.js';
10
+ import {getStepAuthorizationRequest} from './oid4vp.js';
15
11
  import {timingSafeEqual} from 'node:crypto';
16
12
  import {verifyDidProofJwt} from '../verify.js';
17
13
 
@@ -428,58 +424,51 @@ function _normalizeCredentialDefinitionTypes({credentialRequests}) {
428
424
  async function _processExchange({
429
425
  req, res, workflow, exchangeRecord, isBatchRequest
430
426
  }) {
431
- let step;
432
- const {id: workflowId} = workflow;
433
- const {exchange, meta} = exchangeRecord;
434
- let {updated: lastUpdated} = meta;
435
- try {
436
- // validate body against expected credential requests
437
- const {openId: {expectedCredentialRequests}} = exchange;
438
- let credentialRequests;
439
- if(isBatchRequest) {
440
- ({credential_requests: credentialRequests} = req.body);
441
- } else {
442
- if(expectedCredentialRequests.length > 1) {
443
- // FIXME: it is no longer the case that the batch endpoint must be used
444
- // for multiple requests; determine if the request has changed
445
-
446
- // clients interacting with exchanges with more than one VC to be
447
- // delivered must use the "batch credential" endpoint
448
- // FIXME: improve error
449
- throw new Error('batch_credential_endpoint must be used');
427
+ // process exchange and capture values to return
428
+ let format;
429
+ let didProofRequired = false;
430
+ const exchangeProcessor = new ExchangeProcessor({
431
+ workflow, exchangeRecord,
432
+ async prepareStep({exchange, step}) {
433
+ // validate body against expected credential requests
434
+ const {openId: {expectedCredentialRequests}} = exchange;
435
+ let credentialRequests;
436
+ if(isBatchRequest) {
437
+ ({credential_requests: credentialRequests} = req.body);
438
+ } else {
439
+ if(expectedCredentialRequests.length > 1) {
440
+ // FIXME: batch endpoint has been removed in OID4VCI 1.0; if used,
441
+ // then it is for a legacy OID4VCI draft 13 request, so this should
442
+ // be reworked
443
+ throw new Error('batch_credential_endpoint must be used');
444
+ }
445
+ credentialRequests = [req.body];
450
446
  }
451
- credentialRequests = [req.body];
452
- }
453
447
 
454
- // before asserting, normalize credential requests to use `type` instead of
455
- // `types`; this is to allow for OID4VCI draft implementers that followed
456
- // the non-normative examples
457
- _normalizeCredentialDefinitionTypes({credentialRequests});
458
- const {format} = _assertCredentialRequests({
459
- workflow, credentialRequests, expectedCredentialRequests
460
- });
448
+ // before asserting, normalize credential requests to use `type` instead
449
+ // of `types`; this is to allow for OID4VCI draft implementers that
450
+ // followed the non-normative examples
451
+ _normalizeCredentialDefinitionTypes({credentialRequests});
452
+ ({format} = _assertCredentialRequests({
453
+ workflow, credentialRequests, expectedCredentialRequests
454
+ }));
461
455
 
462
- // process exchange step if present
463
- const currentStep = exchange.step;
464
- if(currentStep) {
465
- step = await evaluateExchangeStep({workflow, exchange});
466
456
  const {jwtDidProofRequest} = step;
467
457
 
468
458
  // check to see if step supports OID4VP during OID4VCI
469
459
  if(step.openId) {
470
460
  // if there is no `presentationSubmission`, request one
471
461
  const {results} = exchange.variables;
472
- if(!results?.[exchange.step]?.openId?.presentationSubmission) {
473
- // FIXME: optimize away double step-template processing that
474
- // currently occurs when calling `_getAuthorizationRequest`
462
+ if(!results[exchange.step]?.openId?.presentationSubmission) {
475
463
  // note: only the "default" `clientProfileId` is supported at this
476
464
  // time because there isn't presently a defined way to specify
477
465
  // alternatives
478
466
  const clientProfileId = step.openId.clientProfiles ?
479
467
  'default' : undefined;
480
- const {
481
- authorizationRequest
482
- } = await getAuthorizationRequest({req, clientProfileId});
468
+ // get authorization request
469
+ const {authorizationRequest} = await getStepAuthorizationRequest({
470
+ workflow, exchange, step, clientProfileId
471
+ });
483
472
  return _requestOID4VP({authorizationRequest, res});
484
473
  }
485
474
  // otherwise drop down below to complete exchange...
@@ -489,6 +478,7 @@ async function _processExchange({
489
478
  // `proof` must be in every credential request; if any request is
490
479
  // missing `proof` then request a DID proof
491
480
  if(credentialRequests.some(cr => !cr.proof?.jwt)) {
481
+ didProofRequired = true;
492
482
  return _requestDidProof({res, exchangeRecord});
493
483
  }
494
484
 
@@ -507,48 +497,36 @@ async function _processExchange({
507
497
  throw new Error('every DID must be the same');
508
498
  }
509
499
  // store did results in variables associated with current step
510
- if(!exchange.variables.results) {
511
- exchange.variables.results = {};
512
- }
513
- exchange.variables.results[currentStep] = {
500
+ exchange.variables.results[exchange.step] = {
501
+ ...exchange.variables.results[exchange.step],
514
502
  // common use case of DID Authentication; provide `did` for ease
515
503
  // of use in templates
516
504
  did
517
505
  };
518
506
  }
507
+ },
508
+ inputRequired({exchange, step}) {
509
+ // input is required if:
510
+ // 1. a `jwtDidProofRequest` is required and hasn't been provided
511
+ // 2. OID4VP is enabled and no OID4VP result has been stored yet
512
+ return didProofRequired || (step.openId && !exchange.variables
513
+ .results[exchange.step]?.openId?.authorizationRequest);
514
+ },
515
+ issue({
516
+ workflow, exchange, step, issueRequestsParams,
517
+ verifiablePresentation
518
+ }) {
519
+ return defaultIssue({
520
+ workflow, exchange, step, issueRequestsParams,
521
+ verifiablePresentation, format
522
+ });
519
523
  }
520
-
521
- // mark exchange complete
522
- exchange.state = 'complete';
523
- try {
524
- exchange.sequence++;
525
- await exchanges.complete({workflowId, exchange});
526
- await emitExchangeUpdated({workflow, exchange, step});
527
- lastUpdated = Date.now();
528
- } catch(e) {
529
- exchange.sequence--;
530
- throw e;
531
- }
532
-
533
- // FIXME: decide what the best recovery path is if delivery fails (but no
534
- // replay attack detected) after exchange has been marked complete
535
-
536
- // issue VCs
537
- return issue({workflow, exchange, step, format});
538
- } catch(e) {
539
- if(e.name === 'InvalidStateError') {
540
- throw e;
541
- }
542
- // write last error if exchange hasn't been frequently updated
543
- const copy = {...exchange};
544
- copy.sequence++;
545
- copy.lastError = e;
546
- exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
547
- .catch(error => logger.error(
548
- 'Could not set last exchange error: ' + error.message, {error}));
549
- await emitExchangeUpdated({workflow, exchange, step});
550
- throw e;
524
+ });
525
+ const response = await exchangeProcessor.process();
526
+ if(!response.verifiablePresentation) {
527
+ return null;
551
528
  }
529
+ return {response, format};
552
530
  }
553
531
 
554
532
  async function _requestDidProof({res, exchangeRecord}) {