@bedrock/vc-delivery 4.3.0 → 4.4.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/config.js CHANGED
@@ -1,19 +1,31 @@
1
1
  /*!
2
- * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
 
6
+ const c = bedrock.util.config.main;
7
+ const cc = c.computer();
6
8
  const {config} = bedrock;
7
9
 
8
- // use `vc-exchanger` namespace
9
- const namespace = 'vc-exchanger';
10
+ // use `vc-workflow` namespace
11
+ const namespace = 'vc-workflow';
10
12
  config[namespace] = {};
11
13
 
12
- // create dev application identity for vc-exchanger (must be overridden in
14
+ // create dev application identity for vc-workflow (must be overridden in
13
15
  // deployments) ...and `ensureConfigOverride` has already been set via
14
16
  // `bedrock-app-identity` so it doesn't have to be set here
15
- config['app-identity'].seeds.services['vc-exchanger'] = {
17
+ config['app-identity'].seeds.services['vc-workflow'] = {
16
18
  id: 'did:key:z6MknmKKxYiYo6txxX2bCgzeuBDkPPb5SJ36p232XkVEk7mf',
17
19
  seedMultibase: 'z1Abgbd91bbZHPYakVA7EPvhY9NZ2EaTkEpmwdBCfifokDn',
20
+ serviceType: 'vc-workflow'
21
+ };
22
+
23
+ // backwards compatibility: `vc-exchanger` alias:
24
+ cc('vc-exchanger', () => config[namespace]);
25
+ config['app-identity'].seeds.services['vc-exchanger'] = {
18
26
  serviceType: 'vc-exchanger'
19
27
  };
28
+ cc('app-identity.seeds.services.vc-exchanger.id', () =>
29
+ config['app-identity'].seeds.services['vc-workflow'].id);
30
+ cc('app-identity.seeds.services.vc-exchanger.seedMultibase', () =>
31
+ config['app-identity'].seeds.services['vc-workflow'].seedMultibase);
package/lib/exchanges.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as database from '@bedrock/mongodb';
@@ -32,7 +32,20 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
32
32
  await database.openCollections([COLLECTION_NAME]);
33
33
 
34
34
  await database.createIndexes([{
35
- // cover exchange queries by exchanger ID + exchange ID
35
+ // cover exchange queries by local workflow ID + exchange ID
36
+ collection: COLLECTION_NAME,
37
+ fields: {localWorkflowId: 1, 'exchange.id': 1},
38
+ options: {
39
+ partialFilterExpression: {
40
+ localWorkflowId: {$exists: true}
41
+ },
42
+ unique: true, background: false
43
+ }
44
+ }, {
45
+ // backwards compatibility: cover exchange queries by
46
+ // local exchanger ID + exchange ID; local exchanger ID is the same as
47
+ // local workflow ID and this index can be eventually dropped once no
48
+ // deployments use `localExchangerId`
36
49
  collection: COLLECTION_NAME,
37
50
  fields: {localExchangerId: 1, 'exchange.id': 1},
38
51
  options: {unique: true, background: false}
@@ -55,14 +68,14 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
55
68
  * Inserts an exchange record.
56
69
  *
57
70
  * @param {object} options - The options to use.
58
- * @param {string} options.exchangerId - The ID of the exchanger that the
71
+ * @param {string} options.workflowId - The ID of the workflow that the
59
72
  * exchange is associated with.
60
73
  * @param {object} options.exchange - The exchange to insert.
61
74
  *
62
75
  * @returns {Promise<object>} Resolves to the database record.
63
76
  */
64
- export async function insert({exchangerId, exchange}) {
65
- assert.string(exchangerId, 'exchangerId');
77
+ export async function insert({workflowId, exchange}) {
78
+ assert.string(workflowId, 'workflowId');
66
79
  assert.object(exchange, 'exchange');
67
80
  assert.string(exchange.id, 'exchange.id');
68
81
  // optional time to live in seconds
@@ -79,11 +92,13 @@ export async function insert({exchangerId, exchange}) {
79
92
  // TTL is in seconds
80
93
  meta.expires = new Date(now + exchange.ttl * 1000);
81
94
  }
82
- const {localId: localExchangerId} = parseLocalId({id: exchangerId});
95
+ const {localId: localWorkflowId} = parseLocalId({id: workflowId});
83
96
  const record = {
84
- localExchangerId,
97
+ localWorkflowId,
98
+ // backwards compatibility: enable existing systems to find record
99
+ localExchangerId: localWorkflowId,
85
100
  meta,
86
- // possible states are: `pending`, `complete`, or `invalid`
101
+ // possible states are: `pending`, `active`, `complete`, or `invalid`
87
102
  exchange: {...exchange, sequence: 0, state: 'pending'}
88
103
  };
89
104
 
@@ -111,7 +126,7 @@ export async function insert({exchangerId, exchange}) {
111
126
  * Gets an exchange record.
112
127
  *
113
128
  * @param {object} options - The options to use.
114
- * @param {string} options.exchangerId - The ID of the exchanger that the
129
+ * @param {string} options.workflowId - The ID of the workflow that the
115
130
  * exchange is associated with.
116
131
  * @param {string} options.id - The ID of the exchange to retrieve.
117
132
  * @param {boolean} [options.explain=false] - An optional explain boolean.
@@ -119,18 +134,23 @@ export async function insert({exchangerId, exchange}) {
119
134
  * @returns {Promise<object | ExplainObject>} Resolves with the record that
120
135
  * matches the query or an ExplainObject if `explain=true`.
121
136
  */
122
- export async function get({exchangerId, id, explain = false} = {}) {
123
- assert.string(exchangerId, 'exchangerId');
137
+ export async function get({workflowId, id, explain = false} = {}) {
138
+ assert.string(workflowId, 'workflowId');
124
139
  assert.string(id, 'id');
125
140
 
126
- const {localId: localExchangerId} = parseLocalId({id: exchangerId});
141
+ const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
127
142
  const collection = database.collections[COLLECTION_NAME];
128
143
  const query = {
129
- localExchangerId,
144
+ localWorkflowId,
130
145
  'exchange.id': id,
131
146
  // treat exchange as not found if invalid
132
147
  'exchange.state': {$ne: 'invalid'}
133
148
  };
149
+ // backwards compatibility: query on `localExchangerId`
150
+ if(base.endsWith('/exchangers')) {
151
+ query.localWorkflowId = {$in: [null, localWorkflowId]};
152
+ query.localExchangerId = localWorkflowId;
153
+ }
134
154
  const projection = {_id: 0, exchange: 1, meta: 1};
135
155
 
136
156
  if(explain) {
@@ -145,7 +165,9 @@ export async function get({exchangerId, id, explain = false} = {}) {
145
165
  throw new BedrockError('Exchange not found.', {
146
166
  name: 'NotFoundError',
147
167
  details: {
148
- exchanger: exchangerId,
168
+ workflow: workflowId,
169
+ // backwards compatibility
170
+ exchanger: workflowId,
149
171
  exchange: id,
150
172
  httpStatusCode: 404,
151
173
  public: true
@@ -155,11 +177,18 @@ export async function get({exchangerId, id, explain = false} = {}) {
155
177
 
156
178
  // backwards compatibility; initialize `sequence`
157
179
  if(record.exchange.sequence === undefined) {
158
- await collection.updateOne({
159
- localExchangerId,
180
+ const query = {
181
+ localWorkflowId,
160
182
  'exchange.id': id,
161
183
  'exchange.sequence': null
162
- }, {$set: {'exchange.sequence': 0}});
184
+ };
185
+ // backwards compatibility: query on `localExchangerId`
186
+ if(base.endsWith('/exchangers')) {
187
+ query.localWorkflowId = {$in: [null, localWorkflowId]};
188
+ query.localExchangerId = localWorkflowId;
189
+ }
190
+
191
+ await collection.updateOne(query, {$set: {'exchange.sequence': 0}});
163
192
  record.exchange.sequence = 0;
164
193
  }
165
194
 
@@ -167,10 +196,11 @@ export async function get({exchangerId, id, explain = false} = {}) {
167
196
  }
168
197
 
169
198
  /**
170
- * Updates a pending exchange with new variables, step, and TTL information.
199
+ * Updates a pending exchange with new state, variables, step, and TTL
200
+ * information.
171
201
  *
172
202
  * @param {object} options - The options to use.
173
- * @param {string} options.exchangerId - The ID of the exchanger the exchange
203
+ * @param {string} options.workflowId - The ID of the workflow the exchange
174
204
  * is associated with.
175
205
  * @param {object} options.exchange - The exchange to update.
176
206
  * @param {boolean} [options.explain=false] - An optional explain boolean.
@@ -178,8 +208,8 @@ export async function get({exchangerId, id, explain = false} = {}) {
178
208
  * @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
179
209
  * success or an ExplainObject if `explain=true`.
180
210
  */
181
- export async function update({exchangerId, exchange, explain = false} = {}) {
182
- assert.string(exchangerId, 'exchangerId');
211
+ export async function update({workflowId, exchange, explain = false} = {}) {
212
+ assert.string(workflowId, 'workflowId');
183
213
  assert.object(exchange, 'exchange');
184
214
  const {id} = exchange;
185
215
 
@@ -187,7 +217,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
187
217
  const now = Date.now();
188
218
  const update = {
189
219
  $inc: {'exchange.sequence': 1},
190
- $set: {'meta.updated': now}
220
+ $set: {'exchange.state': exchange.state, 'meta.updated': now}
191
221
  };
192
222
  // update exchange `variables`, `step`, and `ttl`
193
223
  if(exchange.variables) {
@@ -200,17 +230,22 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
200
230
  update.$set['exchange.ttl'] = exchange.ttl;
201
231
  }
202
232
 
203
- const {localId: localExchangerId} = parseLocalId({id: exchangerId});
233
+ const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
204
234
 
205
235
  const collection = database.collections[COLLECTION_NAME];
206
236
  const query = {
207
- localExchangerId,
237
+ localWorkflowId,
208
238
  'exchange.id': id,
209
239
  // exchange sequence must match previous sequence
210
240
  'exchange.sequence': exchange.sequence - 1,
211
- // previous state must be `pending` in order to update it
212
- 'exchange.state': 'pending'
241
+ // previous state must be `pending` or `active` in order to update it
242
+ 'exchange.state': {$in: ['pending', 'active']}
213
243
  };
244
+ // backwards compatibility: query on `localExchangerId`
245
+ if(base.endsWith('/exchangers')) {
246
+ query.localWorkflowId = {$in: [null, localWorkflowId]};
247
+ query.localExchangerId = localWorkflowId;
248
+ }
214
249
 
215
250
  if(explain) {
216
251
  // 'find().limit(1)' is used here because 'updateOne()' doesn't return a
@@ -238,7 +273,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
238
273
 
239
274
  // if no document was matched, try to get an existing exchange; if the
240
275
  // exchange does not exist, a not found error will be automatically thrown
241
- await get({exchangerId, id});
276
+ await get({workflowId, id});
242
277
 
243
278
  /* Note: Here the exchange *does* exist, but the step or state did not
244
279
  match which is a conflict error. */
@@ -258,7 +293,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
258
293
  * Marks an exchange as complete.
259
294
  *
260
295
  * @param {object} options - The options to use.
261
- * @param {string} options.exchangerId - The ID of the exchanger the exchange
296
+ * @param {string} options.workflowId - The ID of the workflow the exchange
262
297
  * is associated with.
263
298
  * @param {object} options.exchange - The exchange to mark as complete.
264
299
  * @param {boolean} [options.explain=false] - An optional explain boolean.
@@ -266,8 +301,8 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
266
301
  * @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
267
302
  * success or an ExplainObject if `explain=true`.
268
303
  */
269
- export async function complete({exchangerId, exchange, explain = false} = {}) {
270
- assert.string(exchangerId, 'exchangerId');
304
+ export async function complete({workflowId, exchange, explain = false} = {}) {
305
+ assert.string(workflowId, 'workflowId');
271
306
  assert.object(exchange, 'exchange');
272
307
  const {id} = exchange;
273
308
 
@@ -291,17 +326,23 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
291
326
  update.$set['exchange.ttl'] = exchange.ttl;
292
327
  }
293
328
 
294
- const {localId: localExchangerId} = parseLocalId({id: exchangerId});
329
+ const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
295
330
 
296
331
  const collection = database.collections[COLLECTION_NAME];
297
332
  const query = {
298
- localExchangerId,
333
+ localWorkflowId,
299
334
  'exchange.id': id,
300
335
  // exchange sequence must match previous sequence
301
336
  'exchange.sequence': exchange.sequence - 1,
302
- // previous state must be `pending` in order to change to `complete`
303
- 'exchange.state': 'pending'
337
+ // previous state must be `pending` or `active` in order to change to
338
+ // `complete`
339
+ 'exchange.state': {$in: ['pending', 'active']}
304
340
  };
341
+ // backwards compatibility: query on `localExchangerId`
342
+ if(base.endsWith('/exchangers')) {
343
+ query.localWorkflowId = {$in: [null, localWorkflowId]};
344
+ query.localExchangerId = localWorkflowId;
345
+ }
305
346
 
306
347
  if(explain) {
307
348
  // 'find().limit(1)' is used here because 'updateOne()' doesn't return a
@@ -329,13 +370,14 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
329
370
 
330
371
  // if no document was matched, try to get an existing exchange; if the
331
372
  // exchange does not exist, a not found error will be automatically thrown
332
- const record = await get({exchangerId, id});
373
+ const record = await get({workflowId, id});
333
374
 
334
375
  /* Note: Here the exchange *does* exist, but couldn't be updated because
335
376
  another process changed it. That change either left it in a still pending
336
377
  state or it was already completed. If it was already completed, it is an
337
378
  error condition that must result in invalidating the exchange. */
338
- if(record.state === 'pending') {
379
+ if(record.exchange.state === 'pending' ||
380
+ record.exchange.state === 'active') {
339
381
  // exchange still pending, another process updated it
340
382
  throw new BedrockError('Could not update exchange; conflict error.', {
341
383
  name: 'InvalidStateError',
@@ -350,7 +392,7 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
350
392
  // state is either `complete` or `invalid`, so throw duplicate completed
351
393
  // exchange error and invalidate exchange if needed, but do not throw any
352
394
  // error to client; only log it
353
- if(record.state !== 'invalid') {
395
+ if(record.exchange.state !== 'invalid') {
354
396
  _invalidateExchange({record}).catch(
355
397
  error => logger.error(`Could not invalidate exchange "${id}".`, {error}));
356
398
  }
@@ -387,9 +429,13 @@ async function _markExchangeInvalid({record}) {
387
429
  // mark exchange invalid
388
430
  try {
389
431
  const query = {
390
- localExchangerId: record.localExchangerId,
432
+ localWorkflowId: record.localWorkflowId,
391
433
  'exchange.id': record.exchange.id
392
434
  };
435
+ // backwards compatibility: query on `localExchangerId`
436
+ if(!record.localWorkflowId) {
437
+ query.localExchangerId = record.localExchangerId;
438
+ }
393
439
  const update = {
394
440
  $set: {
395
441
  'exchange.state': 'invalid',
package/lib/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import {decodeId, generateId} from 'bnid';
@@ -12,7 +12,7 @@ import {ZcapClient} from '@digitalbazaar/ezcap';
12
12
  const {config} = bedrock;
13
13
 
14
14
  export async function evaluateTemplate({
15
- exchanger, exchange, typedTemplate
15
+ workflow, exchange, typedTemplate
16
16
  } = {}) {
17
17
  // run jsonata compiler; only `jsonata` template type is supported and this
18
18
  // assumes only this template type will be passed in
@@ -20,8 +20,12 @@ export async function evaluateTemplate({
20
20
  const {variables = {}} = exchange;
21
21
  // always include `globals` as keyword for self-referencing exchange info
22
22
  variables.globals = {
23
+ workflow: {
24
+ id: workflow.id
25
+ },
26
+ // backwards compatibility
23
27
  exchanger: {
24
- id: exchanger.id
28
+ id: workflow.id
25
29
  },
26
30
  exchange: {
27
31
  id: exchange.id
@@ -30,7 +34,7 @@ export async function evaluateTemplate({
30
34
  return jsonata(template).evaluate(variables, variables);
31
35
  }
32
36
 
33
- export function getExchangerId({routePrefix, localId} = {}) {
37
+ export function getWorkflowId({routePrefix, localId} = {}) {
34
38
  return `${config.server.baseUri}${routePrefix}/${localId}`;
35
39
  }
36
40
 
@@ -44,12 +48,15 @@ export async function generateRandom() {
44
48
  });
45
49
  }
46
50
 
47
- export async function getZcapClient({exchanger} = {}) {
51
+ export async function getZcapClient({workflow} = {}) {
48
52
  // get service agent for communicating with the issuer instance
49
- const {serviceAgent} = await serviceAgents.get(
50
- {serviceType: 'vc-exchanger'});
53
+ const {pathname} = new URL(workflow.id);
54
+ // backwards-compatibility: support deprecated `vc-exchanger`
55
+ const serviceType = pathname.startsWith('/workflows/') ?
56
+ 'vc-workflow' : 'vc-exchanger';
57
+ const {serviceAgent} = await serviceAgents.get({serviceType});
51
58
  const {capabilityAgent, zcaps} = await serviceAgents.getEphemeralAgent(
52
- {config: exchanger, serviceAgent});
59
+ {config: workflow, serviceAgent});
53
60
 
54
61
  // create zcap client for issuing VCs
55
62
  const zcapClient = new ZcapClient({
package/lib/http.js CHANGED
@@ -1,22 +1,20 @@
1
1
  /*!
2
- * Copyright (c) 2018-2023 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as _openId from './openId.js';
5
5
  import * as bedrock from '@bedrock/core';
6
6
  import * as exchanges from './exchanges.js';
7
- import {createChallenge as _createChallenge, verify} from './verify.js';
8
7
  import {
9
8
  createExchangeBody, useExchangeBody
10
- } from '../schemas/bedrock-vc-exchanger.js';
11
- import {evaluateTemplate, generateRandom, getExchangerId} from './helpers.js';
9
+ } from '../schemas/bedrock-vc-workflow.js';
12
10
  import {exportJWK, generateKeyPair, importJWK} from 'jose';
11
+ import {generateRandom, getWorkflowId} from './helpers.js';
13
12
  import {metering, middleware} from '@bedrock/service-core';
14
13
  import {asyncHandler} from '@bedrock/express';
15
14
  import bodyParser from 'body-parser';
16
15
  import cors from 'cors';
17
- import {issue} from './issue.js';
18
- import {klona} from 'klona';
19
16
  import {logger} from './logger.js';
17
+ import {processExchange} from './vcapi.js';
20
18
  import {createValidateMiddleware as validate} from '@bedrock/validation';
21
19
 
22
20
  const {util: {BedrockError}} = bedrock;
@@ -31,8 +29,6 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
31
29
  }));
32
30
  });
33
31
 
34
- const MAXIMUM_STEPS = 100;
35
-
36
32
  export async function addRoutes({app, service} = {}) {
37
33
  const {routePrefix} = service;
38
34
 
@@ -42,15 +38,15 @@ export async function addRoutes({app, service} = {}) {
42
38
  exchange: `${baseUrl}/exchanges/:exchangeId`
43
39
  };
44
40
 
45
- // used to retrieve service object (exchanger) config
41
+ // used to retrieve service object (workflow) config
46
42
  const getConfigMiddleware = middleware.createGetConfigMiddleware({service});
47
43
 
48
44
  // used to fetch exchange record in parallel
49
45
  const getExchange = asyncHandler(async (req, res, next) => {
50
46
  const {localId, exchangeId: id} = req.params;
51
- const exchangerId = getExchangerId({routePrefix, localId});
47
+ const workflowId = getWorkflowId({routePrefix, localId});
52
48
  // expose access to result via `req`; do not wait for it to settle here
53
- const exchangePromise = exchanges.get({exchangerId, id}).catch(e => e);
49
+ const exchangePromise = exchanges.get({workflowId, id}).catch(e => e);
54
50
  req.getExchange = async () => {
55
51
  const record = await exchangePromise;
56
52
  if(record instanceof Error) {
@@ -121,7 +117,7 @@ export async function addRoutes({app, service} = {}) {
121
117
  }
122
118
 
123
119
  // insert exchange
124
- const {id: exchangerId} = config;
120
+ const {id: workflowId} = config;
125
121
  const exchange = {
126
122
  id: await generateRandom(),
127
123
  ttl,
@@ -129,8 +125,8 @@ export async function addRoutes({app, service} = {}) {
129
125
  openId,
130
126
  step
131
127
  };
132
- await exchanges.insert({exchangerId, exchange});
133
- const location = `${exchangerId}/exchanges/${exchange.id}`;
128
+ await exchanges.insert({workflowId, exchange});
129
+ const location = `${workflowId}/exchanges/${exchange.id}`;
134
130
  res.status(204).location(location).send();
135
131
  } catch(error) {
136
132
  logger.error(error.message, {error});
@@ -164,147 +160,9 @@ export async function addRoutes({app, service} = {}) {
164
160
  getExchange,
165
161
  getConfigMiddleware,
166
162
  asyncHandler(async (req, res) => {
167
- const {config: exchanger} = req.serviceObject;
163
+ const {config: workflow} = req.serviceObject;
168
164
  const {exchange} = await req.getExchange();
169
-
170
- // get any `verifiablePresentation` from the body...
171
- let receivedPresentation = req?.body?.verifiablePresentation;
172
-
173
- // process exchange step(s)
174
- let i = 0;
175
- let currentStep = exchange.step;
176
- let step;
177
- while(true) {
178
- if(i++ > MAXIMUM_STEPS) {
179
- throw new BedrockError('Maximum steps exceeded.', {
180
- name: 'DataError',
181
- details: {httpStatusCode: 500, public: true}
182
- });
183
- }
184
-
185
- // no step present, break out to complete exchange
186
- if(!currentStep) {
187
- break;
188
- }
189
-
190
- // get current step details
191
- step = exchanger.steps[currentStep];
192
- if(step.stepTemplate) {
193
- // generate step from the template; assume the template type is
194
- // `jsonata` per the JSON schema
195
- step = await evaluateTemplate(
196
- {exchanger, exchange, typedTemplate: step.stepTemplate});
197
- if(Object.keys(step).length === 0) {
198
- throw new BedrockError('Empty step detected.', {
199
- name: 'DataError',
200
- details: {httpStatusCode: 500, public: true}
201
- });
202
- }
203
- }
204
-
205
- // if next step is the same as the current step, throw an error
206
- if(step.nextStep === currentStep) {
207
- throw new BedrockError('Cyclical step detected.', {
208
- name: 'DataError',
209
- details: {httpStatusCode: 500, public: true}
210
- });
211
- }
212
-
213
- // handle VPR: if step requires it, then `verifiablePresentation` must
214
- // be in the request
215
- if(step.verifiablePresentationRequest) {
216
- const {createChallenge} = step;
217
- const isInitialStep = exchange.step === exchanger.initialStep;
218
-
219
- // if no presentation was received in the body...
220
- if(!receivedPresentation) {
221
- const verifiablePresentationRequest = klona(
222
- step.verifiablePresentationRequest);
223
- if(createChallenge) {
224
- /* Note: When creating a challenge, the initial step always
225
- uses the local exchange ID because the initial step itself
226
- is one-time use. Subsequent steps, which only VC-API (as opposed
227
- to other protocols) supports creating additional challenges via
228
- the VC-API verifier API. */
229
- let challenge;
230
- if(isInitialStep) {
231
- challenge = exchange.id;
232
- } else {
233
- // generate a new challenge using verifier API
234
- ({challenge} = await _createChallenge({exchanger}));
235
- }
236
- verifiablePresentationRequest.challenge = challenge;
237
- }
238
- // send VPR and return
239
- res.json({verifiablePresentationRequest});
240
- return;
241
- }
242
-
243
- // verify the received VP
244
- const expectedChallenge = isInitialStep ? exchange.id : undefined;
245
- const {verificationMethod} = await verify({
246
- exchanger,
247
- verifiablePresentationRequest: step.verifiablePresentationRequest,
248
- presentation: receivedPresentation, expectedChallenge
249
- });
250
-
251
- // store VP results in variables associated with current step
252
- if(!exchange.variables.results) {
253
- exchange.variables.results = {};
254
- }
255
- exchange.variables.results[currentStep] = {
256
- // common use case of DID Authentication; provide `did` for ease
257
- // of use in templates and consistency with OID4VCI which only
258
- // receives `did` not verification method nor VP
259
- did: verificationMethod.controller,
260
- verificationMethod,
261
- verifiablePresentation: receivedPresentation
262
- };
263
-
264
- // clear received presentation as it has been processed
265
- receivedPresentation = null;
266
-
267
- // if there is no next step, break out to complete exchange
268
- if(!step.nextStep) {
269
- break;
270
- }
271
-
272
- // update the exchange to go to the next step, then loop to send
273
- // next VPR
274
- currentStep = exchange.step = step.nextStep;
275
- exchange.sequence++;
276
- await exchanges.update({exchangerId: exchanger.id, exchange});
277
-
278
- // FIXME: there may be VCs to issue during this step, do so before
279
- // sending the VPR above
280
- } else if(step.nextStep) {
281
- // next steps without VPRs are prohibited
282
- throw new BedrockError(
283
- 'Invalid step detected; continuing exchanges must include VPRs.', {
284
- name: 'DataError',
285
- details: {httpStatusCode: 500, public: true}
286
- });
287
- }
288
- }
289
-
290
- // mark exchange complete
291
- exchange.sequence++;
292
- await exchanges.complete({exchangerId: exchanger.id, exchange});
293
-
294
- // FIXME: decide what the best recovery path is if delivery fails (but no
295
- // replay attack detected) after exchange has been marked complete
296
-
297
- // issue any VCs; may return an empty result if the step defines no
298
- // VCs to issue
299
- const result = await issue({exchanger, exchange});
300
-
301
- // if last `step` has a redirect URL, include it in the response
302
- if(step?.redirectUrl) {
303
- result.redirectUrl = step.redirectUrl;
304
- }
305
-
306
- // send result
307
- res.json(result);
165
+ await processExchange({req, res, workflow, exchange});
308
166
  }));
309
167
 
310
168
  // create OID4VCI routes to be used with each individual exchange