@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.
- package/lib/ExchangeProcessor.js +556 -0
- package/lib/helpers.js +14 -3
- package/lib/inviteRequest/inviteRequest.js +22 -72
- package/lib/issue.js +35 -23
- package/lib/oid4/authorizationRequest.js +1 -1
- package/lib/oid4/oid4vci.js +59 -81
- package/lib/oid4/oid4vp.js +146 -226
- package/lib/vcapi.js +15 -230
- package/lib/verify.js +10 -8
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
73
|
+
// no issue requests params
|
|
84
74
|
return [];
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
if(!
|
|
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
|
-
|
|
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
|
|
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 = '
|
|
15
|
+
const OID4VP_JWT_TYP = 'oauth-authz-req+jwt';
|
|
16
16
|
const TEXT_ENCODER = new TextEncoder();
|
|
17
17
|
|
|
18
18
|
export async function create({
|
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -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
|
|
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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
({
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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}) {
|