@bedrock/vc-delivery 6.2.0 → 6.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/helpers.js CHANGED
@@ -19,11 +19,16 @@ const ALLOWED_ERROR_KEYS = [
19
19
  ];
20
20
 
21
21
  export async function evaluateTemplate({
22
- workflow, exchange, typedTemplate
22
+ workflow, exchange, typedTemplate, variables
23
23
  } = {}) {
24
24
  // run jsonata compiler; only `jsonata` template type is supported and this
25
25
  // assumes only this template type will be passed in
26
26
  const {template} = typedTemplate;
27
+ variables = variables ?? getTemplateVariables({workflow, exchange});
28
+ return jsonata(template).evaluate(variables, variables);
29
+ }
30
+
31
+ export function getTemplateVariables({workflow, exchange} = {}) {
27
32
  const {variables = {}} = exchange;
28
33
  // always include `globals` as keyword for self-referencing exchange info
29
34
  variables.globals = {
@@ -38,7 +43,7 @@ export async function evaluateTemplate({
38
43
  id: exchange.id
39
44
  }
40
45
  };
41
- return jsonata(template).evaluate(variables, variables);
46
+ return variables;
42
47
  }
43
48
 
44
49
  export function getWorkflowId({routePrefix, localId} = {}) {
@@ -207,6 +212,33 @@ export async function unenvelopePresentation({
207
212
  return {presentation, ...result};
208
213
  }
209
214
 
215
+ export async function validateStep({step} = {}) {
216
+ // FIXME: use `ajv` and do JSON schema check
217
+ if(Object.keys(step).length === 0) {
218
+ throw new BedrockError('Empty exchange step detected.', {
219
+ name: 'DataError',
220
+ details: {httpStatusCode: 500, public: true}
221
+ });
222
+ }
223
+ if(step.issueRequests !== undefined && !Array.isArray(step.issueRequests)) {
224
+ throw new BedrockError(
225
+ 'Invalid "issueRequests" in step.', {
226
+ name: 'DataError',
227
+ details: {httpStatusCode: 500, public: true}
228
+ });
229
+ }
230
+ // use of `jwtDidProofRequest` and `openId` together is prohibited
231
+ const {jwtDidProofRequest, openId} = step;
232
+ if(jwtDidProofRequest && openId) {
233
+ throw new BedrockError(
234
+ 'Invalid workflow configuration; only one of ' +
235
+ '"jwtDidProofRequest" and "openId" is permitted in a step.', {
236
+ name: 'DataError',
237
+ details: {httpStatusCode: 500, public: true}
238
+ });
239
+ }
240
+ }
241
+
210
242
  function _getEnvelope({envelope, format}) {
211
243
  const isString = typeof envelope === 'string';
212
244
  if(isString) {
package/lib/issue.js CHANGED
@@ -1,43 +1,115 @@
1
1
  /*!
2
2
  * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
+ import * as bedrock from '@bedrock/core';
4
5
  import {
5
6
  evaluateTemplate,
7
+ getTemplateVariables,
6
8
  getWorkflowIssuerInstances,
7
9
  getZcapClient
8
10
  } from './helpers.js';
9
11
  import {createPresentation} from '@digitalbazaar/vc';
10
12
 
13
+ const {util: {BedrockError}} = bedrock;
14
+
11
15
  export async function issue({
12
- workflow, exchange, format = 'application/vc'
16
+ workflow, exchange, step, format = 'application/vc'
13
17
  } = {}) {
14
18
  // use any templates from workflow and variables from exchange to produce
15
19
  // credentials to be issued; issue via the configured issuer instance
16
- const verifiableCredential = [];
17
20
  const {credentialTemplates = []} = workflow;
18
21
  if(!credentialTemplates || credentialTemplates.length === 0) {
19
22
  // nothing to issue
20
23
  return {response: {}};
21
24
  }
22
25
 
23
- // evaluate template
24
- const issueRequests = await Promise.all(credentialTemplates.map(
25
- typedTemplate => evaluateTemplate({workflow, exchange, typedTemplate})));
26
+ // generate all issue requests for current step in exchange
27
+ const issueRequests = await _createIssueRequests({
28
+ workflow, exchange, step, credentialTemplates
29
+ });
26
30
  // issue all VCs
27
31
  const vcs = await _issue({workflow, issueRequests, format});
28
- verifiableCredential.push(...vcs);
29
32
 
30
33
  // generate VP to return VCs
31
34
  const verifiablePresentation = createPresentation();
32
35
  // FIXME: add any encrypted VCs to VP
33
36
 
34
37
  // add any issued VCs to VP
35
- if(verifiableCredential.length > 0) {
36
- verifiablePresentation.verifiableCredential = verifiableCredential;
38
+ if(vcs.length > 0) {
39
+ verifiablePresentation.verifiableCredential = vcs;
37
40
  }
38
41
  return {response: {verifiablePresentation}, format};
39
42
  }
40
43
 
44
+ async function _createIssueRequests({
45
+ workflow, exchange, step, credentialTemplates
46
+ }) {
47
+ // if step does not define `issueRequests`, then use all for templates for
48
+ // backwards compatibility
49
+ let params;
50
+ if(!step?.issueRequests) {
51
+ params = credentialTemplates.map(typedTemplate => ({typedTemplate}));
52
+ } else {
53
+ // resolve all issue requests params in parallel
54
+ const variables = getTemplateVariables({workflow, exchange});
55
+ params = await Promise.all(step.issueRequests.map(async r => {
56
+ // find the typed template to use
57
+ let typedTemplate;
58
+ if(r.credentialTemplateIndex !== undefined) {
59
+ typedTemplate = credentialTemplates[r.credentialTemplateIndex];
60
+ } else if(r.credentialTemplateId !== undefined) {
61
+ typedTemplate = credentialTemplates.find(
62
+ t => t.id === r.credentialTemplateId);
63
+ }
64
+ if(typedTemplate === undefined) {
65
+ throw new BedrockError(
66
+ 'Credential template ' +
67
+ `"${r.credentialTemplateIndex ?? r.credentialTemplateId}" ` +
68
+ 'not found.', {
69
+ name: 'DataError',
70
+ details: {httpStatusCode: 500, public: true}
71
+ });
72
+ }
73
+
74
+ // allow different variables to be specified for the typed template
75
+ let vars = variables;
76
+ if(r.variables !== undefined) {
77
+ if(typeof r.variables === 'string') {
78
+ vars = variables[r.variables];
79
+ } else {
80
+ vars = r.variables;
81
+ }
82
+ if(!(vars && typeof vars === 'object')) {
83
+ throw new BedrockError(
84
+ `Issue request variables "${r.variables}" not found or invalid.`, {
85
+ name: 'DataError',
86
+ details: {httpStatusCode: 500, public: true}
87
+ });
88
+ }
89
+ }
90
+ return {
91
+ typedTemplate,
92
+ variables: {
93
+ // always include globals but allow local override
94
+ globals: variables.globals,
95
+ ...vars
96
+ }
97
+ };
98
+ }));
99
+ }
100
+
101
+ // evaluate all issue requests
102
+ return Promise.all(params.map(({typedTemplate, variables}) =>
103
+ evaluateTemplate({workflow, exchange, typedTemplate, variables})));
104
+ }
105
+
106
+ function _getIssueZcap({workflow, zcaps, format}) {
107
+ const issuerInstances = getWorkflowIssuerInstances({workflow});
108
+ const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find(
109
+ ({supportedFormats}) => supportedFormats.includes(format));
110
+ return zcaps[issueRefId];
111
+ }
112
+
41
113
  async function _issue({workflow, issueRequests, format} = {}) {
42
114
  // create zcap client for issuing VCs
43
115
  const {zcapClient, zcaps} = await getZcapClient({workflow});
@@ -53,10 +125,8 @@ async function _issue({workflow, issueRequests, format} = {}) {
53
125
  '/issue' : '/credentials/issue';
54
126
  }
55
127
 
56
- const issuedVCs = [];
57
-
58
128
  // issue VCs in parallel
59
- await Promise.all(issueRequests.map(async issueRequest => {
129
+ return Promise.all(issueRequests.map(async issueRequest => {
60
130
  /* Note: Issue request formats can be any one of these:
61
131
 
62
132
  1. `{credential, options?}`
@@ -69,15 +139,6 @@ async function _issue({workflow, issueRequests, format} = {}) {
69
139
  const {
70
140
  data: {verifiableCredential}
71
141
  } = await zcapClient.write({url, capability, json});
72
- issuedVCs.push(verifiableCredential);
142
+ return verifiableCredential;
73
143
  }));
74
-
75
- return issuedVCs;
76
- }
77
-
78
- function _getIssueZcap({workflow, zcaps, format}) {
79
- const issuerInstances = getWorkflowIssuerInstances({workflow});
80
- const {zcapReferenceIds: {issue: issueRefId}} = issuerInstances.find(
81
- ({supportedFormats}) => supportedFormats.includes(format));
82
- return zcaps[issueRefId];
83
144
  }
@@ -4,7 +4,7 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from '../exchanges.js';
6
6
  import {
7
- deepEqual, evaluateTemplate, getWorkflowIssuerInstances
7
+ deepEqual, evaluateTemplate, getWorkflowIssuerInstances, validateStep
8
8
  } from '../helpers.js';
9
9
  import {importJWK, SignJWT} from 'jose';
10
10
  import {checkAccessToken} from '@bedrock/oauth2-verifier';
@@ -427,33 +427,18 @@ async function _processExchange({
427
427
  });
428
428
 
429
429
  // process exchange step if present
430
+ let step;
430
431
  const currentStep = exchange.step;
431
432
  if(currentStep) {
432
- let step = workflow.steps[exchange.step];
433
+ step = workflow.steps[exchange.step];
433
434
  if(step.stepTemplate) {
434
435
  // generate step from the template; assume the template type is
435
436
  // `jsonata` per the JSON schema
436
437
  step = await evaluateTemplate(
437
438
  {workflow, exchange, typedTemplate: step.stepTemplate});
438
- if(Object.keys(step).length === 0) {
439
- throw new BedrockError('Could not create exchange step.', {
440
- name: 'DataError',
441
- details: {httpStatusCode: 500, public: true}
442
- });
443
- }
444
- }
445
-
446
- // do late workflow configuration validation
447
- const {jwtDidProofRequest, openId} = step;
448
- // use of `jwtDidProofRequest` and `openId` together is prohibited
449
- if(jwtDidProofRequest && openId) {
450
- throw new BedrockError(
451
- 'Invalid workflow configuration; only one of ' +
452
- '"jwtDidProofRequest" and "openId" is permitted in a step.', {
453
- name: 'DataError',
454
- details: {httpStatusCode: 500, public: true}
455
- });
456
439
  }
440
+ await validateStep({step});
441
+ const {jwtDidProofRequest} = step;
457
442
 
458
443
  // check to see if step supports OID4VP during OID4VCI
459
444
  if(step.openId) {
@@ -518,7 +503,7 @@ async function _processExchange({
518
503
  // replay attack detected) after exchange has been marked complete
519
504
 
520
505
  // issue VCs
521
- return issue({workflow, exchange, format});
506
+ return issue({workflow, exchange, step, format});
522
507
  } catch(e) {
523
508
  if(e.name === 'InvalidStateError') {
524
509
  throw e;
@@ -3,7 +3,9 @@
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from '../exchanges.js';
6
- import {evaluateTemplate, unenvelopePresentation} from '../helpers.js';
6
+ import {
7
+ evaluateTemplate, unenvelopePresentation, validateStep
8
+ } from '../helpers.js';
7
9
  import {
8
10
  presentationSubmission as presentationSubmissionSchema,
9
11
  verifiablePresentation as verifiablePresentationSchema
@@ -50,13 +52,8 @@ export async function getAuthorizationRequest({req}) {
50
52
  // `jsonata` per the JSON schema
51
53
  step = await evaluateTemplate(
52
54
  {workflow, exchange, typedTemplate: step.stepTemplate});
53
- if(Object.keys(step).length === 0) {
54
- throw new BedrockError('Could not create authorization request.', {
55
- name: 'DataError',
56
- details: {httpStatusCode: 500, public: true}
57
- });
58
- }
59
55
  }
56
+ await validateStep({step});
60
57
 
61
58
  // step must have `openId` to perform OID4VP
62
59
  if(!step.openId) {
package/lib/vcapi.js CHANGED
@@ -5,7 +5,7 @@ import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from './exchanges.js';
6
6
  import {createChallenge as _createChallenge, verify} from './verify.js';
7
7
  import {
8
- evaluateTemplate, generateRandom, unenvelopePresentation
8
+ evaluateTemplate, generateRandom, unenvelopePresentation, validateStep
9
9
  } from './helpers.js';
10
10
  import {exportJWK, generateKeyPair, importJWK} from 'jose';
11
11
  import {compile} from '@bedrock/validation';
@@ -121,13 +121,8 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
121
121
  // `jsonata` per the JSON schema
122
122
  step = await evaluateTemplate(
123
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
124
  }
125
+ await validateStep({step});
131
126
 
132
127
  // if next step is the same as the current step, throw an error
133
128
  if(step.nextStep === currentStep) {
@@ -272,7 +267,7 @@ export async function processExchange({req, res, workflow, exchangeRecord}) {
272
267
 
273
268
  // issue any VCs; may return an empty response if the step defines no
274
269
  // VCs to issue
275
- const {response} = await issue({workflow, exchange});
270
+ const {response} = await issue({workflow, exchange, step});
276
271
 
277
272
  // if last `step` has a redirect URL, include it in the response
278
273
  if(step?.redirectUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -330,6 +330,29 @@ export const issuerInstances = {
330
330
  items: issuerInstance
331
331
  };
332
332
 
333
+ const issueRequestParameters = {
334
+ title: 'Issue Request Parameters',
335
+ type: 'object',
336
+ oneOf: [{
337
+ required: ['credentialTemplateId']
338
+ }, {
339
+ required: ['credentialTemplateIndex']
340
+ }],
341
+ additionalProperties: false,
342
+ properties: {
343
+ credentialTemplateId: {
344
+ type: 'string'
345
+ },
346
+ credentialTemplateIndex: {
347
+ type: 'number'
348
+ },
349
+ // optionally specify different variables
350
+ variables: {
351
+ oneOf: [{type: 'string'}, {type: 'object'}]
352
+ }
353
+ }
354
+ };
355
+
333
356
  const step = {
334
357
  title: 'Exchange Step',
335
358
  type: 'object',
@@ -344,10 +367,12 @@ const step = {
344
367
  required: [
345
368
  'allowUnprotectedPresentation',
346
369
  'createChallenge',
347
- 'verifiablePresentationRequest',
370
+ 'issueRequests',
348
371
  'jwtDidProofRequest',
349
372
  'nextStep',
350
- 'openId'
373
+ 'openId',
374
+ 'presentationSchema',
375
+ 'verifiablePresentationRequest'
351
376
  ]
352
377
  }
353
378
  }, {
@@ -363,21 +388,10 @@ const step = {
363
388
  createChallenge: {
364
389
  type: 'boolean'
365
390
  },
366
- verifiablePresentationRequest: {
367
- type: 'object'
368
- },
369
- presentationSchema: {
370
- type: 'object',
371
- required: ['type', 'jsonSchema'],
372
- additionalProperties: false,
373
- properties: {
374
- type: {
375
- type: 'string'
376
- },
377
- jsonSchema: {
378
- type: 'object'
379
- }
380
- }
391
+ issueRequests: {
392
+ type: 'array',
393
+ minItems: 0,
394
+ items: issueRequestParameters
381
395
  },
382
396
  jwtDidProofRequest: {
383
397
  type: 'object',
@@ -411,7 +425,6 @@ const step = {
411
425
  nextStep: {
412
426
  type: 'string'
413
427
  },
414
- stepTemplate: typedTemplate,
415
428
  // required to support OID4VP (but can be provided by step template instead)
416
429
  openId: {
417
430
  type: 'object',
@@ -441,6 +454,23 @@ const step = {
441
454
  type: 'object'
442
455
  }
443
456
  }
457
+ },
458
+ presentationSchema: {
459
+ type: 'object',
460
+ required: ['type', 'jsonSchema'],
461
+ additionalProperties: false,
462
+ properties: {
463
+ type: {
464
+ type: 'string'
465
+ },
466
+ jsonSchema: {
467
+ type: 'object'
468
+ }
469
+ }
470
+ },
471
+ stepTemplate: typedTemplate,
472
+ verifiablePresentationRequest: {
473
+ type: 'object'
444
474
  }
445
475
  }
446
476
  };