@bedrock/vc-delivery 7.1.1 → 7.3.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
@@ -5,8 +5,10 @@ import * as bedrock from '@bedrock/core';
5
5
  import * as vcjwt from './vcjwt.js';
6
6
  import {decodeId, generateId} from 'bnid';
7
7
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
8
+ import {httpClient} from '@digitalbazaar/http-client';
8
9
  import {httpsAgent} from '@bedrock/https-agent';
9
10
  import jsonata from 'jsonata';
11
+ import {logger} from './logger.js';
10
12
  import {serializeError} from 'serialize-error';
11
13
  import {serviceAgents} from '@bedrock/service-agent';
12
14
  import {ZcapClient} from '@digitalbazaar/ezcap';
@@ -53,6 +55,27 @@ export function buildPresentationFromResults({
53
55
  return vp;
54
56
  }
55
57
 
58
+ export function emitExchangeUpdated({workflow, exchange, step}) {
59
+ if(!step?.callback?.url) {
60
+ // no-op when there is no callback to notify
61
+ return;
62
+ }
63
+
64
+ const {url} = step.callback;
65
+ const exchangeId = `${workflow.id}/exchanges/${exchange.id}`;
66
+ httpClient.post(url, {
67
+ agent: httpsAgent,
68
+ json: {
69
+ event: {
70
+ data: {exchangeId}
71
+ }
72
+ }
73
+ }).catch(
74
+ error => logger.error(
75
+ 'Could not send "exchangeUpdated" push notification: ' +
76
+ error.message, {error}));
77
+ }
78
+
56
79
  export async function evaluateTemplate({
57
80
  workflow, exchange, typedTemplate, variables
58
81
  } = {}) {
@@ -188,6 +211,36 @@ export function deepEqual(obj1, obj2) {
188
211
  return true;
189
212
  }
190
213
 
214
+ export function createVerifyOptions({
215
+ verifyPresentationOptions,
216
+ expectedChallenge,
217
+ verifiablePresentationRequest,
218
+ presentation,
219
+ domain,
220
+ checks
221
+ }) {
222
+ // start with `verifyPresentationOptions`, then overwrite as needed
223
+ const options = {...verifyPresentationOptions};
224
+
225
+ // update `checks` with anything additional from `verifyPresentationOptions`
226
+ const checkSet = new Set(checks);
227
+ if(verifyPresentationOptions.checks) {
228
+ Object.entries(verifyPresentationOptions.checks)
229
+ .forEach(([check, enabled]) => enabled && checkSet.add(check));
230
+ }
231
+ options.checks = [...checkSet];
232
+
233
+ // update `challenge`
234
+ options.challenge = expectedChallenge ??
235
+ verifiablePresentationRequest.challenge ??
236
+ presentation?.proof?.challenge;
237
+
238
+ // update `domain`
239
+ options.domain = domain;
240
+
241
+ return options;
242
+ }
243
+
191
244
  export function stripStacktrace(error) {
192
245
  // serialize error and allow-list specific properties
193
246
  const serialized = serializeError(error);
@@ -1,10 +1,11 @@
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 exchanges from '../exchanges.js';
6
6
  import {
7
- deepEqual, evaluateTemplate, getWorkflowIssuerInstances, validateStep
7
+ deepEqual, emitExchangeUpdated,
8
+ evaluateTemplate, getWorkflowIssuerInstances, validateStep
8
9
  } from '../helpers.js';
9
10
  import {importJWK, SignJWT} from 'jose';
10
11
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
@@ -397,6 +398,7 @@ function _normalizeCredentialDefinitionTypes({credentialRequests}) {
397
398
  async function _processExchange({
398
399
  req, res, workflow, exchangeRecord, isBatchRequest
399
400
  }) {
401
+ let step;
400
402
  const {id: workflowId} = workflow;
401
403
  const {exchange, meta} = exchangeRecord;
402
404
  let {updated: lastUpdated} = meta;
@@ -428,7 +430,6 @@ async function _processExchange({
428
430
  });
429
431
 
430
432
  // process exchange step if present
431
- let step;
432
433
  const currentStep = exchange.step;
433
434
  if(currentStep) {
434
435
  step = workflow.steps[exchange.step];
@@ -494,6 +495,7 @@ async function _processExchange({
494
495
  try {
495
496
  exchange.sequence++;
496
497
  await exchanges.complete({workflowId, exchange});
498
+ emitExchangeUpdated({workflow, exchange, step});
497
499
  lastUpdated = Date.now();
498
500
  } catch(e) {
499
501
  exchange.sequence--;
@@ -516,6 +518,7 @@ async function _processExchange({
516
518
  exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
517
519
  .catch(error => logger.error(
518
520
  'Could not set last exchange error: ' + error.message, {error}));
521
+ emitExchangeUpdated({workflow, exchange, step});
519
522
  throw e;
520
523
  }
521
524
  }
@@ -4,7 +4,8 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from '../exchanges.js';
6
6
  import {
7
- buildPresentationFromResults, evaluateTemplate, unenvelopePresentation,
7
+ buildPresentationFromResults, emitExchangeUpdated,
8
+ evaluateTemplate, unenvelopePresentation,
8
9
  validateStep
9
10
  } from '../helpers.js';
10
11
  import {
@@ -141,6 +142,7 @@ export async function getAuthorizationRequest({req}) {
141
142
  try {
142
143
  exchange.sequence++;
143
144
  await exchanges.update({workflowId: workflow.id, exchange});
145
+ emitExchangeUpdated({workflow, exchange, step});
144
146
  } catch(e) {
145
147
  exchange.sequence--;
146
148
  if(e.name !== 'InvalidStateError') {
@@ -168,11 +170,12 @@ export async function processAuthorizationResponse({req}) {
168
170
  const exchangeRecord = await req.getExchange();
169
171
  let {exchange} = exchangeRecord;
170
172
  let {meta: {updated: lastUpdated}} = exchangeRecord;
173
+ let step;
171
174
  try {
172
175
  // get authorization request and updated exchange associated with exchange
173
176
  const arRequest = await getAuthorizationRequest({req});
174
- const {authorizationRequest, step} = arRequest;
175
- ({exchange} = arRequest);
177
+ const {authorizationRequest} = arRequest;
178
+ ({exchange, step} = arRequest);
176
179
 
177
180
  // FIXME: check the VP against the presentation submission if requested
178
181
  // FIXME: check the VP against "trustedIssuer" in VPR, if provided
@@ -193,9 +196,15 @@ export async function processAuthorizationResponse({req}) {
193
196
  // verify the received VP
194
197
  const {verifiablePresentationRequest} = await oid4vp.toVpr(
195
198
  {authorizationRequest});
196
- const {allowUnprotectedPresentation = false} = step;
199
+ const {
200
+ allowUnprotectedPresentation = false,
201
+ verifyPresentationOptions = {},
202
+ verifyPresentationResultSchema
203
+ } = step;
197
204
  const verifyResult = await verify({
198
205
  workflow,
206
+ verifyPresentationOptions,
207
+ verifyPresentationResultSchema,
199
208
  verifiablePresentationRequest,
200
209
  presentation,
201
210
  allowUnprotectedPresentation,
@@ -244,6 +253,7 @@ export async function processAuthorizationResponse({req}) {
244
253
  exchange.state = 'complete';
245
254
  await exchanges.complete({workflowId: workflow.id, exchange});
246
255
  }
256
+ emitExchangeUpdated({workflow, exchange, step});
247
257
  lastUpdated = Date.now();
248
258
  } catch(e) {
249
259
  exchange.sequence--;
@@ -271,6 +281,7 @@ export async function processAuthorizationResponse({req}) {
271
281
  exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
272
282
  .catch(error => logger.error(
273
283
  'Could not set last exchange error: ' + error.message, {error}));
284
+ emitExchangeUpdated({workflow, exchange, step});
274
285
  throw e;
275
286
  }
276
287
  }
package/lib/vcapi.js CHANGED
@@ -5,7 +5,8 @@ import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from './exchanges.js';
6
6
  import {createChallenge as _createChallenge, verify} from './verify.js';
7
7
  import {
8
- buildPresentationFromResults, evaluateTemplate, generateRandom,
8
+ buildPresentationFromResults, emitExchangeUpdated,
9
+ evaluateTemplate, generateRandom,
9
10
  unenvelopePresentation, validateStep
10
11
  } from './helpers.js';
11
12
  import {exportJWK, generateKeyPair, importJWK} from 'jose';
@@ -93,6 +94,8 @@ export async function createExchange({workflow, exchange}) {
93
94
  export async function processExchange({req, res, workflow, exchangeRecord}) {
94
95
  const {exchange, meta} = exchangeRecord;
95
96
  let {updated: lastUpdated} = meta;
97
+ let step;
98
+
96
99
  try {
97
100
  // get any `verifiablePresentation` from the body...
98
101
  let receivedPresentation = req?.body?.verifiablePresentation;
@@ -100,7 +103,6 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
100
103
  // process exchange step(s)
101
104
  let i = 0;
102
105
  let currentStep = exchange.step;
103
- let step;
104
106
  while(true) {
105
107
  if(i++ > MAXIMUM_STEPS) {
106
108
  throw new BedrockError('Maximum steps exceeded.', {
@@ -193,9 +195,15 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
193
195
 
194
196
  // verify the received VP
195
197
  const expectedChallenge = isInitialStep ? exchange.id : undefined;
196
- const {allowUnprotectedPresentation = false} = step;
198
+ const {
199
+ allowUnprotectedPresentation = false,
200
+ verifyPresentationOptions = {},
201
+ verifyPresentationResultSchema
202
+ } = step;
197
203
  const verifyResult = await verify({
198
204
  workflow,
205
+ verifyPresentationOptions,
206
+ verifyPresentationResultSchema,
199
207
  verifiablePresentationRequest: step.verifiablePresentationRequest,
200
208
  presentation: receivedPresentation,
201
209
  allowUnprotectedPresentation,
@@ -249,6 +257,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
249
257
  try {
250
258
  exchange.sequence++;
251
259
  await exchanges.update({workflowId: workflow.id, exchange});
260
+ emitExchangeUpdated({workflow, exchange, step});
252
261
  lastUpdated = Date.now();
253
262
  } catch(e) {
254
263
  exchange.sequence--;
@@ -273,6 +282,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
273
282
  try {
274
283
  exchange.sequence++;
275
284
  await exchanges.complete({workflowId: workflow.id, exchange});
285
+ emitExchangeUpdated({workflow, exchange, step});
276
286
  } catch(e) {
277
287
  exchange.sequence--;
278
288
  throw e;
@@ -305,6 +315,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
305
315
  exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
306
316
  .catch(error => logger.error(
307
317
  'Could not set last exchange error: ' + error.message, {error}));
318
+ emitExchangeUpdated({workflow, exchange, step});
308
319
  throw e;
309
320
  }
310
321
  }
package/lib/verify.js CHANGED
@@ -4,8 +4,13 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
6
6
  import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
7
- import {getZcapClient, stripStacktrace} from './helpers.js';
7
+ import {
8
+ createVerifyOptions,
9
+ getZcapClient,
10
+ stripStacktrace
11
+ } from './helpers.js';
8
12
  import {importJWK, jwtVerify} from 'jose';
13
+ import {compile} from '@bedrock/validation';
9
14
  import {didIo} from '@bedrock/did-io';
10
15
 
11
16
  const {util: {BedrockError}} = bedrock;
@@ -25,8 +30,9 @@ export async function createChallenge({workflow} = {}) {
25
30
  }
26
31
 
27
32
  export async function verify({
28
- workflow, verifiablePresentationRequest, presentation,
29
- allowUnprotectedPresentation = false, expectedChallenge
33
+ workflow, verifyPresentationOptions, verifiablePresentationRequest,
34
+ presentation, allowUnprotectedPresentation = false, expectedChallenge,
35
+ verifyPresentationResultSchema
30
36
  } = {}) {
31
37
  // create zcap client for verifying
32
38
  const {zcapClient, zcaps} = await getZcapClient({workflow});
@@ -46,17 +52,18 @@ export async function verify({
46
52
  new URL(workflow.id).origin;
47
53
  let result;
48
54
  try {
55
+ const options = createVerifyOptions({
56
+ verifyPresentationOptions,
57
+ expectedChallenge,
58
+ verifiablePresentationRequest,
59
+ presentation,
60
+ domain,
61
+ checks
62
+ });
49
63
  result = await zcapClient.write({
50
64
  capability,
51
65
  json: {
52
- options: {
53
- // FIXME: support multi-proof presentations?
54
- challenge: expectedChallenge ??
55
- verifiablePresentationRequest.challenge ??
56
- presentation?.proof?.challenge,
57
- domain,
58
- checks
59
- },
66
+ options,
60
67
  verifiablePresentation: presentation
61
68
  }
62
69
  });
@@ -120,7 +127,15 @@ export async function verify({
120
127
  const verificationMethod = presentationResult?.results[0]
121
128
  .verificationMethod ?? null;
122
129
 
123
- // FIXME: ensure VP satisfies VPR
130
+ // validate against the verify presentation result schema, if applicable
131
+ if(verifyPresentationResultSchema) {
132
+ const {jsonSchema: schema} = verifyPresentationResultSchema;
133
+ const validate = compile({schema});
134
+ const {valid, error} = validate(result.data);
135
+ if(!valid) {
136
+ throw error;
137
+ }
138
+ }
124
139
 
125
140
  return {
126
141
  verified,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "7.1.1",
3
+ "version": "7.3.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -39,6 +39,7 @@
39
39
  "@digitalbazaar/ed25519-multikey": "^1.3.1",
40
40
  "@digitalbazaar/ed25519-signature-2020": "^5.4.0",
41
41
  "@digitalbazaar/ezcap": "^4.1.0",
42
+ "@digitalbazaar/http-client": "^4.2.0",
42
43
  "@digitalbazaar/oid4-client": "^4.3.0",
43
44
  "@digitalbazaar/vc": "^7.2.0",
44
45
  "assert-plus": "^1.0.0",
@@ -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 {MAX_ISSUER_INSTANCES} from '../lib/constants.js';
5
5
  import {schemas} from '@bedrock/validation';
@@ -381,6 +381,7 @@ const step = {
381
381
  not: {
382
382
  required: [
383
383
  'allowUnprotectedPresentation',
384
+ 'callback',
384
385
  'createChallenge',
385
386
  'issueRequests',
386
387
  'jwtDidProofRequest',
@@ -400,6 +401,16 @@ const step = {
400
401
  allowUnprotectedPresentation: {
401
402
  type: 'boolean'
402
403
  },
404
+ callback: {
405
+ type: 'object',
406
+ required: ['url'],
407
+ additionalProperties: false,
408
+ properties: {
409
+ url: {
410
+ type: 'string'
411
+ }
412
+ }
413
+ },
403
414
  createChallenge: {
404
415
  type: 'boolean'
405
416
  },
@@ -486,6 +497,28 @@ const step = {
486
497
  stepTemplate: typedTemplate,
487
498
  verifiablePresentationRequest: {
488
499
  type: 'object'
500
+ },
501
+ verifyPresentationOptions: {
502
+ type: 'object',
503
+ properties: {
504
+ checks: {
505
+ type: 'object'
506
+ }
507
+ },
508
+ additionalProperties: true
509
+ },
510
+ verifyPresentationResultSchema: {
511
+ type: 'object',
512
+ required: ['type', 'jsonSchema'],
513
+ additionalProperties: false,
514
+ properties: {
515
+ type: {
516
+ type: 'string'
517
+ },
518
+ jsonSchema: {
519
+ type: 'object'
520
+ }
521
+ }
489
522
  }
490
523
  }
491
524
  };