@bedrock/vc-delivery 3.4.0 → 3.5.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/helpers.js CHANGED
@@ -1,15 +1,24 @@
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 {decodeId, generateId} from 'bnid';
6
6
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
7
7
  import {httpsAgent} from '@bedrock/https-agent';
8
+ import jsonata from 'jsonata';
8
9
  import {serviceAgents} from '@bedrock/service-agent';
9
10
  import {ZcapClient} from '@digitalbazaar/ezcap';
10
11
 
11
12
  const {config} = bedrock;
12
13
 
14
+ export async function evaluateTemplate({exchange, typedTemplate} = {}) {
15
+ // run jsonata compiler; only `jsonata` template type is supported and this
16
+ // assumes only this template type will be passed in
17
+ const {template} = typedTemplate;
18
+ const {variables = {}} = exchange;
19
+ return jsonata(template).evaluate(variables, variables);
20
+ }
21
+
13
22
  export function getExchangerId({routePrefix, localId} = {}) {
14
23
  return `${config.server.baseUri}${routePrefix}/${localId}`;
15
24
  }
package/lib/http.js CHANGED
@@ -8,12 +8,12 @@ import {createChallenge as _createChallenge, verify} from './verify.js';
8
8
  import {
9
9
  createExchangeBody, useExchangeBody
10
10
  } from '../schemas/bedrock-vc-exchanger.js';
11
+ import {evaluateTemplate, generateRandom} from './helpers.js';
11
12
  import {exportJWK, generateKeyPair, importJWK} from 'jose';
12
13
  import {metering, middleware} from '@bedrock/service-core';
13
14
  import {asyncHandler} from '@bedrock/express';
14
15
  import bodyParser from 'body-parser';
15
16
  import cors from 'cors';
16
- import {generateRandom} from './helpers.js';
17
17
  import {issue} from './issue.js';
18
18
  import {klona} from 'klona';
19
19
  import {logger} from './logger.js';
@@ -31,6 +31,8 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
31
31
  }));
32
32
  });
33
33
 
34
+ const MAXIMUM_STEPS = 100;
35
+
34
36
  export async function addRoutes({app, service} = {}) {
35
37
  const {routePrefix} = service;
36
38
 
@@ -48,8 +50,15 @@ export async function addRoutes({app, service} = {}) {
48
50
  const {localId, exchangeId: id} = req.params;
49
51
  const {baseUri} = bedrock.config.server;
50
52
  const exchangerId = `${baseUri}${routePrefix}/${localId}`;
51
- // save promise in request, do not wait for it to settle
52
- req.exchange = exchanges.get({exchangerId, id});
53
+ // expose access to result via `req`; do not wait for it to settle here
54
+ const exchangePromise = exchanges.get({exchangerId, id}).catch(e => e);
55
+ req.getExchange = async () => {
56
+ const record = await exchangePromise;
57
+ if(record instanceof Error) {
58
+ throw record;
59
+ }
60
+ return record;
61
+ };
53
62
  next();
54
63
  });
55
64
 
@@ -141,7 +150,7 @@ export async function addRoutes({app, service} = {}) {
141
150
  getConfigMiddleware,
142
151
  middleware.authorizeServiceObjectRequest(),
143
152
  asyncHandler(async (req, res) => {
144
- const {exchange} = await req.exchange;
153
+ const {exchange} = await req.getExchange();
145
154
  // do not return any oauth2 credentials
146
155
  delete exchange.openId?.oauth2?.keyPair?.privateKeyJwk;
147
156
  res.json({exchange});
@@ -157,21 +166,49 @@ export async function addRoutes({app, service} = {}) {
157
166
  getConfigMiddleware,
158
167
  asyncHandler(async (req, res) => {
159
168
  const {config: exchanger} = req.serviceObject;
160
- const {exchange} = await req.exchange;
169
+ const {exchange} = await req.getExchange();
161
170
 
162
171
  // get any `verifiablePresentation` from the body...
163
172
  let receivedPresentation = req?.body?.verifiablePresentation;
164
173
 
165
174
  // process exchange step(s)
175
+ let i = 0;
166
176
  let currentStep = exchange.step;
167
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
+
168
185
  // no step present, break out to complete exchange
169
186
  if(!currentStep) {
170
187
  break;
171
188
  }
172
189
 
173
190
  // get current step details
174
- const step = exchanger.steps[currentStep];
191
+ let 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
+ {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
+ }
175
212
 
176
213
  // handle VPR: if step requires it, then `verifiablePresentation` must
177
214
  // be in the request
@@ -239,6 +276,16 @@ export async function addRoutes({app, service} = {}) {
239
276
  exchangerId: exchanger.id, exchange, expectedStep: currentStep
240
277
  });
241
278
  currentStep = exchange.step;
279
+
280
+ // FIXME: there may be VCs to issue during this step, do so before
281
+ // sending the VPR above
282
+ } else if(step.nextStep) {
283
+ // next steps without VPRs are prohibited
284
+ throw new BedrockError(
285
+ 'Invalid step detected; continuing exchanges must include VPRs.', {
286
+ name: 'DataError',
287
+ details: {httpStatusCode: 500, public: true}
288
+ });
242
289
  }
243
290
  }
244
291
 
@@ -250,11 +297,12 @@ export async function addRoutes({app, service} = {}) {
250
297
  // FIXME: decide what the best recovery path is if delivery fails (but no
251
298
  // replay attack detected) after exchange has been marked complete
252
299
 
253
- // issue VCs
254
- const {verifiablePresentation} = await issue({exchanger, exchange});
300
+ // issue any VCs; may return an empty result if the step defines no
301
+ // VCs to issue
302
+ const result = await issue({exchanger, exchange});
255
303
 
256
- // send VP
257
- res.json({verifiablePresentation});
304
+ // send result
305
+ res.json(result);
258
306
  }));
259
307
 
260
308
  // create OID4VCI routes to be used with each individual exchange
package/lib/issue.js CHANGED
@@ -1,26 +1,26 @@
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
+ import {evaluateTemplate, getZcapClient} from './helpers.js';
4
5
  import {createPresentation} from '@digitalbazaar/vc';
5
- import {getZcapClient} from './helpers.js';
6
- import jsonata from 'jsonata';
7
6
 
8
7
  export async function issue({exchanger, exchange} = {}) {
9
8
  // use any templates from exchanger and variables from exchange to produce
10
9
  // credentials to be issued; issue via the configured issuer instance
11
10
  const verifiableCredential = [];
12
11
  const {credentialTemplates = []} = exchanger;
13
- if(credentialTemplates) {
14
- const {variables = {}} = exchange;
15
- // run jsonata compiler; only `jsonata` template type is supported and this
16
- // was validated when the exchanger was created
17
- const credentials = await Promise.all(credentialTemplates.map(
18
- ({template: t}) => jsonata(t).evaluate(variables, variables)));
19
- // issue all VCs
20
- const vcs = await _issue({exchanger, credentials});
21
- verifiableCredential.push(...vcs);
12
+ if(!credentialTemplates || credentialTemplates.length === 0) {
13
+ // nothing to issue
14
+ return {};
22
15
  }
23
16
 
17
+ // evaluate template
18
+ const credentials = await Promise.all(credentialTemplates.map(
19
+ typedTemplate => evaluateTemplate({exchange, typedTemplate})));
20
+ // issue all VCs
21
+ const vcs = await _issue({exchanger, credentials});
22
+ verifiableCredential.push(...vcs);
23
+
24
24
  // generate VP to return VCs
25
25
  const verifiablePresentation = createPresentation();
26
26
  // FIXME: add any encrypted VCs to VP
package/lib/openId.js CHANGED
@@ -116,7 +116,7 @@ export async function createRoutes({
116
116
  cors(),
117
117
  getExchange,
118
118
  asyncHandler(async (req, res) => {
119
- const {exchange} = await req.exchange;
119
+ const {exchange} = await req.getExchange();
120
120
  if(!exchange.openId) {
121
121
  // FIXME: improve error
122
122
  // unsupported protocol for the exchange
@@ -138,7 +138,7 @@ export async function createRoutes({
138
138
  getConfigMiddleware,
139
139
  getExchange,
140
140
  asyncHandler(async (req, res) => {
141
- const exchangeRecord = await req.exchange;
141
+ const exchangeRecord = await req.getExchange();
142
142
  const {exchange} = exchangeRecord;
143
143
  if(!exchange.openId) {
144
144
  // FIXME: improve error
@@ -391,7 +391,7 @@ function _getAlgFromPrivateKey({privateKeyJwk}) {
391
391
 
392
392
  async function _processCredentialRequests({req, res, isBatchRequest}) {
393
393
  const {config: exchanger} = req.serviceObject;
394
- const exchangeRecord = await req.exchange;
394
+ const exchangeRecord = await req.getExchange();
395
395
  const {exchange} = exchangeRecord;
396
396
  if(!exchange.openId) {
397
397
  // FIXME: improve error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -109,8 +109,8 @@ export const createExchangeBody = {
109
109
  }
110
110
  };
111
111
 
112
- const credentialTemplate = {
113
- title: 'Credential Template',
112
+ const typedTemplate = {
113
+ title: 'Typed Template',
114
114
  type: 'object',
115
115
  required: ['type', 'template'],
116
116
  additionalProperties: false,
@@ -129,13 +129,33 @@ export const credentialTemplates = {
129
129
  title: 'Credential Templates',
130
130
  type: 'array',
131
131
  minItems: 1,
132
- items: credentialTemplate
132
+ items: typedTemplate
133
133
  };
134
134
 
135
135
  const step = {
136
136
  title: 'Exchange Step',
137
137
  type: 'object',
138
+ minProperties: 1,
138
139
  additionalProperties: false,
140
+ // step can either use a template so it will be generated using variables
141
+ // associated with the exchange, or static values can be provided
142
+ oneOf: [{
143
+ // `stepTemplate` must be present and nothing else
144
+ required: ['stepTemplate'],
145
+ not: {
146
+ required: [
147
+ 'createChallenge',
148
+ 'verifiablePresentationRequest',
149
+ 'jwtDidProofRequest',
150
+ 'nextStep'
151
+ ]
152
+ }
153
+ }, {
154
+ // anything except `stepTemplate` can be used
155
+ not: {
156
+ required: ['stepTemplate']
157
+ }
158
+ }],
139
159
  properties: {
140
160
  createChallenge: {
141
161
  type: 'boolean'
@@ -174,10 +194,8 @@ const step = {
174
194
  },
175
195
  nextStep: {
176
196
  type: 'string'
177
- }
178
- // FIXME: add jsonata template to convert VPR or
179
- // `jwtDidProofRequest` to more variables to be
180
- // used when issuing VCs
197
+ },
198
+ stepTemplate: typedTemplate
181
199
  }
182
200
  };
183
201