@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 +34 -2
- package/lib/issue.js +82 -21
- package/lib/oid4/oid4vci.js +6 -21
- package/lib/oid4/oid4vp.js +4 -7
- package/lib/vcapi.js +3 -8
- package/package.json +1 -1
- package/schemas/bedrock-vc-workflow.js +48 -18
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
|
|
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
|
-
//
|
|
24
|
-
const issueRequests = await
|
|
25
|
-
|
|
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(
|
|
36
|
-
verifiablePresentation.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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/oid4/oid4vci.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as exchanges from '../exchanges.js';
|
|
6
|
-
import {
|
|
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
|
@@ -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
|
-
'
|
|
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
|
-
|
|
367
|
-
type: '
|
|
368
|
-
|
|
369
|
-
|
|
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
|
};
|