@bedrock/vc-delivery 3.0.2 → 3.1.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/README.md CHANGED
@@ -1 +1 @@
1
- # bedrock-module-template-http
1
+ # bedrock-vc-delivery
package/lib/exchanges.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as database from '@bedrock/mongodb';
@@ -156,21 +156,123 @@ export async function get({exchangerId, id, explain = false} = {}) {
156
156
  return record;
157
157
  }
158
158
 
159
+ /**
160
+ * Updates a pending exchange with new variables, step, and TTL information.
161
+ *
162
+ * @param {object} options - The options to use.
163
+ * @param {string} options.exchangerId - The ID of the exchanger the exchange
164
+ * is associated with.
165
+ * @param {object} options.exchange - The exchange to update.
166
+ * @param {string} [options.expectedStep] - The expected current step in the
167
+ * exchange.
168
+ * @param {boolean} [options.explain=false] - An optional explain boolean.
169
+ *
170
+ * @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
171
+ * success or an ExplainObject if `explain=true`.
172
+ */
173
+ export async function update({
174
+ exchangerId, exchange, expectedStep, explain = false
175
+ } = {}) {
176
+ assert.string(exchangerId, 'exchangerId');
177
+ assert.object(exchange, 'exchange');
178
+ assert.optionalString(expectedStep, expectedStep);
179
+ const {id} = exchange;
180
+
181
+ // build update
182
+ const now = Date.now();
183
+ const update = {
184
+ $set: {'meta.updated': now}
185
+ };
186
+ // update exchange `variables.results[step]`, `step`, and `ttl`
187
+ if(exchange.variables?.results) {
188
+ update.$set['exchange.variables.results'] = exchange.variables.results;
189
+ }
190
+ if(exchange.step !== undefined) {
191
+ update.$set['exchange.step'] = exchange.step;
192
+ }
193
+ if(exchange.ttl !== undefined) {
194
+ update.$set['exchange.ttl'] = exchange.ttl;
195
+ }
196
+
197
+ const {localId: localExchangerId} = parseLocalId({id: exchangerId});
198
+
199
+ const collection = database.collections[COLLECTION_NAME];
200
+ const query = {
201
+ localExchangerId,
202
+ 'exchange.id': id,
203
+ // previous state must be `pending` in order to update it
204
+ 'exchange.state': 'pending'
205
+ };
206
+ // current step must match previous step
207
+ if(expectedStep === undefined) {
208
+ query['exchange.step'] = null;
209
+ } else {
210
+ query['exchange.step'] = expectedStep;
211
+ }
212
+
213
+ if(explain) {
214
+ // 'find().limit(1)' is used here because 'updateOne()' doesn't return a
215
+ // cursor which allows the use of the explain function.
216
+ const cursor = await collection.find(query).limit(1);
217
+ return cursor.explain('executionStats');
218
+ }
219
+
220
+ try {
221
+ const result = await collection.updateOne(query, update);
222
+ if(result.result.n > 0) {
223
+ // document modified: success
224
+ return true;
225
+ }
226
+ } catch(e) {
227
+ throw new BedrockError('Could not update exchange.', {
228
+ name: 'OperationError',
229
+ details: {
230
+ public: true,
231
+ httpStatusCode: 500
232
+ },
233
+ cause: e
234
+ });
235
+ }
236
+
237
+ // if no document was matched, try to get an existing exchange; if the
238
+ // exchange does not exist, a not found error will be automatically thrown
239
+ await get({exchangerId, id});
240
+
241
+ /* Note: Here the exchange *does* exist, but the step or state did not
242
+ match which is a conflict error. */
243
+
244
+ // throw duplicate completed exchange error
245
+ throw new BedrockError('Could not update exchange; conflict error.', {
246
+ name: 'InvalidStateError',
247
+ details: {
248
+ public: true,
249
+ // this is a client-side conflict error
250
+ httpStatusCode: 409
251
+ }
252
+ });
253
+ }
254
+
159
255
  /**
160
256
  * Marks an exchange as complete.
161
257
  *
162
258
  * @param {object} options - The options to use.
163
259
  * @param {string} options.exchangerId - The ID of the exchanger the exchange
164
260
  * is associated with.
165
- * @param {object} options.id - The ID of the exchange to mark as complete.
261
+ * @param {object} options.exchange - The exchange to mark as complete.
262
+ * @param {string} [options.expectedStep] - The expected current step in the
263
+ * exchange.
166
264
  * @param {boolean} [options.explain=false] - An optional explain boolean.
167
265
  *
168
266
  * @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
169
267
  * success or an ExplainObject if `explain=true`.
170
268
  */
171
- export async function complete({exchangerId, id, explain = false} = {}) {
269
+ export async function complete({
270
+ exchangerId, exchange, expectedStep, explain = false
271
+ } = {}) {
172
272
  assert.string(exchangerId, 'exchangerId');
173
- assert.string(id, 'id');
273
+ assert.object(exchange, 'exchange');
274
+ assert.optionalString(expectedStep, expectedStep);
275
+ const {id} = exchange;
174
276
 
175
277
  // build update
176
278
  const now = Date.now();
@@ -180,6 +282,16 @@ export async function complete({exchangerId, id, explain = false} = {}) {
180
282
  'meta.updated': now
181
283
  }
182
284
  };
285
+ // update exchange `variables.results[step]`, `step`, and `ttl`
286
+ if(exchange.variables?.results) {
287
+ update.$set['exchange.variables.results'] = exchange.variables.results;
288
+ }
289
+ if(exchange.step !== undefined) {
290
+ update.$set['exchange.step'] = exchange.step;
291
+ }
292
+ if(exchange.ttl !== undefined) {
293
+ update.$set['exchange.ttl'] = exchange.ttl;
294
+ }
183
295
 
184
296
  const {localId: localExchangerId} = parseLocalId({id: exchangerId});
185
297
 
@@ -190,6 +302,12 @@ export async function complete({exchangerId, id, explain = false} = {}) {
190
302
  // previous state must be `pending` in order to change to `complete`
191
303
  'exchange.state': 'pending'
192
304
  };
305
+ // current step must match previous step
306
+ if(expectedStep === undefined) {
307
+ query['exchange.step'] = null;
308
+ } else {
309
+ query['exchange.step'] = expectedStep;
310
+ }
193
311
 
194
312
  if(explain) {
195
313
  // 'find().limit(1)' is used here because 'updateOne()' doesn't return a
package/lib/http.js CHANGED
@@ -133,7 +133,21 @@ export async function addRoutes({app, service} = {}) {
133
133
  metering.reportOperationUsage({req});
134
134
  }));
135
135
 
136
- // VC-API exchange endpoint
136
+ // VC-API get exchange endpoint
137
+ app.get(
138
+ routes.exchange,
139
+ cors(),
140
+ getExchange,
141
+ getConfigMiddleware,
142
+ middleware.authorizeServiceObjectRequest(),
143
+ asyncHandler(async (req, res) => {
144
+ const {exchange} = await req.exchange;
145
+ // do not return any oauth2 credentials
146
+ delete exchange.openId?.oauth2?.keyPair?.privateKeyJwk;
147
+ res.json({exchange});
148
+ }));
149
+
150
+ // VC-API use exchange endpoint
137
151
  app.options(routes.exchange, cors());
138
152
  app.post(
139
153
  routes.exchange,
@@ -145,19 +159,28 @@ export async function addRoutes({app, service} = {}) {
145
159
  const {config: exchanger} = req.serviceObject;
146
160
  const {exchange} = await req.exchange;
147
161
 
148
- // process exchange step if present
149
- if(exchange.step) {
150
- const step = exchanger.steps[exchange.step];
162
+ // get any `verifiablePresentation` from the body...
163
+ let receivedPresentation = req?.body?.verifiablePresentation;
151
164
 
152
- // handle VPR; if step requires it, then `verifiablePresentation` must
165
+ // process exchange step(s)
166
+ let currentStep = exchange.step;
167
+ while(true) {
168
+ // no step present, break out to complete exchange
169
+ if(!currentStep) {
170
+ break;
171
+ }
172
+
173
+ // get current step details
174
+ const step = exchanger.steps[currentStep];
175
+
176
+ // handle VPR: if step requires it, then `verifiablePresentation` must
153
177
  // be in the request
154
178
  if(step.verifiablePresentationRequest) {
155
179
  const {createChallenge} = step;
156
180
  const isInitialStep = exchange.step === exchanger.initialStep;
157
181
 
158
- // if `verifiablePresentation` is not in the body...
159
- const presentation = req?.body?.verifiablePresentation;
160
- if(!presentation) {
182
+ // if no presentation was received in the body...
183
+ if(!receivedPresentation) {
161
184
  const verifiablePresentationRequest = klona(
162
185
  step.verifiablePresentationRequest);
163
186
  if(createChallenge) {
@@ -175,38 +198,54 @@ export async function addRoutes({app, service} = {}) {
175
198
  }
176
199
  verifiablePresentationRequest.challenge = challenge;
177
200
  }
178
- // send VPR
201
+ // send VPR and return
179
202
  res.json({verifiablePresentationRequest});
180
203
  return;
181
204
  }
182
205
 
183
- // verify the VP
206
+ // verify the received VP
184
207
  const expectedChallenge = isInitialStep ? exchange.id : undefined;
185
- const {verificationMethod} = await verify(
186
- {exchanger, presentation, expectedChallenge});
208
+ const {verificationMethod} = await verify({
209
+ exchanger, presentation: receivedPresentation, expectedChallenge
210
+ });
187
211
 
188
- // FIXME: ensure VP satisfies step VPR; implement templates (jsonata)
189
- // to convert VPR responses to more exchange variables
212
+ // FIXME: ensure VP satisfies step VPR
190
213
 
191
- // store VP in variables
192
- exchange.variables[exchange.step] = {
214
+ // store VP results in variables associated with current step
215
+ if(!exchange.variables.results) {
216
+ exchange.variables.results = {};
217
+ }
218
+ exchange.variables.results[currentStep] = {
219
+ // common use case of DID Authentication; provide `did` for ease
220
+ // of use in templates and consistency with OID4VCI which only
221
+ // receives `did` not verification method nor VP
193
222
  did: verificationMethod.controller,
194
- verifiablePresentation: presentation
223
+ verificationMethod,
224
+ verifiablePresentation: receivedPresentation
195
225
  };
196
226
 
197
- // FIXME: update the exchange to go to the next step if there is one
198
- // if(step.nextStep) {
199
- // exchange.step = step.nextStep;
200
- // // FIXME: break exchange step processor into its own local API;
201
- // // ensure VPR has been met, store VP in variables, and
202
- // // loop to send next VPR
203
- // return;
204
- // }
227
+ // clear received presentation as it has been processed
228
+ receivedPresentation = null;
229
+
230
+ // if there is no next step, break out to complete exchange
231
+ if(!step.nextStep) {
232
+ break;
233
+ }
234
+
235
+ // update the exchange to go to the next step, then loop to send
236
+ // next VPR
237
+ exchange.step = step.nextStep;
238
+ await exchanges.update({
239
+ exchangerId: exchanger.id, exchange, expectedStep: currentStep
240
+ });
241
+ currentStep = exchange.step;
205
242
  }
206
243
  }
207
244
 
208
245
  // mark exchange complete
209
- await exchanges.complete({exchangerId: exchanger.id, id: exchange.id});
246
+ await exchanges.complete({
247
+ exchangerId: exchanger.id, exchange, expectedStep: currentStep
248
+ });
210
249
 
211
250
  // FIXME: decide what the best recovery path is if delivery fails (but no
212
251
  // replay attack detected) after exchange has been marked complete
package/lib/index.js CHANGED
@@ -58,7 +58,9 @@ bedrock.events.on('bedrock.init', async () => {
58
58
  required: false
59
59
  }]
60
60
  },
61
- usageAggregator
61
+ async usageAggregator({meter, signal} = {}) {
62
+ return usageAggregator({meter, signal, service});
63
+ }
62
64
  });
63
65
 
64
66
  bedrock.events.on('bedrock-express.configure.routes', async app => {
@@ -100,8 +102,8 @@ async function validateConfigFn({config} = {}) {
100
102
  }
101
103
 
102
104
  // if `steps` are specified, then `initialStep` MUST be included
103
- const {steps = [], initialStep} = config;
104
- if(steps.length > 0 && initialStep === undefined) {
105
+ const {steps, initialStep} = config;
106
+ if(steps && initialStep === undefined) {
105
107
  throw new BedrockError(
106
108
  '"initialStep" is required when "steps" are provided.', {
107
109
  name: 'DataError',
package/lib/openId.js CHANGED
@@ -394,7 +394,8 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
394
394
  {credentialRequests, expectedCredentialRequests});
395
395
 
396
396
  // process exchange step if present
397
- if(exchange.step) {
397
+ const currentStep = exchange.step;
398
+ if(currentStep) {
398
399
  const step = exchanger.steps[exchange.step];
399
400
 
400
401
  // handle JWT DID Proof request; if step requires it, then `proof` must
@@ -418,13 +419,22 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
418
419
  // FIXME: improve error
419
420
  throw new Error('every DID must be the same');
420
421
  }
421
- // add `did` to exchange variables
422
- exchange.variables[exchange.step] = {did};
422
+ // store did results in variables associated with current step
423
+ if(!exchange.variables.results) {
424
+ exchange.variables.results = {};
425
+ }
426
+ exchange.variables.results[currentStep] = {
427
+ // common use case of DID Authentication; provide `did` for ease
428
+ // of use in templates
429
+ did
430
+ };
423
431
  }
424
432
  }
425
433
 
426
434
  // mark exchange complete
427
- await exchanges.complete({exchangerId: exchanger.id, id: exchange.id});
435
+ await exchanges.complete({
436
+ exchangerId: exchanger.id, exchange, expectedStep: currentStep
437
+ });
428
438
 
429
439
  // FIXME: decide what the best recovery path is if delivery fails (but no
430
440
  // replay attack detected) after exchange has been marked complete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -56,7 +56,7 @@
56
56
  "@bedrock/mongodb": "^10.0.0",
57
57
  "@bedrock/oauth2-verifier": "^1.0.0",
58
58
  "@bedrock/service-agent": "^7.0.0",
59
- "@bedrock/service-core": "^8.0.0",
59
+ "@bedrock/service-core": "^8.0.2",
60
60
  "@bedrock/validation": "^7.1.0"
61
61
  },
62
62
  "directories": {
@@ -172,13 +172,12 @@ const step = {
172
172
  }
173
173
  }
174
174
  },
175
+ nextStep: {
176
+ type: 'string'
177
+ }
175
178
  // FIXME: add jsonata template to convert VPR or
176
179
  // `jwtDidProofRequest` to more variables to be
177
180
  // used when issuing VCs
178
- // FIXME: `nextStep` feature not yet implemented
179
- // nextStep: {
180
- // type: 'string'
181
- // }
182
181
  }
183
182
  };
184
183