@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 +10 -1
- package/lib/http.js +58 -10
- package/lib/issue.js +12 -12
- package/lib/openId.js +3 -3
- package/package.json +1 -1
- package/schemas/bedrock-vc-exchanger.js +25 -7
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
|
-
//
|
|
52
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
res.json(
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -109,8 +109,8 @@ export const createExchangeBody = {
|
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const
|
|
113
|
-
title: '
|
|
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:
|
|
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
|
-
|
|
179
|
-
// `jwtDidProofRequest` to more variables to be
|
|
180
|
-
// used when issuing VCs
|
|
197
|
+
},
|
|
198
|
+
stepTemplate: typedTemplate
|
|
181
199
|
}
|
|
182
200
|
};
|
|
183
201
|
|