@bedrock/vc-delivery 7.11.1 → 7.11.3

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/vcapi.js CHANGED
@@ -3,17 +3,9 @@
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from './storage/exchanges.js';
6
- import {createChallenge as _createChallenge, verify} from './verify.js';
7
- import {
8
- buildPresentationFromResults,
9
- buildVerifyPresentationResults,
10
- emitExchangeUpdated,
11
- evaluateExchangeStep,
12
- generateRandom, validateVerifiablePresentation
13
- } from './helpers.js';
6
+ import {evaluateExchangeStep, generateRandom} from './helpers.js';
14
7
  import {exportJWK, generateKeyPair, importJWK} from 'jose';
15
- import {issue} from './issue.js';
16
- import {logger} from './logger.js';
8
+ import {ExchangeProcessor} from './ExchangeProcessor.js';
17
9
 
18
10
  // supported protocols
19
11
  import * as inviteRequest from './inviteRequest/inviteRequest.js';
@@ -22,7 +14,6 @@ import * as oid4vp from './oid4/oid4vp.js';
22
14
 
23
15
  const {util: {BedrockError}} = bedrock;
24
16
 
25
- const MAXIMUM_STEPS = 100;
26
17
  const FIFTEEN_MINUTES = 60 * 15;
27
18
 
28
19
  // 48 hours; make configurable
@@ -112,36 +103,13 @@ export async function getProtocols({req} = {}) {
112
103
  }
113
104
 
114
105
  export async function processExchange({req, res, workflow, exchangeRecord}) {
115
- const {exchange, meta} = exchangeRecord;
116
- let {updated: lastUpdated} = meta;
117
- let step;
106
+ // get any `verifiablePresentation` from the body...
107
+ const receivedPresentation = req?.body?.verifiablePresentation;
118
108
 
119
- try {
120
- // get any `verifiablePresentation` from the body...
121
- let receivedPresentation = req?.body?.verifiablePresentation;
122
-
123
- // process exchange step(s)
124
- let i = 0;
125
- let currentStep = exchange.step;
126
- while(true) {
127
- if(i++ > MAXIMUM_STEPS) {
128
- throw new BedrockError('Maximum steps exceeded.', {
129
- name: 'DataError',
130
- details: {httpStatusCode: 500, public: true}
131
- });
132
- }
133
-
134
- // no step present, break out to complete exchange
135
- if(!currentStep) {
136
- break;
137
- }
138
-
139
- // get current step details
140
- step = await evaluateExchangeStep({
141
- workflow, exchange, stepName: currentStep
142
- });
143
-
144
- // if step does not support VCAPI, throw
109
+ // use exchange processor to generate a response
110
+ const exchangeProcessor = new ExchangeProcessor({
111
+ workflow, exchangeRecord,
112
+ prepareStep({workflow, step}) {
145
113
  if(!_supportsVcApi({workflow, step})) {
146
114
  throw new BedrockError(
147
115
  'VC API protocol not supported by this exchange.', {
@@ -149,198 +117,15 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
149
117
  details: {httpStatusCode: 400, public: true}
150
118
  });
151
119
  }
152
-
153
- // if next step is the same as the current step, throw an error
154
- if(step.nextStep === currentStep) {
155
- throw new BedrockError('Cyclical step detected.', {
156
- name: 'DataError',
157
- details: {httpStatusCode: 500, public: true}
158
- });
159
- }
160
-
161
- // handle VPR: if step requires it, then `verifiablePresentation` must
162
- // be in the request
163
- if(step.verifiablePresentationRequest) {
164
- const {createChallenge} = step;
165
- const isInitialStep = exchange.step === workflow.initialStep;
166
-
167
- // if no presentation was received in the body...
168
- if(!receivedPresentation) {
169
- const verifiablePresentationRequest = structuredClone(
170
- step.verifiablePresentationRequest);
171
- if(createChallenge) {
172
- /* Note: When creating a challenge, the initial step always
173
- uses the local exchange ID because the initial step itself
174
- is one-time use. Subsequent steps, which only VC-API (as opposed
175
- to other protocols) supports creating additional challenges via
176
- the VC-API verifier API. */
177
- let challenge;
178
- if(isInitialStep) {
179
- challenge = exchange.id;
180
- } else {
181
- // generate a new challenge using verifier API
182
- ({challenge} = await _createChallenge({workflow}));
183
- }
184
- verifiablePresentationRequest.challenge = challenge;
185
- }
186
- // send VPR and return
187
- res.json({verifiablePresentationRequest});
188
- // if exchange is pending, mark it as active out-of-band
189
- if(exchange.state === 'pending') {
190
- exchange.state = 'active';
191
- exchange.sequence++;
192
- exchanges.update({workflowId: workflow.id, exchange}).catch(
193
- error => logger.error(
194
- 'Could not mark exchange active: ' + error.message, {error}));
195
- }
196
- return;
197
- }
198
-
199
- const {presentationSchema} = step;
200
-
201
- const isEnvelopedVP =
202
- receivedPresentation?.type === 'EnvelopedVerifiablePresentation';
203
-
204
- if(presentationSchema && !isEnvelopedVP) {
205
- validateVerifiablePresentation({
206
- schema: presentationSchema.jsonSchema,
207
- presentation: receivedPresentation
208
- });
209
- }
210
-
211
- // verify the received VP
212
- const expectedChallenge = isInitialStep ? exchange.id : undefined;
213
- const {
214
- allowUnprotectedPresentation = false,
215
- verifyPresentationOptions = {},
216
- verifyPresentationResultSchema
217
- } = step;
218
- const verifyResult = await verify({
219
- workflow,
220
- verifyPresentationOptions,
221
- verifyPresentationResultSchema,
222
- verifiablePresentationRequest: step.verifiablePresentationRequest,
223
- presentation: receivedPresentation,
224
- allowUnprotectedPresentation,
225
- expectedChallenge
226
- });
227
-
228
- // validate enveloped VP after verification
229
- if(presentationSchema && isEnvelopedVP) {
230
- validateVerifiablePresentation({
231
- schema: presentationSchema.jsonSchema,
232
- presentation: verifyResult?.presentationResult?.presentation ?? {}
233
- });
234
- }
235
-
236
- // store VP results in variables associated with current step
237
- if(!exchange.variables.results) {
238
- exchange.variables.results = {};
239
- }
240
- const {verificationMethod} = verifyResult;
241
- const result = {
242
- // common use case of DID Authentication; provide `did` for ease
243
- // of use in templates and consistency with OID4VCI which only
244
- // receives `did` not verification method nor VP
245
- did: verificationMethod?.controller || null,
246
- verificationMethod,
247
- verifiablePresentation: buildPresentationFromResults({
248
- presentation: receivedPresentation,
249
- verifyResult
250
- }),
251
- verifyPresentationResults: buildVerifyPresentationResults({
252
- verifyResult
253
- })
254
- };
255
- exchange.variables.results[currentStep] = result;
256
-
257
- // clear received presentation as it has been processed
258
- receivedPresentation = null;
259
-
260
- // if there is no next step, break out to complete exchange
261
- if(!step.nextStep) {
262
- break;
263
- }
264
-
265
- // FIXME: remove this once the other FIXME below is implemented
266
- // and provides support for issuance in non-last step
267
- if(step.verifiablePresentation || step.issueRequests?.length > 0) {
268
- throw new BedrockError(
269
- 'Invalid step detected; continuing exchanges currently must ' +
270
- 'only issue in the final step.', {
271
- name: 'DataError',
272
- details: {httpStatusCode: 500, public: true}
273
- });
274
- }
275
-
276
- // update the exchange to go to the next step, then loop to send
277
- // next VPR
278
- exchange.step = step.nextStep;
279
- // ensure exchange state is active
280
- if(exchange.state === 'pending') {
281
- exchange.state = 'active';
282
- }
283
- try {
284
- exchange.sequence++;
285
- await exchanges.update({workflowId: workflow.id, exchange});
286
- await emitExchangeUpdated({workflow, exchange, step});
287
- lastUpdated = Date.now();
288
- } catch(e) {
289
- exchange.sequence--;
290
- throw e;
291
- }
292
-
293
- // FIXME: there may be VCs to issue during this step, do so before
294
- // sending the VPR above and remove error that prevents continuing
295
- // exchanges that issue
296
- }
297
- currentStep = step.nextStep;
298
- }
299
-
300
- // issue any VCs that are to be stored in the exchange (`result` is set)
301
- await issue({workflow, exchange, step, filter: params => params.result});
302
-
303
- // mark exchange complete
304
- exchange.state = 'complete';
305
- try {
306
- exchange.sequence++;
307
- await exchanges.complete({workflowId: workflow.id, exchange});
308
- await emitExchangeUpdated({workflow, exchange, step});
309
- } catch(e) {
310
- exchange.sequence--;
311
- throw e;
312
- }
313
- lastUpdated = Date.now();
314
-
315
- // FIXME: decide what the best recovery path is if delivery fails (but no
316
- // replay attack detected) after exchange has been marked complete
317
-
318
- // issue any VCs; may return an empty response if the step defines no
319
- // VCs to issue
320
- const {response} = await issue({workflow, exchange, step});
321
-
322
- // if last `step` has a redirect URL, include it in the response
323
- if(step?.redirectUrl) {
324
- response.redirectUrl = step.redirectUrl;
120
+ },
121
+ inputRequired({step}) {
122
+ return step.verifiablePresentationRequest && !receivedPresentation;
325
123
  }
124
+ });
125
+ const response = await exchangeProcessor.process({receivedPresentation});
326
126
 
327
- // send response
328
- res.json(response);
329
- } catch(e) {
330
- if(e.name === 'InvalidStateError') {
331
- throw e;
332
- }
333
- // write last error if exchange hasn't been frequently updated
334
- const {id: workflowId} = workflow;
335
- const copy = {...exchange};
336
- copy.sequence++;
337
- copy.lastError = e;
338
- await exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
339
- .catch(error => logger.error(
340
- 'Could not set last exchange error: ' + error.message, {error}));
341
- await emitExchangeUpdated({workflow, exchange, step});
342
- throw e;
343
- }
127
+ // send response
128
+ res.json(response);
344
129
  }
345
130
 
346
131
  async function _initOAuth2({oauth2}) {
package/lib/verify.js CHANGED
@@ -31,27 +31,28 @@ export async function createChallenge({workflow} = {}) {
31
31
 
32
32
  export async function verify({
33
33
  workflow, verifyPresentationOptions, verifiablePresentationRequest,
34
- presentation, allowUnprotectedPresentation = false, expectedChallenge,
34
+ presentation, allowUnprotectedPresentation = false,
35
+ expectedChallenge, expectedDomain,
35
36
  verifyPresentationResultSchema
36
37
  } = {}) {
37
38
  // create zcap client for verifying
38
39
  const {zcapClient, zcaps} = await getZcapClient({workflow});
39
40
 
40
- // verify presentation
41
- const hasProof = presentation?.proof ||
41
+ // determine if presentation is secured or not
42
+ const isSecured = presentation?.proof ||
42
43
  presentation?.type === 'EnvelopedVerifiablePresentation';
43
44
 
44
- const checks = (!hasProof && allowUnprotectedPresentation) ?
45
- [] : ['proof'];
45
+ const checks = (!isSecured && allowUnprotectedPresentation) ? [] : ['proof'];
46
46
  if(!expectedChallenge) {
47
47
  // if no expected challenge, rely on verifier for challenge management
48
48
  checks.push('challenge');
49
49
  }
50
- const capability = zcaps.verifyPresentation;
51
- const domain = verifiablePresentationRequest.domain ??
52
- new URL(workflow.id).origin;
50
+
51
+ // verify presentation
53
52
  let result;
54
53
  try {
54
+ const domain = expectedDomain ??
55
+ verifiablePresentationRequest?.domain ?? new URL(workflow.id).origin;
55
56
  const options = createVerifyOptions({
56
57
  verifyPresentationOptions,
57
58
  expectedChallenge,
@@ -60,6 +61,7 @@ export async function verify({
60
61
  domain,
61
62
  checks
62
63
  });
64
+ const capability = zcaps.verifyPresentation;
63
65
  result = await zcapClient.write({
64
66
  capability,
65
67
  json: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "7.11.1",
3
+ "version": "7.11.3",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",