@bedrock/vc-delivery 5.2.0 → 5.3.1

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/exchanges.js CHANGED
@@ -3,18 +3,16 @@
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
7
  import assert from 'assert-plus';
7
8
  import {logger} from './logger.js';
8
- import {parseLocalId} from './helpers.js';
9
+ import {serializeError} from 'serialize-error';
9
10
 
10
11
  const {util: {BedrockError}} = bedrock;
11
12
 
12
- /* Note: Exchanges either have TTLs and can be "completed" or they persist
13
- and never "complete" nor expire.
14
-
15
- Exchanges are always in one of three states: `pending`, `complete`, or
16
- `invalid`. They can only transistion from `pending` to `complete` or from
17
- `complete` to `invalid`.
13
+ /* Note: Exchanges have default TTLs of 15 minutes and are always in one of
14
+ four states: `pending`, `active`, `complete`, or `invalid`. They can only
15
+ transition from `pending` to `complete` or from `complete` to `invalid`.
18
16
 
19
17
  If an exchange is marked as complete, any attempt to mark it complete again
20
18
  will result in an action, as specified in the exchange record, being taken
@@ -28,6 +26,15 @@ provided during the exchange a capability to verify them must be provided. */
28
26
 
29
27
  const COLLECTION_NAME = 'vc-exchange';
30
28
 
29
+ // allow updates to the last error every 500ms
30
+ const LAST_ERROR_UPDATE_CONSTRAINTS = {
31
+ // if the exchange has been updated 5 or more times, apply the time limit
32
+ sequenceThreshold: 5,
33
+ // 1 second must expire before updating last error once sequence threshold
34
+ // has been hit
35
+ updateTimeLimit: 1000
36
+ };
37
+
31
38
  bedrock.events.on('bedrock-mongodb.ready', async () => {
32
39
  await database.openCollections([COLLECTION_NAME]);
33
40
 
@@ -88,9 +95,14 @@ export async function insert({workflowId, exchange}) {
88
95
  // build exchange record
89
96
  const now = Date.now();
90
97
  const meta = {created: now, updated: now};
98
+ // possible states are: `pending`, `active`, `complete`, or `invalid`
99
+ exchange = {...exchange, sequence: 0, state: 'pending'};
91
100
  if(exchange.ttl !== undefined) {
92
- // TTL is in seconds
93
- meta.expires = new Date(now + exchange.ttl * 1000);
101
+ // TTL is in seconds, convert to `expires`
102
+ const expires = new Date(now + exchange.ttl * 1000);
103
+ meta.expires = expires;
104
+ exchange.expires = expires.toISOString().replace(/\.\d+Z$/, 'Z');
105
+ delete exchange.ttl;
94
106
  }
95
107
  const {localId: localWorkflowId} = parseLocalId({id: workflowId});
96
108
  const record = {
@@ -98,8 +110,7 @@ export async function insert({workflowId, exchange}) {
98
110
  // backwards compatibility: enable existing systems to find record
99
111
  localExchangerId: localWorkflowId,
100
112
  meta,
101
- // possible states are: `pending`, `active`, `complete`, or `invalid`
102
- exchange: {...exchange, sequence: 0, state: 'pending'}
113
+ exchange
103
114
  };
104
115
 
105
116
  // insert the exchange and get the updated record
@@ -129,12 +140,16 @@ export async function insert({workflowId, exchange}) {
129
140
  * @param {string} options.workflowId - The ID of the workflow that the
130
141
  * exchange is associated with.
131
142
  * @param {string} options.id - The ID of the exchange to retrieve.
143
+ * @param {boolean} [options.allowExpired=false] - Controls whether an expired
144
+ * exchange that is still in the database can be retrieved or not.
132
145
  * @param {boolean} [options.explain=false] - An optional explain boolean.
133
146
  *
134
147
  * @returns {Promise<object | ExplainObject>} Resolves with the record that
135
148
  * matches the query or an ExplainObject if `explain=true`.
136
149
  */
137
- export async function get({workflowId, id, explain = false} = {}) {
150
+ export async function get({
151
+ workflowId, id, allowExpired = false, explain = false
152
+ } = {}) {
138
153
  assert.string(workflowId, 'workflowId');
139
154
  assert.string(id, 'id');
140
155
 
@@ -160,7 +175,16 @@ export async function get({workflowId, id, explain = false} = {}) {
160
175
  return cursor.explain('executionStats');
161
176
  }
162
177
 
163
- const record = await collection.findOne(query, {projection});
178
+ let record = await collection.findOne(query, {projection});
179
+ if(record?.exchange.expires && !allowExpired) {
180
+ // ensure `expires` is enforced programmatically even if background job
181
+ // has not yet removed the record
182
+ const now = new Date();
183
+ const expires = new Date(record.exchange.expires);
184
+ if(now >= expires) {
185
+ record = null;
186
+ }
187
+ }
164
188
  if(!record) {
165
189
  throw new BedrockError('Exchange not found.', {
166
190
  name: 'NotFoundError',
@@ -196,8 +220,8 @@ export async function get({workflowId, id, explain = false} = {}) {
196
220
  }
197
221
 
198
222
  /**
199
- * Updates a pending exchange with new state, variables, step, and TTL
200
- * information.
223
+ * Updates a pending or active exchange with new state, variables, step, and
224
+ * TTL, and error information.
201
225
  *
202
226
  * @param {object} options - The options to use.
203
227
  * @param {string} options.workflowId - The ID of the workflow the exchange
@@ -214,21 +238,7 @@ export async function update({workflowId, exchange, explain = false} = {}) {
214
238
  const {id} = exchange;
215
239
 
216
240
  // build update
217
- const now = Date.now();
218
- const update = {
219
- $inc: {'exchange.sequence': 1},
220
- $set: {'exchange.state': exchange.state, 'meta.updated': now}
221
- };
222
- // update exchange `variables`, `step`, and `ttl`
223
- if(exchange.variables) {
224
- update.$set['exchange.variables'] = exchange.variables;
225
- }
226
- if(exchange.step !== undefined) {
227
- update.$set['exchange.step'] = exchange.step;
228
- }
229
- if(exchange.ttl !== undefined) {
230
- update.$set['exchange.ttl'] = exchange.ttl;
231
- }
241
+ const update = _buildUpdate({exchange, complete: false});
232
242
 
233
243
  const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
234
244
 
@@ -304,27 +314,13 @@ export async function update({workflowId, exchange, explain = false} = {}) {
304
314
  export async function complete({workflowId, exchange, explain = false} = {}) {
305
315
  assert.string(workflowId, 'workflowId');
306
316
  assert.object(exchange, 'exchange');
317
+ if(exchange.state !== 'complete') {
318
+ throw new Error('"exchange.state" must be set to "complete".');
319
+ }
307
320
  const {id} = exchange;
308
321
 
309
322
  // build update
310
- const now = Date.now();
311
- const update = {
312
- $inc: {'exchange.sequence': 1},
313
- $set: {
314
- 'exchange.state': 'complete',
315
- 'meta.updated': now
316
- }
317
- };
318
- // update exchange `variables.results[step]`, `step`, and `ttl`
319
- if(exchange.variables?.results) {
320
- update.$set['exchange.variables.results'] = exchange.variables.results;
321
- }
322
- if(exchange.step !== undefined) {
323
- update.$set['exchange.step'] = exchange.step;
324
- }
325
- if(exchange.ttl !== undefined) {
326
- update.$set['exchange.ttl'] = exchange.ttl;
327
- }
323
+ const update = _buildUpdate({exchange, complete: true});
328
324
 
329
325
  const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
330
326
 
@@ -408,6 +404,105 @@ export async function complete({workflowId, exchange, explain = false} = {}) {
408
404
  });
409
405
  }
410
406
 
407
+ /**
408
+ * Sets the last error associated with an exchange, provided that the exchange
409
+ * has not been recently or frequently updated.
410
+ *
411
+ * @param {object} options - The options to use.
412
+ * @param {string} options.workflowId - The ID of the workflow the exchange
413
+ * is associated with.
414
+ * @param {object} options.exchange - The exchange to update with `lastError`
415
+ * set.
416
+ * @param {object} options.lastUpdated - The last update time (in milliseconds).
417
+ * @param {boolean} [options.explain=false] - An optional explain boolean.
418
+ *
419
+ * @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
420
+ * success or an ExplainObject if `explain=true`.
421
+ */
422
+ export async function setLastError({
423
+ workflowId, exchange, lastUpdated, explain = false
424
+ } = {}) {
425
+ assert.string(workflowId, 'workflowId');
426
+ assert.object(exchange, 'exchange');
427
+ assert.object(exchange.lastError, 'exchange.lastError');
428
+ assert.number(lastUpdated, 'lastUpdate');
429
+
430
+ // prevent too many updates to an exchange to write the last error to it
431
+ // by limiting to a few
432
+ const now = Date.now();
433
+ if(exchange.sequence > LAST_ERROR_UPDATE_CONSTRAINTS.sequenceThreshold &&
434
+ now < (lastUpdated + LAST_ERROR_UPDATE_CONSTRAINTS.updateTimeLimit)) {
435
+ // deny update, too many last error updates
436
+ return false;
437
+ }
438
+
439
+ // build update
440
+ const update = {
441
+ $inc: {'exchange.sequence': 1},
442
+ $set: {
443
+ 'meta.updated': now,
444
+ 'exchange.lastError': serializeError(stripStacktrace(exchange.lastError))
445
+ }
446
+ };
447
+
448
+ const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
449
+
450
+ const {id} = exchange;
451
+ const collection = database.collections[COLLECTION_NAME];
452
+ const query = {
453
+ localWorkflowId,
454
+ 'exchange.id': id,
455
+ // exchange sequence must match previous sequence
456
+ 'exchange.sequence': exchange.sequence - 1
457
+ };
458
+ // backwards compatibility: query on `localExchangerId`
459
+ if(base.endsWith('/exchangers')) {
460
+ query.localWorkflowId = {$in: [null, localWorkflowId]};
461
+ query.localExchangerId = localWorkflowId;
462
+ }
463
+
464
+ if(explain) {
465
+ // 'find().limit(1)' is used here because 'updateOne()' doesn't return a
466
+ // cursor which allows the use of the explain function.
467
+ const cursor = await collection.find(query).limit(1);
468
+ return cursor.explain('executionStats');
469
+ }
470
+
471
+ try {
472
+ const result = await collection.updateOne(query, update);
473
+ if(result.result.n > 0) {
474
+ // document modified: success
475
+ return true;
476
+ }
477
+ } catch(e) {
478
+ throw new BedrockError('Could not update exchange.', {
479
+ name: 'OperationError',
480
+ details: {
481
+ public: true,
482
+ httpStatusCode: 500
483
+ },
484
+ cause: e
485
+ });
486
+ }
487
+
488
+ // if no document was matched, try to get an existing exchange; if the
489
+ // exchange does not exist, a not found error will be automatically thrown
490
+ await get({workflowId, id});
491
+
492
+ /* Note: Here the exchange *does* exist, but the step or state did not
493
+ match which is a conflict error. */
494
+
495
+ // throw duplicate completed exchange error
496
+ throw new BedrockError('Could not update exchange; conflict error.', {
497
+ name: 'InvalidStateError',
498
+ details: {
499
+ public: true,
500
+ // this is a client-side conflict error
501
+ httpStatusCode: 409
502
+ }
503
+ });
504
+ }
505
+
411
506
  async function _invalidateExchange({record}) {
412
507
  try {
413
508
  // mark exchange invalid, but do not throw any error to client; only log it
@@ -463,6 +558,48 @@ async function _markExchangeInvalid({record}) {
463
558
  }
464
559
  }
465
560
 
561
+ function _buildUpdate({exchange, complete}) {
562
+ // build update
563
+ const now = Date.now();
564
+ const update = {
565
+ $inc: {'exchange.sequence': 1},
566
+ $set: {'exchange.state': exchange.state, 'meta.updated': now},
567
+ $unset: {}
568
+ };
569
+ if(complete) {
570
+ // exchange complete, only update results
571
+ if(exchange.variables?.results) {
572
+ update.$set['exchange.variables.results'] = exchange.variables.results;
573
+ }
574
+ } else {
575
+ // exchange not complete, update all variables
576
+ if(exchange.variables) {
577
+ update.$set['exchange.variables'] = exchange.variables;
578
+ }
579
+ }
580
+ if(exchange.step !== undefined) {
581
+ update.$set['exchange.step'] = exchange.step;
582
+ }
583
+ // only set `ttl` if expires not previously set / has been cleared
584
+ if(exchange.ttl !== undefined && exchange.expires === undefined) {
585
+ // TTL is in seconds, convert to expires
586
+ const expires = new Date(now + exchange.ttl * 1000);
587
+ // unset and previously set `ttl`
588
+ update.$unset['exchange.ttl'] = true;
589
+ update.$set['meta.expires'] = expires;
590
+ update.$set['exchange.expires'] =
591
+ expires.toISOString().replace(/\.\d+Z$/, 'Z');
592
+ }
593
+ if(exchange.lastError !== undefined) {
594
+ update.$set['exchange.lastError'] =
595
+ serializeError(stripStacktrace(exchange.lastError));
596
+ } else {
597
+ update.$unset['exchange.lastError'] = true;
598
+ }
599
+
600
+ return update;
601
+ }
602
+
466
603
  /**
467
604
  * An object containing information on the query plan.
468
605
  *
package/lib/helpers.js CHANGED
@@ -7,6 +7,7 @@ import {decodeId, generateId} from 'bnid';
7
7
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
8
8
  import {httpsAgent} from '@bedrock/https-agent';
9
9
  import jsonata from 'jsonata';
10
+ import {serializeError} from 'serialize-error';
10
11
  import {serviceAgents} from '@bedrock/service-agent';
11
12
  import {ZcapClient} from '@digitalbazaar/ezcap';
12
13
 
@@ -104,6 +105,21 @@ export function decodeLocalId({localId} = {}) {
104
105
  }));
105
106
  }
106
107
 
108
+ export function stripStacktrace(error) {
109
+ error = serializeError(error);
110
+ delete error.stack;
111
+ if(error.errors) {
112
+ error.errors = error.errors.map(stripStacktrace);
113
+ }
114
+ if(Array.isArray(error.details?.errors)) {
115
+ error.details.errors = error.details.errors.map(stripStacktrace);
116
+ }
117
+ if(error.cause) {
118
+ error.cause = stripStacktrace(error.cause);
119
+ }
120
+ return error;
121
+ }
122
+
107
123
  export async function unenvelopeCredential({
108
124
  envelopedCredential, format
109
125
  } = {}) {
package/lib/http.js CHANGED
@@ -113,8 +113,8 @@ export async function addRoutes({app, service} = {}) {
113
113
  getConfigMiddleware,
114
114
  asyncHandler(async (req, res) => {
115
115
  const {config: workflow} = req.serviceObject;
116
- const {exchange} = await req.getExchange();
117
- await processExchange({req, res, workflow, exchange});
116
+ const exchangeRecord = await req.getExchange();
117
+ await processExchange({req, res, workflow, exchangeRecord});
118
118
  }));
119
119
 
120
120
  // create OID4* routes to be used with each individual exchange
@@ -10,6 +10,7 @@ import {importJWK, SignJWT} from 'jose';
10
10
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
11
11
  import {getAuthorizationRequest} from './oid4vp.js';
12
12
  import {issue} from '../issue.js';
13
+ import {logger} from '../logger.js';
13
14
  import {timingSafeEqual} from 'node:crypto';
14
15
  import {verifyDidProofJwt} from '../verify.js';
15
16
 
@@ -145,118 +146,7 @@ export async function processCredentialRequests({req, res, isBatchRequest}) {
145
146
  // ensure oauth2 access token is valid
146
147
  await _checkAuthz({req, workflow, exchange});
147
148
 
148
- // validate body against expected credential requests
149
- const {openId: {expectedCredentialRequests}} = exchange;
150
- let credentialRequests;
151
- if(isBatchRequest) {
152
- ({credential_requests: credentialRequests} = req.body);
153
- } else {
154
- if(expectedCredentialRequests.length > 1) {
155
- // FIXME: it is no longer the case that the batch endpoint must be used
156
- // for multiple requests; determine if the request has changed
157
-
158
- // clients interacting with exchanges with more than one VC to be
159
- // delivered must use the "batch credential" endpoint
160
- // FIXME: improve error
161
- throw new Error('batch_credential_endpoint must be used');
162
- }
163
- credentialRequests = [req.body];
164
- }
165
-
166
- // before asserting, normalize credential requests to use `type` instead of
167
- // `types`; this is to allow for OID4VCI draft implementers that followed
168
- // the non-normative examples
169
- _normalizeCredentialDefinitionTypes({credentialRequests});
170
- const {format} = _assertCredentialRequests({
171
- workflow, credentialRequests, expectedCredentialRequests
172
- });
173
-
174
- // process exchange step if present
175
- const currentStep = exchange.step;
176
- if(currentStep) {
177
- let step = workflow.steps[exchange.step];
178
- if(step.stepTemplate) {
179
- // generate step from the template; assume the template type is
180
- // `jsonata` per the JSON schema
181
- step = await evaluateTemplate(
182
- {workflow, exchange, typedTemplate: step.stepTemplate});
183
- if(Object.keys(step).length === 0) {
184
- throw new BedrockError('Could not create exchange step.', {
185
- name: 'DataError',
186
- details: {httpStatusCode: 500, public: true}
187
- });
188
- }
189
- }
190
-
191
- // do late workflow configuration validation
192
- const {jwtDidProofRequest, openId} = step;
193
- // use of `jwtDidProofRequest` and `openId` together is prohibited
194
- if(jwtDidProofRequest && openId) {
195
- throw new BedrockError(
196
- 'Invalid workflow configuration; only one of ' +
197
- '"jwtDidProofRequest" and "openId" is permitted in a step.', {
198
- name: 'DataError',
199
- details: {httpStatusCode: 500, public: true}
200
- });
201
- }
202
-
203
- // check to see if step supports OID4VP during OID4VCI
204
- if(step.openId) {
205
- // if there is no `presentationSubmission`, request one
206
- const {results} = exchange.variables;
207
- if(!results?.[exchange.step]?.openId?.presentationSubmission) {
208
- // FIXME: optimize away double step-template processing that currently
209
- // occurs when calling `_getAuthorizationRequest`
210
- const {
211
- authorizationRequest
212
- } = await getAuthorizationRequest({req});
213
- return _requestOID4VP({authorizationRequest, res});
214
- }
215
- // otherwise drop down below to complete exchange...
216
- } else if(jwtDidProofRequest) {
217
- // handle OID4VCI specialized JWT DID Proof request...
218
-
219
- // `proof` must be in every credential request; if any request is missing
220
- // `proof` then request a DID proof
221
- if(credentialRequests.some(cr => !cr.proof?.jwt)) {
222
- return _requestDidProof({res, exchangeRecord});
223
- }
224
-
225
- // verify every DID proof and get resulting DIDs
226
- const results = await Promise.all(
227
- credentialRequests.map(async cr => {
228
- const {proof: {jwt}} = cr;
229
- const {did} = await verifyDidProofJwt({workflow, exchange, jwt});
230
- return did;
231
- }));
232
- // require `did` to be the same for every proof
233
- // FIXME: determine if this needs to be more flexible
234
- const did = results[0];
235
- if(results.some(d => did !== d)) {
236
- // FIXME: improve error
237
- throw new Error('every DID must be the same');
238
- }
239
- // store did results in variables associated with current step
240
- if(!exchange.variables.results) {
241
- exchange.variables.results = {};
242
- }
243
- exchange.variables.results[currentStep] = {
244
- // common use case of DID Authentication; provide `did` for ease
245
- // of use in templates
246
- did
247
- };
248
- }
249
- }
250
-
251
- // mark exchange complete
252
- exchange.sequence++;
253
- await exchanges.complete({workflowId: workflow.id, exchange});
254
-
255
- // FIXME: decide what the best recovery path is if delivery fails (but no
256
- // replay attack detected) after exchange has been marked complete
257
-
258
- // issue VCs
259
- return issue({workflow, exchange, format});
149
+ return _processExchange({req, res, workflow, exchangeRecord, isBatchRequest});
260
150
  }
261
151
 
262
152
  function _assertCredentialRequests({
@@ -499,6 +389,147 @@ function _normalizeCredentialDefinitionTypes({credentialRequests}) {
499
389
  }
500
390
  }
501
391
 
392
+ async function _processExchange({
393
+ req, res, workflow, exchangeRecord, isBatchRequest
394
+ }) {
395
+ const {id: workflowId} = workflow;
396
+ const {exchange, meta} = exchangeRecord;
397
+ let {updated: lastUpdated} = meta;
398
+ try {
399
+ // validate body against expected credential requests
400
+ const {openId: {expectedCredentialRequests}} = exchange;
401
+ let credentialRequests;
402
+ if(isBatchRequest) {
403
+ ({credential_requests: credentialRequests} = req.body);
404
+ } else {
405
+ if(expectedCredentialRequests.length > 1) {
406
+ // FIXME: it is no longer the case that the batch endpoint must be used
407
+ // for multiple requests; determine if the request has changed
408
+
409
+ // clients interacting with exchanges with more than one VC to be
410
+ // delivered must use the "batch credential" endpoint
411
+ // FIXME: improve error
412
+ throw new Error('batch_credential_endpoint must be used');
413
+ }
414
+ credentialRequests = [req.body];
415
+ }
416
+
417
+ // before asserting, normalize credential requests to use `type` instead of
418
+ // `types`; this is to allow for OID4VCI draft implementers that followed
419
+ // the non-normative examples
420
+ _normalizeCredentialDefinitionTypes({credentialRequests});
421
+ const {format} = _assertCredentialRequests({
422
+ workflow, credentialRequests, expectedCredentialRequests
423
+ });
424
+
425
+ // process exchange step if present
426
+ const currentStep = exchange.step;
427
+ if(currentStep) {
428
+ let step = workflow.steps[exchange.step];
429
+ if(step.stepTemplate) {
430
+ // generate step from the template; assume the template type is
431
+ // `jsonata` per the JSON schema
432
+ step = await evaluateTemplate(
433
+ {workflow, exchange, typedTemplate: step.stepTemplate});
434
+ if(Object.keys(step).length === 0) {
435
+ throw new BedrockError('Could not create exchange step.', {
436
+ name: 'DataError',
437
+ details: {httpStatusCode: 500, public: true}
438
+ });
439
+ }
440
+ }
441
+
442
+ // do late workflow configuration validation
443
+ const {jwtDidProofRequest, openId} = step;
444
+ // use of `jwtDidProofRequest` and `openId` together is prohibited
445
+ if(jwtDidProofRequest && openId) {
446
+ throw new BedrockError(
447
+ 'Invalid workflow configuration; only one of ' +
448
+ '"jwtDidProofRequest" and "openId" is permitted in a step.', {
449
+ name: 'DataError',
450
+ details: {httpStatusCode: 500, public: true}
451
+ });
452
+ }
453
+
454
+ // check to see if step supports OID4VP during OID4VCI
455
+ if(step.openId) {
456
+ // if there is no `presentationSubmission`, request one
457
+ const {results} = exchange.variables;
458
+ if(!results?.[exchange.step]?.openId?.presentationSubmission) {
459
+ // FIXME: optimize away double step-template processing that
460
+ // currently occurs when calling `_getAuthorizationRequest`
461
+ const {
462
+ authorizationRequest
463
+ } = await getAuthorizationRequest({req});
464
+ return _requestOID4VP({authorizationRequest, res});
465
+ }
466
+ // otherwise drop down below to complete exchange...
467
+ } else if(jwtDidProofRequest) {
468
+ // handle OID4VCI specialized JWT DID Proof request...
469
+
470
+ // `proof` must be in every credential request; if any request is
471
+ // missing `proof` then request a DID proof
472
+ if(credentialRequests.some(cr => !cr.proof?.jwt)) {
473
+ return _requestDidProof({res, exchangeRecord});
474
+ }
475
+
476
+ // verify every DID proof and get resulting DIDs
477
+ const results = await Promise.all(
478
+ credentialRequests.map(async cr => {
479
+ const {proof: {jwt}} = cr;
480
+ const {did} = await verifyDidProofJwt({workflow, exchange, jwt});
481
+ return did;
482
+ }));
483
+ // require `did` to be the same for every proof
484
+ // FIXME: determine if this needs to be more flexible
485
+ const did = results[0];
486
+ if(results.some(d => did !== d)) {
487
+ // FIXME: improve error
488
+ throw new Error('every DID must be the same');
489
+ }
490
+ // store did results in variables associated with current step
491
+ if(!exchange.variables.results) {
492
+ exchange.variables.results = {};
493
+ }
494
+ exchange.variables.results[currentStep] = {
495
+ // common use case of DID Authentication; provide `did` for ease
496
+ // of use in templates
497
+ did
498
+ };
499
+ }
500
+ }
501
+
502
+ // mark exchange complete
503
+ exchange.state = 'complete';
504
+ try {
505
+ exchange.sequence++;
506
+ await exchanges.complete({workflowId, exchange});
507
+ lastUpdated = Date.now();
508
+ } catch(e) {
509
+ exchange.sequence--;
510
+ throw e;
511
+ }
512
+
513
+ // FIXME: decide what the best recovery path is if delivery fails (but no
514
+ // replay attack detected) after exchange has been marked complete
515
+
516
+ // issue VCs
517
+ return issue({workflow, exchange, format});
518
+ } catch(e) {
519
+ if(e.name === 'InvalidStateError') {
520
+ throw e;
521
+ }
522
+ // write last error if exchange hasn't been frequently updated
523
+ const copy = {...exchange};
524
+ copy.sequence++;
525
+ copy.lastError = e;
526
+ exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
527
+ .catch(error => logger.error(
528
+ 'Could not set last exchange error: ' + error.message, {error}));
529
+ throw e;
530
+ }
531
+ }
532
+
502
533
  async function _requestDidProof({res, exchangeRecord}) {
503
534
  /* `9.4 Credential Issuer-provided nonce` allows the credential
504
535
  issuer infrastructure to provide the nonce via an error:
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../schemas/bedrock-vc-workflow.js';
11
11
  import {compile} from '@bedrock/validation';
12
12
  import {klona} from 'klona';
13
+ import {logger} from '../logger.js';
13
14
  import {oid4vp} from '@digitalbazaar/oid4-client';
14
15
  import {verify} from '../verify.js';
15
16
 
@@ -139,10 +140,11 @@ export async function getAuthorizationRequest({req}) {
139
140
  }
140
141
 
141
142
  if(updateExchange) {
142
- exchange.sequence++;
143
143
  try {
144
+ exchange.sequence++;
144
145
  await exchanges.update({workflowId: workflow.id, exchange});
145
146
  } catch(e) {
147
+ exchange.sequence--;
146
148
  if(e.name !== 'InvalidStateError') {
147
149
  // unrecoverable error
148
150
  throw e;
@@ -167,89 +169,111 @@ export async function processAuthorizationResponse({req}) {
167
169
  const {config: workflow} = req.serviceObject;
168
170
  const exchangeRecord = await req.getExchange();
169
171
  let {exchange} = exchangeRecord;
170
-
171
- // get authorization request and updated exchange associated with exchange
172
- const arRequest = await getAuthorizationRequest({req});
173
- const {authorizationRequest, step} = arRequest;
174
- ({exchange} = arRequest);
175
-
176
- // FIXME: check the VP against the presentation submission if requested
177
- // FIXME: check the VP against "trustedIssuer" in VPR, if provided
178
- const {presentationSchema} = step;
179
- if(presentationSchema) {
180
- // if the VP is enveloped, validate the contents of the envelope
181
- const toValidate = envelope ? envelope.contents : presentation;
182
-
183
- // validate the received VP / envelope contents
184
- const {jsonSchema: schema} = presentationSchema;
185
- const validate = compile({schema});
186
- const {valid, error} = validate(toValidate);
187
- if(!valid) {
188
- throw error;
172
+ let {meta: {updated: lastUpdated}} = exchangeRecord;
173
+ try {
174
+ // get authorization request and updated exchange associated with exchange
175
+ const arRequest = await getAuthorizationRequest({req});
176
+ const {authorizationRequest, step} = arRequest;
177
+ ({exchange} = arRequest);
178
+
179
+ // FIXME: check the VP against the presentation submission if requested
180
+ // FIXME: check the VP against "trustedIssuer" in VPR, if provided
181
+ const {presentationSchema} = step;
182
+ if(presentationSchema) {
183
+ // if the VP is enveloped, validate the contents of the envelope
184
+ const toValidate = envelope ? envelope.contents : presentation;
185
+
186
+ // validate the received VP / envelope contents
187
+ const {jsonSchema: schema} = presentationSchema;
188
+ const validate = compile({schema});
189
+ const {valid, error} = validate(toValidate);
190
+ if(!valid) {
191
+ throw error;
192
+ }
189
193
  }
190
- }
191
194
 
192
- // verify the received VP
193
- const {verifiablePresentationRequest} = await oid4vp.toVpr(
194
- {authorizationRequest});
195
- const {allowUnprotectedPresentation = false} = step;
196
- const verifyResult = await verify({
197
- workflow,
198
- verifiablePresentationRequest,
199
- presentation,
200
- allowUnprotectedPresentation,
201
- expectedChallenge: authorizationRequest.nonce
202
- });
203
- const {verificationMethod} = verifyResult;
195
+ // verify the received VP
196
+ const {verifiablePresentationRequest} = await oid4vp.toVpr(
197
+ {authorizationRequest});
198
+ const {allowUnprotectedPresentation = false} = step;
199
+ const verifyResult = await verify({
200
+ workflow,
201
+ verifiablePresentationRequest,
202
+ presentation,
203
+ allowUnprotectedPresentation,
204
+ expectedChallenge: authorizationRequest.nonce
205
+ });
206
+ const {verificationMethod} = verifyResult;
204
207
 
205
- // store VP results in variables associated with current step
206
- const currentStep = exchange.step;
207
- if(!exchange.variables.results) {
208
- exchange.variables.results = {};
209
- }
210
- const results = {
211
- // common use case of DID Authentication; provide `did` for ease
212
- // of use in template
213
- did: verificationMethod?.controller || null,
214
- verificationMethod,
215
- verifiablePresentation: presentation,
216
- openId: {
217
- authorizationRequest,
218
- presentationSubmission
208
+ // store VP results in variables associated with current step
209
+ const currentStep = exchange.step;
210
+ if(!exchange.variables.results) {
211
+ exchange.variables.results = {};
212
+ }
213
+ const results = {
214
+ // common use case of DID Authentication; provide `did` for ease
215
+ // of use in template
216
+ did: verificationMethod?.controller || null,
217
+ verificationMethod,
218
+ verifiablePresentation: presentation,
219
+ openId: {
220
+ authorizationRequest,
221
+ presentationSubmission
222
+ }
223
+ };
224
+ if(envelope) {
225
+ // normalize VP from inside envelope to `verifiablePresentation`
226
+ results.envelopedPresentation = presentation;
227
+ results.verifiablePresentation = verifyResult
228
+ .presentationResult.presentation;
229
+ }
230
+ exchange.variables.results[currentStep] = results;
231
+ try {
232
+ exchange.sequence++;
233
+
234
+ // if there is something to issue, update exchange, do not complete it
235
+ const {credentialTemplates = []} = workflow;
236
+ if(credentialTemplates?.length > 0 &&
237
+ (exchange.state === 'pending' || exchange.state === 'active')) {
238
+ // ensure exchange state is set to `active` (will be rejected as a
239
+ // conflict if the state in database at update time isn't `pending` or
240
+ // `active`)
241
+ exchange.state = 'active';
242
+ await exchanges.update({workflowId: workflow.id, exchange});
243
+ } else {
244
+ // mark exchange complete
245
+ exchange.state = 'complete';
246
+ await exchanges.complete({workflowId: workflow.id, exchange});
247
+ }
248
+ lastUpdated = Date.now();
249
+ } catch(e) {
250
+ exchange.sequence--;
251
+ throw e;
219
252
  }
220
- };
221
- if(envelope) {
222
- // normalize VP from inside envelope to `verifiablePresentation`
223
- results.envelopedPresentation = presentation;
224
- results.verifiablePresentation = verifyResult
225
- .presentationResult.presentation;
226
- }
227
- exchange.variables.results[currentStep] = results;
228
- exchange.sequence++;
229
-
230
- // if there is something to issue, update exchange, do not complete it
231
- const {credentialTemplates = []} = workflow;
232
- if(credentialTemplates?.length > 0 &&
233
- (exchange.state === 'pending' || exchange.state === 'active')) {
234
- // ensure exchange state is set to `active` (will be rejected as a
235
- // conflict if the state in database at update time isn't `pending` or
236
- // `active`)
237
- exchange.state = 'active';
238
- await exchanges.update({workflowId: workflow.id, exchange});
239
- } else {
240
- // mark exchange complete
241
- await exchanges.complete({workflowId: workflow.id, exchange});
242
- }
243
253
 
244
- const result = {};
254
+ const result = {};
245
255
 
246
- // include `redirect_uri` if specified in step
247
- const redirect_uri = step.openId?.redirect_uri;
248
- if(redirect_uri) {
249
- result.redirect_uri = redirect_uri;
250
- }
256
+ // include `redirect_uri` if specified in step
257
+ const redirect_uri = step.openId?.redirect_uri;
258
+ if(redirect_uri) {
259
+ result.redirect_uri = redirect_uri;
260
+ }
251
261
 
252
- return result;
262
+ return result;
263
+ } catch(e) {
264
+ if(e.name === 'InvalidStateError') {
265
+ throw e;
266
+ }
267
+ // write last error if exchange hasn't been frequently updated
268
+ const {id: workflowId} = workflow;
269
+ const copy = {...exchange};
270
+ copy.sequence++;
271
+ copy.lastError = e;
272
+ exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
273
+ .catch(error => logger.error(
274
+ 'Could not set last exchange error: ' + error.message, {error}));
275
+ throw e;
276
+ }
253
277
  }
254
278
 
255
279
  function _createClientMetaData() {
package/lib/vcapi.js CHANGED
@@ -90,179 +90,209 @@ export async function createExchange({workflow, exchange}) {
90
90
  return exchange;
91
91
  }
92
92
 
93
- export async function processExchange({req, res, workflow, exchange}) {
94
- // get any `verifiablePresentation` from the body...
95
- let receivedPresentation = req?.body?.verifiablePresentation;
93
+ export async function processExchange({req, res, workflow, exchangeRecord}) {
94
+ const {exchange, meta} = exchangeRecord;
95
+ let {updated: lastUpdated} = meta;
96
+ try {
97
+ // get any `verifiablePresentation` from the body...
98
+ let receivedPresentation = req?.body?.verifiablePresentation;
96
99
 
97
- // process exchange step(s)
98
- let i = 0;
99
- let currentStep = exchange.step;
100
- let step;
101
- while(true) {
102
- if(i++ > MAXIMUM_STEPS) {
103
- throw new BedrockError('Maximum steps exceeded.', {
104
- name: 'DataError',
105
- details: {httpStatusCode: 500, public: true}
106
- });
107
- }
100
+ // process exchange step(s)
101
+ let i = 0;
102
+ let currentStep = exchange.step;
103
+ let step;
104
+ while(true) {
105
+ if(i++ > MAXIMUM_STEPS) {
106
+ throw new BedrockError('Maximum steps exceeded.', {
107
+ name: 'DataError',
108
+ details: {httpStatusCode: 500, public: true}
109
+ });
110
+ }
108
111
 
109
- // no step present, break out to complete exchange
110
- if(!currentStep) {
111
- break;
112
- }
112
+ // no step present, break out to complete exchange
113
+ if(!currentStep) {
114
+ break;
115
+ }
116
+
117
+ // get current step details
118
+ step = workflow.steps[currentStep];
119
+ if(step.stepTemplate) {
120
+ // generate step from the template; assume the template type is
121
+ // `jsonata` per the JSON schema
122
+ step = await evaluateTemplate(
123
+ {workflow, exchange, typedTemplate: step.stepTemplate});
124
+ if(Object.keys(step).length === 0) {
125
+ throw new BedrockError('Empty step detected.', {
126
+ name: 'DataError',
127
+ details: {httpStatusCode: 500, public: true}
128
+ });
129
+ }
130
+ }
113
131
 
114
- // get current step details
115
- step = workflow.steps[currentStep];
116
- if(step.stepTemplate) {
117
- // generate step from the template; assume the template type is
118
- // `jsonata` per the JSON schema
119
- step = await evaluateTemplate(
120
- {workflow, exchange, typedTemplate: step.stepTemplate});
121
- if(Object.keys(step).length === 0) {
122
- throw new BedrockError('Empty step detected.', {
132
+ // if next step is the same as the current step, throw an error
133
+ if(step.nextStep === currentStep) {
134
+ throw new BedrockError('Cyclical step detected.', {
123
135
  name: 'DataError',
124
136
  details: {httpStatusCode: 500, public: true}
125
137
  });
126
138
  }
127
- }
128
139
 
129
- // if next step is the same as the current step, throw an error
130
- if(step.nextStep === currentStep) {
131
- throw new BedrockError('Cyclical step detected.', {
132
- name: 'DataError',
133
- details: {httpStatusCode: 500, public: true}
134
- });
135
- }
140
+ // handle VPR: if step requires it, then `verifiablePresentation` must
141
+ // be in the request
142
+ if(step.verifiablePresentationRequest) {
143
+ const {createChallenge} = step;
144
+ const isInitialStep = exchange.step === workflow.initialStep;
136
145
 
137
- // handle VPR: if step requires it, then `verifiablePresentation` must
138
- // be in the request
139
- if(step.verifiablePresentationRequest) {
140
- const {createChallenge} = step;
141
- const isInitialStep = exchange.step === workflow.initialStep;
146
+ // if no presentation was received in the body...
147
+ if(!receivedPresentation) {
148
+ const verifiablePresentationRequest = klona(
149
+ step.verifiablePresentationRequest);
150
+ if(createChallenge) {
151
+ /* Note: When creating a challenge, the initial step always
152
+ uses the local exchange ID because the initial step itself
153
+ is one-time use. Subsequent steps, which only VC-API (as opposed
154
+ to other protocols) supports creating additional challenges via
155
+ the VC-API verifier API. */
156
+ let challenge;
157
+ if(isInitialStep) {
158
+ challenge = exchange.id;
159
+ } else {
160
+ // generate a new challenge using verifier API
161
+ ({challenge} = await _createChallenge({workflow}));
162
+ }
163
+ verifiablePresentationRequest.challenge = challenge;
164
+ }
165
+ // send VPR and return
166
+ res.json({verifiablePresentationRequest});
167
+ // if exchange is pending, mark it as active out-of-band
168
+ if(exchange.state === 'pending') {
169
+ exchange.state = 'active';
170
+ exchange.sequence++;
171
+ exchanges.update({workflowId: workflow.id, exchange}).catch(
172
+ error => logger.error(
173
+ 'Could not mark exchange active: ' + error.message, {error}));
174
+ }
175
+ return;
176
+ }
142
177
 
143
- // if no presentation was received in the body...
144
- if(!receivedPresentation) {
145
- const verifiablePresentationRequest = klona(
146
- step.verifiablePresentationRequest);
147
- if(createChallenge) {
148
- /* Note: When creating a challenge, the initial step always
149
- uses the local exchange ID because the initial step itself
150
- is one-time use. Subsequent steps, which only VC-API (as opposed
151
- to other protocols) supports creating additional challenges via
152
- the VC-API verifier API. */
153
- let challenge;
154
- if(isInitialStep) {
155
- challenge = exchange.id;
178
+ const {presentationSchema} = step;
179
+ if(presentationSchema) {
180
+ // if the VP is enveloped, get the presentation from the envelope
181
+ let presentation;
182
+ if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
183
+ ({presentation} = await unenvelopePresentation({
184
+ envelopedPresentation: receivedPresentation
185
+ }));
156
186
  } else {
157
- // generate a new challenge using verifier API
158
- ({challenge} = await _createChallenge({workflow}));
187
+ presentation = receivedPresentation;
159
188
  }
160
- verifiablePresentationRequest.challenge = challenge;
161
- }
162
- // send VPR and return
163
- res.json({verifiablePresentationRequest});
164
- // if exchange is pending, mark it as active out-of-band
165
- if(exchange.state === 'pending') {
166
- exchange.state = 'active';
167
- exchange.sequence++;
168
- exchanges.update({workflowId: workflow.id, exchange}).catch(
169
- error => logger.error(
170
- 'Could not mark exchange active: ' + error.message, {error}));
171
- }
172
- return;
173
- }
174
189
 
175
- const {presentationSchema} = step;
176
- if(presentationSchema) {
177
- // if the VP is enveloped, get the presentation from the envelope
178
- let presentation;
179
- if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
180
- ({presentation} = await unenvelopePresentation({
181
- envelopedPresentation: receivedPresentation
182
- }));
183
- } else {
184
- presentation = receivedPresentation;
190
+ // validate the received VP
191
+ const {jsonSchema: schema} = presentationSchema;
192
+ const validate = compile({schema});
193
+ const {valid, error} = validate(presentation);
194
+ if(!valid) {
195
+ throw error;
196
+ }
185
197
  }
186
198
 
187
- // validate the received VP
188
- const {jsonSchema: schema} = presentationSchema;
189
- const validate = compile({schema});
190
- const {valid, error} = validate(presentation);
191
- if(!valid) {
192
- throw error;
199
+ // verify the received VP
200
+ const expectedChallenge = isInitialStep ? exchange.id : undefined;
201
+ const {allowUnprotectedPresentation = false} = step;
202
+ const {verificationMethod} = await verify({
203
+ workflow,
204
+ verifiablePresentationRequest: step.verifiablePresentationRequest,
205
+ presentation: receivedPresentation,
206
+ allowUnprotectedPresentation,
207
+ expectedChallenge
208
+ });
209
+
210
+ // store VP results in variables associated with current step
211
+ if(!exchange.variables.results) {
212
+ exchange.variables.results = {};
193
213
  }
194
- }
214
+ exchange.variables.results[currentStep] = {
215
+ // common use case of DID Authentication; provide `did` for ease
216
+ // of use in templates and consistency with OID4VCI which only
217
+ // receives `did` not verification method nor VP
218
+ did: verificationMethod?.controller || null,
219
+ verificationMethod,
220
+ verifiablePresentation: receivedPresentation
221
+ };
195
222
 
196
- // verify the received VP
197
- const expectedChallenge = isInitialStep ? exchange.id : undefined;
198
- const {allowUnprotectedPresentation = false} = step;
199
- const {verificationMethod} = await verify({
200
- workflow,
201
- verifiablePresentationRequest: step.verifiablePresentationRequest,
202
- presentation: receivedPresentation,
203
- allowUnprotectedPresentation,
204
- expectedChallenge
205
- });
223
+ // clear received presentation as it has been processed
224
+ receivedPresentation = null;
206
225
 
207
- // store VP results in variables associated with current step
208
- if(!exchange.variables.results) {
209
- exchange.variables.results = {};
210
- }
211
- exchange.variables.results[currentStep] = {
212
- // common use case of DID Authentication; provide `did` for ease
213
- // of use in templates and consistency with OID4VCI which only
214
- // receives `did` not verification method nor VP
215
- did: verificationMethod?.controller || null,
216
- verificationMethod,
217
- verifiablePresentation: receivedPresentation
218
- };
226
+ // if there is no next step, break out to complete exchange
227
+ if(!step.nextStep) {
228
+ break;
229
+ }
219
230
 
220
- // clear received presentation as it has been processed
221
- receivedPresentation = null;
231
+ // update the exchange to go to the next step, then loop to send
232
+ // next VPR
233
+ currentStep = exchange.step = step.nextStep;
234
+ // ensure exchange state is active
235
+ if(exchange.state === 'pending') {
236
+ exchange.state = 'active';
237
+ }
238
+ try {
239
+ exchange.sequence++;
240
+ await exchanges.update({workflowId: workflow.id, exchange});
241
+ lastUpdated = Date.now();
242
+ } catch(e) {
243
+ exchange.sequence--;
244
+ throw e;
245
+ }
222
246
 
223
- // if there is no next step, break out to complete exchange
224
- if(!step.nextStep) {
225
- break;
247
+ // FIXME: there may be VCs to issue during this step, do so before
248
+ // sending the VPR above
249
+ } else if(step.nextStep) {
250
+ // next steps without VPRs are prohibited
251
+ throw new BedrockError(
252
+ 'Invalid step detected; continuing exchanges must include VPRs.', {
253
+ name: 'DataError',
254
+ details: {httpStatusCode: 500, public: true}
255
+ });
226
256
  }
257
+ }
227
258
 
228
- // update the exchange to go to the next step, then loop to send
229
- // next VPR
230
- currentStep = exchange.step = step.nextStep;
231
- // ensure exchange state is active
232
- if(exchange.state === 'pending') {
233
- exchange.state = 'active';
234
- }
259
+ // mark exchange complete
260
+ exchange.state = 'complete';
261
+ try {
235
262
  exchange.sequence++;
236
- await exchanges.update({workflowId: workflow.id, exchange});
237
-
238
- // FIXME: there may be VCs to issue during this step, do so before
239
- // sending the VPR above
240
- } else if(step.nextStep) {
241
- // next steps without VPRs are prohibited
242
- throw new BedrockError(
243
- 'Invalid step detected; continuing exchanges must include VPRs.', {
244
- name: 'DataError',
245
- details: {httpStatusCode: 500, public: true}
246
- });
263
+ await exchanges.complete({workflowId: workflow.id, exchange});
264
+ } catch(e) {
265
+ exchange.sequence--;
266
+ throw e;
247
267
  }
248
- }
268
+ lastUpdated = Date.now();
249
269
 
250
- // mark exchange complete
251
- exchange.sequence++;
252
- await exchanges.complete({workflowId: workflow.id, exchange});
270
+ // FIXME: decide what the best recovery path is if delivery fails (but no
271
+ // replay attack detected) after exchange has been marked complete
253
272
 
254
- // FIXME: decide what the best recovery path is if delivery fails (but no
255
- // replay attack detected) after exchange has been marked complete
273
+ // issue any VCs; may return an empty response if the step defines no
274
+ // VCs to issue
275
+ const {response} = await issue({workflow, exchange});
256
276
 
257
- // issue any VCs; may return an empty response if the step defines no
258
- // VCs to issue
259
- const {response} = await issue({workflow, exchange});
277
+ // if last `step` has a redirect URL, include it in the response
278
+ if(step?.redirectUrl) {
279
+ response.redirectUrl = step.redirectUrl;
280
+ }
260
281
 
261
- // if last `step` has a redirect URL, include it in the response
262
- if(step?.redirectUrl) {
263
- response.redirectUrl = step.redirectUrl;
282
+ // send response
283
+ res.json(response);
284
+ } catch(e) {
285
+ if(e.name === 'InvalidStateError') {
286
+ throw e;
287
+ }
288
+ // write last error if exchange hasn't been frequently updated
289
+ const {id: workflowId} = workflow;
290
+ const copy = {...exchange};
291
+ copy.sequence++;
292
+ copy.lastError = e;
293
+ exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
294
+ .catch(error => logger.error(
295
+ 'Could not set last exchange error: ' + error.message, {error}));
296
+ throw e;
264
297
  }
265
-
266
- // send response
267
- res.json(response);
268
298
  }
package/lib/verify.js CHANGED
@@ -4,9 +4,9 @@
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
8
  import {importJWK, jwtVerify} from 'jose';
8
9
  import {didIo} from '@bedrock/did-io';
9
- import {getZcapClient} from './helpers.js';
10
10
 
11
11
  const {util: {BedrockError}} = bedrock;
12
12
 
@@ -77,17 +77,17 @@ export async function verify({
77
77
  if(credentialResults) {
78
78
  credentialResults.forEach(result => {
79
79
  if(result.error) {
80
- result.error = _stripStacktrace(result.error);
80
+ result.error = stripStacktrace(result.error);
81
81
  }
82
82
  });
83
83
  }
84
84
  if(presentationResult.error) {
85
- presentationResult.error = _stripStacktrace(presentationResult.error);
85
+ presentationResult.error = stripStacktrace(presentationResult.error);
86
86
  }
87
87
 
88
88
  // generate useful error to return to client
89
89
  const {name, errors, message} = cause.data.error;
90
- const causeError = _stripStacktrace({...cause.data.error});
90
+ const causeError = stripStacktrace({...cause.data.error});
91
91
  delete causeError.errors;
92
92
  const error = new BedrockError(message ?? 'Verification error.', {
93
93
  name: (name === 'VerificationError' || name === 'DataError') ?
@@ -102,7 +102,7 @@ export async function verify({
102
102
  }
103
103
  });
104
104
  if(Array.isArray(errors)) {
105
- error.details.errors = errors.map(_stripStacktrace);
105
+ error.details.errors = errors.map(stripStacktrace);
106
106
  }
107
107
  throw error;
108
108
  }
@@ -159,6 +159,18 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
159
159
  }
160
160
 
161
161
  const vm = await didIo.get({url: kid});
162
+ if(!vm) {
163
+ throw new BedrockError(
164
+ `Verification method identified by "kid" (${kid}) could not be ` +
165
+ 'retrieved.', {
166
+ name: 'DataError',
167
+ details: {
168
+ public: true,
169
+ httpStatusCode: 400
170
+ }
171
+ });
172
+ }
173
+
162
174
  // `vm.controller` must be the issuer of the DID JWT; also ensure that
163
175
  // the specified controller authorized `vm` for the purpose of
164
176
  // authentication
@@ -174,8 +186,13 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
174
186
  match.controller === vm.controller)) {
175
187
  throw new BedrockError(
176
188
  `Verification method controller "${issuer}" did not authorize ` +
177
- `verification method "${vm.id}" for the purpose of "authentication".`,
178
- {name: 'NotAllowedError'});
189
+ `verification method "${vm.id}" for the purpose of "authentication".`, {
190
+ name: 'NotAllowedError',
191
+ details: {
192
+ public: true,
193
+ httpStatusCode: 400
194
+ }
195
+ });
179
196
  }
180
197
  let jwk;
181
198
  if(isEcdsa) {
@@ -251,15 +268,3 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
251
268
 
252
269
  return {verified: true, did: issuer, verifyResult};
253
270
  }
254
-
255
- function _stripStacktrace(error) {
256
- error = {...error};
257
- delete error.stack;
258
- if(error.errors) {
259
- error.errors = error.errors.map(_stripStacktrace);
260
- }
261
- if(error.cause) {
262
- error.cause = _stripStacktrace(error.cause);
263
- }
264
- return error;
265
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "5.2.0",
3
+ "version": "5.3.1",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -47,7 +47,8 @@
47
47
  "cors": "^2.8.5",
48
48
  "jose": "^5.6.3",
49
49
  "jsonata": "^2.0.5",
50
- "klona": "^2.0.6"
50
+ "klona": "^2.0.6",
51
+ "serialize-error": "^11.0.3"
51
52
  },
52
53
  "peerDependencies": {
53
54
  "@bedrock/app-identity": "4.0.0",