@bedrock/vc-delivery 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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
  }
@@ -251,15 +251,3 @@ export async function verifyDidProofJwt({workflow, exchange, jwt} = {}) {
251
251
 
252
252
  return {verified: true, did: issuer, verifyResult};
253
253
  }
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.1.0",
3
+ "version": "5.3.0",
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",