@bedrock/vc-delivery 7.11.2 → 7.12.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
@@ -2,7 +2,6 @@
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 vcjwt from './vcjwt.js';
6
5
  import {decodeId, generateId} from 'bnid';
7
6
  import {compile} from '@bedrock/validation';
8
7
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
@@ -22,16 +21,6 @@ const ALLOWED_ERROR_KEYS = [
22
21
  'status'
23
22
  ];
24
23
 
25
- const JWT_FORMAT_ALIASES = new Set([
26
- 'application/jwt',
27
- 'application/vc+jwt',
28
- 'application/vp+jwt',
29
- 'jwt_vp',
30
- 'jwt_vp_json',
31
- 'jwt_vc_json-ld',
32
- 'jwt_vc_json'
33
- ]);
34
-
35
24
  export function buildPresentationFromResults({
36
25
  presentation, verifyResult
37
26
  }) {
@@ -128,8 +117,9 @@ export async function evaluateExchangeStep({
128
117
  }) {
129
118
  let step = workflow.steps[stepName];
130
119
  if(step.stepTemplate) {
131
- step = await evaluateTemplate(
132
- {workflow, exchange, typedTemplate: step.stepTemplate});
120
+ step = await evaluateTemplate({
121
+ workflow, exchange, typedTemplate: step.stepTemplate
122
+ });
133
123
  }
134
124
  await validateStep({step});
135
125
  return step;
@@ -284,7 +274,7 @@ export function createVerifyOptions({
284
274
  // update `challenge`
285
275
  if(options.challenge === undefined) {
286
276
  options.challenge = expectedChallenge ??
287
- verifiablePresentationRequest.challenge ??
277
+ verifiablePresentationRequest?.challenge ??
288
278
  presentation?.proof?.challenge;
289
279
  }
290
280
 
@@ -357,44 +347,6 @@ export function stripStacktrace(error) {
357
347
  return error;
358
348
  }
359
349
 
360
- export async function unenvelopeCredential({
361
- envelopedCredential, format
362
- } = {}) {
363
- const result = _getEnvelope({envelope: envelopedCredential, format});
364
-
365
- // only supported format is VC-JWT at this time
366
- const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
367
- return {credential, ...result};
368
- }
369
-
370
- export async function unenvelopePresentation({
371
- envelopedPresentation, format
372
- } = {}) {
373
- const result = _getEnvelope({envelope: envelopedPresentation, format});
374
-
375
- // only supported format is VC-JWT at this time
376
- const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});
377
-
378
- // unenvelope any VCs in the presentation
379
- let {verifiableCredential = []} = presentation;
380
- if(!Array.isArray(verifiableCredential)) {
381
- verifiableCredential = [verifiableCredential];
382
- }
383
- if(verifiableCredential.length > 0) {
384
- presentation.verifiableCredential = await Promise.all(
385
- verifiableCredential.map(async vc => {
386
- if(vc?.type !== 'EnvelopedVerifiableCredential') {
387
- return vc;
388
- }
389
- const {credential} = await unenvelopeCredential({
390
- envelopedCredential: vc
391
- });
392
- return credential;
393
- }));
394
- }
395
- return {presentation, ...result};
396
- }
397
-
398
350
  export async function validateStep({step} = {}) {
399
351
  // FIXME: use `ajv` and do JSON schema check
400
352
  if(Object.keys(step).length === 0) {
@@ -422,35 +374,19 @@ export async function validateStep({step} = {}) {
422
374
  }
423
375
  }
424
376
 
425
- function _getEnvelope({envelope, format}) {
426
- const isString = typeof envelope === 'string';
427
- if(isString) {
428
- // supported formats
429
- if(JWT_FORMAT_ALIASES.has(format)) {
430
- format = 'application/jwt';
431
- }
432
- } else {
433
- const {id} = envelope;
434
- if(id?.startsWith('data:application/jwt,')) {
435
- format = 'application/jwt';
436
- envelope = id.slice('data:application/jwt,'.length);
437
- }
438
- }
439
-
440
- if(format === 'application/jwt' && envelope !== undefined) {
441
- return {envelope, format};
377
+ export function validateVerifiablePresentation({schema, presentation}) {
378
+ const validate = compile({schema});
379
+ const {valid, error} = validate(presentation);
380
+ if(!valid) {
381
+ throw error;
442
382
  }
443
-
444
- throw new BedrockError(
445
- `Unsupported credential or presentation envelope format "${format}".`, {
446
- name: 'NotSupportedError',
447
- details: {httpStatusCode: 400, public: true}
448
- });
449
383
  }
450
384
 
451
- export function validateVerifiablePresentation({schema, presentation}) {
385
+ export function validateVerifiablePresentationRequest({
386
+ schema, presentationRequest
387
+ }) {
452
388
  const validate = compile({schema});
453
- const {valid, error} = validate(presentation);
389
+ const {valid, error} = validate(presentationRequest);
454
390
  if(!valid) {
455
391
  throw error;
456
392
  }
@@ -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,6 +12,9 @@ import {randomUUID} from 'node:crypto';
12
12
 
13
13
  const {util: {BedrockError}} = bedrock;
14
14
 
15
+ const ENCRYPTED_RESPONSE_MODES = new Set([
16
+ 'direct_post.jwt', 'dc_api.jwt', 'dc_api'
17
+ ]);
15
18
  const OID4VP_JWT_TYP = 'oauth-authz-req+jwt';
16
19
  const TEXT_ENCODER = new TextEncoder();
17
20
 
@@ -28,6 +31,7 @@ export async function create({
28
31
  // get params from step OID4VP client profile to apply to the AR
29
32
  const {
30
33
  client_id, client_id_scheme,
34
+ dcql_query,
31
35
  nonce,
32
36
  presentation_definition,
33
37
  response_mode, response_uri
@@ -37,6 +41,10 @@ export async function create({
37
41
  // client_id_scheme (draft versions of OID4VP use this param)
38
42
  authorizationRequest.client_id_scheme = client_id_scheme ?? 'redirect_uri';
39
43
 
44
+ // dcql_query
45
+ authorizationRequest.dcql_query = dcql_query ??
46
+ authorizationRequest.dcql_query;
47
+
40
48
  // presentation_definition
41
49
  authorizationRequest.presentation_definition = presentation_definition ??
42
50
  authorizationRequest.presentation_definition;
@@ -175,8 +183,8 @@ async function _createClientMetaData({
175
183
  client_metadata.require_signed_request_object = true;
176
184
  }
177
185
 
178
- // for response mode `direct_post.jwt`, offer encryption options
179
- if(authorizationRequest.response_mode === 'direct_post.jwt') {
186
+ // offer encryption options for encrypted response modes
187
+ if(ENCRYPTED_RESPONSE_MODES.has(authorizationRequest.response_mode)) {
180
188
  // generate ECDH-ES P-256 key
181
189
  const kp = await generateKeyPair('ECDH-ES', {
182
190
  crv: 'P-256', extractable: true
@@ -8,7 +8,6 @@ import {
8
8
  } from '../../schemas/bedrock-vc-workflow.js';
9
9
  import {compile} from '@bedrock/validation';
10
10
  import {oid4vp} from '@digitalbazaar/oid4-client';
11
- import {unenvelopePresentation} from '../helpers.js';
12
11
 
13
12
  const {util: {BedrockError}} = bedrock;
14
13
 
@@ -27,58 +26,46 @@ bedrock.events.on('bedrock.init', () => {
27
26
  });
28
27
  });
29
28
 
30
- export async function parse({req, exchange, clientProfileId} = {}) {
29
+ export async function parse({
30
+ req, exchange, clientProfileId, authorizationRequest
31
+ } = {}) {
31
32
  try {
32
33
  const {body} = req;
33
34
  const {
34
- responseMode, parsed, protectedHeader
35
+ responseMode, parsed, protectedHeader,
36
+ recipientPublicJwk, recipientPublicJwkThumbprint,
37
+ vpTokenMediaType
35
38
  } = await oid4vp.verifier.parseAuthorizationResponse({
36
39
  body,
37
40
  getDecryptParameters() {
38
41
  return _getDecryptParameters({exchange, clientProfileId});
39
- }
42
+ },
43
+ authorizationRequest
40
44
  });
41
45
 
42
- // validate parsed presentation submission
46
+ // validate parsed presentation submission if given
43
47
  const {presentationSubmission} = parsed;
44
- _validate(VALIDATORS.presentationSubmission, presentationSubmission);
48
+ if(presentationSubmission) {
49
+ _validate(VALIDATORS.presentationSubmission, presentationSubmission);
50
+ }
45
51
 
46
52
  // obtain `presentation` and optional `envelope` from parsed `vpToken`
47
53
  const {vpToken} = parsed;
48
54
  let presentation;
49
55
  let envelope;
50
56
 
51
- if(oid4vp.authzResponse.submitsFormat({
52
- presentationSubmission, format: 'mso_mdoc'
53
- })) {
54
- // `vp_token` is declared to be a base64url-encoded mDL device response
55
- presentation = {
56
- '@context': VC_CONTEXT_2,
57
- id: `data:application/mdl-vp-token,${vpToken}`,
58
- type: 'EnvelopedVerifiablePresentation'
59
- };
60
- } else if(typeof vpToken === 'string') {
61
- // FIXME: remove unenveloping here and delegate it to VC API verifier;
62
- // FIXME: check if envelope matches submission once verified
63
- const {
64
- envelope: raw, presentation: contents, format
65
- } = await unenvelopePresentation({
66
- envelopedPresentation: vpToken,
67
- // FIXME: check `presentationSubmission` for VP format
68
- format: 'application/jwt'
69
- });
70
- _validate(VALIDATORS.presentation, contents);
57
+ if(vpTokenMediaType !== 'application/vp') {
58
+ // `vp_token` contains some enveloped format
71
59
  presentation = {
72
60
  '@context': VC_CONTEXT_2,
73
- id: `data:${format},${raw}`,
61
+ id: `data:${vpTokenMediaType},${vpToken}`,
74
62
  type: 'EnvelopedVerifiablePresentation'
75
63
  };
76
- envelope = {raw, contents, format};
64
+ envelope = {mediaType: vpTokenMediaType};
77
65
  } else {
78
- // simplest case: `vpToken` is a VP; validate it
66
+ // simplest case: `vpToken` is a VP; validate it against basic schema
79
67
  presentation = vpToken;
80
68
  _validate(VALIDATORS.presentation, presentation);
81
- // FIXME: validate VP against presentation submission
82
69
  }
83
70
 
84
71
  return {
@@ -86,7 +73,9 @@ export async function parse({req, exchange, clientProfileId} = {}) {
86
73
  presentationSubmission,
87
74
  presentation,
88
75
  envelope,
89
- protectedHeader
76
+ protectedHeader,
77
+ recipientPublicJwk,
78
+ recipientPublicJwkThumbprint
90
79
  };
91
80
  } catch(cause) {
92
81
  throw new BedrockError(
package/lib/oid4/http.js CHANGED
@@ -405,7 +405,7 @@ function _normalizeCredentials({verifiablePresentation}) {
405
405
  // use raw format for each credential
406
406
  const {verifiableCredential} = verifiablePresentation;
407
407
  return verifiableCredential.map(vc => {
408
- // parse any enveloped VC into its non-VC format
408
+ // parse any JWT-enveloped VC into its non-VC format
409
409
  if(vc.type === 'EnvelopedVerifiableCredential' &&
410
410
  vc.id?.startsWith('data:application/jwt,')) {
411
411
  return vc.id.slice('data:application/jwt,'.length);