@bedrock/vc-delivery 4.3.0 → 4.4.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/config.js +17 -5
- package/lib/exchanges.js +84 -38
- package/lib/helpers.js +15 -8
- package/lib/http.js +12 -154
- package/lib/index.js +66 -11
- package/lib/issue.js +17 -16
- package/lib/logger.js +2 -2
- package/lib/openId.js +150 -61
- package/lib/vcapi.js +170 -0
- package/lib/verify.js +98 -25
- package/package.json +1 -1
- package/schemas/{bedrock-vc-exchanger.js → bedrock-vc-workflow.js} +5 -1
package/lib/config.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved.
|
|
2
|
+
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
|
|
6
|
+
const c = bedrock.util.config.main;
|
|
7
|
+
const cc = c.computer();
|
|
6
8
|
const {config} = bedrock;
|
|
7
9
|
|
|
8
|
-
// use `vc-
|
|
9
|
-
const namespace = 'vc-
|
|
10
|
+
// use `vc-workflow` namespace
|
|
11
|
+
const namespace = 'vc-workflow';
|
|
10
12
|
config[namespace] = {};
|
|
11
13
|
|
|
12
|
-
// create dev application identity for vc-
|
|
14
|
+
// create dev application identity for vc-workflow (must be overridden in
|
|
13
15
|
// deployments) ...and `ensureConfigOverride` has already been set via
|
|
14
16
|
// `bedrock-app-identity` so it doesn't have to be set here
|
|
15
|
-
config['app-identity'].seeds.services['vc-
|
|
17
|
+
config['app-identity'].seeds.services['vc-workflow'] = {
|
|
16
18
|
id: 'did:key:z6MknmKKxYiYo6txxX2bCgzeuBDkPPb5SJ36p232XkVEk7mf',
|
|
17
19
|
seedMultibase: 'z1Abgbd91bbZHPYakVA7EPvhY9NZ2EaTkEpmwdBCfifokDn',
|
|
20
|
+
serviceType: 'vc-workflow'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// backwards compatibility: `vc-exchanger` alias:
|
|
24
|
+
cc('vc-exchanger', () => config[namespace]);
|
|
25
|
+
config['app-identity'].seeds.services['vc-exchanger'] = {
|
|
18
26
|
serviceType: 'vc-exchanger'
|
|
19
27
|
};
|
|
28
|
+
cc('app-identity.seeds.services.vc-exchanger.id', () =>
|
|
29
|
+
config['app-identity'].seeds.services['vc-workflow'].id);
|
|
30
|
+
cc('app-identity.seeds.services.vc-exchanger.seedMultibase', () =>
|
|
31
|
+
config['app-identity'].seeds.services['vc-workflow'].seedMultibase);
|
package/lib/exchanges.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import * as database from '@bedrock/mongodb';
|
|
@@ -32,7 +32,20 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
|
|
|
32
32
|
await database.openCollections([COLLECTION_NAME]);
|
|
33
33
|
|
|
34
34
|
await database.createIndexes([{
|
|
35
|
-
// cover exchange queries by
|
|
35
|
+
// cover exchange queries by local workflow ID + exchange ID
|
|
36
|
+
collection: COLLECTION_NAME,
|
|
37
|
+
fields: {localWorkflowId: 1, 'exchange.id': 1},
|
|
38
|
+
options: {
|
|
39
|
+
partialFilterExpression: {
|
|
40
|
+
localWorkflowId: {$exists: true}
|
|
41
|
+
},
|
|
42
|
+
unique: true, background: false
|
|
43
|
+
}
|
|
44
|
+
}, {
|
|
45
|
+
// backwards compatibility: cover exchange queries by
|
|
46
|
+
// local exchanger ID + exchange ID; local exchanger ID is the same as
|
|
47
|
+
// local workflow ID and this index can be eventually dropped once no
|
|
48
|
+
// deployments use `localExchangerId`
|
|
36
49
|
collection: COLLECTION_NAME,
|
|
37
50
|
fields: {localExchangerId: 1, 'exchange.id': 1},
|
|
38
51
|
options: {unique: true, background: false}
|
|
@@ -55,14 +68,14 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
|
|
|
55
68
|
* Inserts an exchange record.
|
|
56
69
|
*
|
|
57
70
|
* @param {object} options - The options to use.
|
|
58
|
-
* @param {string} options.
|
|
71
|
+
* @param {string} options.workflowId - The ID of the workflow that the
|
|
59
72
|
* exchange is associated with.
|
|
60
73
|
* @param {object} options.exchange - The exchange to insert.
|
|
61
74
|
*
|
|
62
75
|
* @returns {Promise<object>} Resolves to the database record.
|
|
63
76
|
*/
|
|
64
|
-
export async function insert({
|
|
65
|
-
assert.string(
|
|
77
|
+
export async function insert({workflowId, exchange}) {
|
|
78
|
+
assert.string(workflowId, 'workflowId');
|
|
66
79
|
assert.object(exchange, 'exchange');
|
|
67
80
|
assert.string(exchange.id, 'exchange.id');
|
|
68
81
|
// optional time to live in seconds
|
|
@@ -79,11 +92,13 @@ export async function insert({exchangerId, exchange}) {
|
|
|
79
92
|
// TTL is in seconds
|
|
80
93
|
meta.expires = new Date(now + exchange.ttl * 1000);
|
|
81
94
|
}
|
|
82
|
-
const {localId:
|
|
95
|
+
const {localId: localWorkflowId} = parseLocalId({id: workflowId});
|
|
83
96
|
const record = {
|
|
84
|
-
|
|
97
|
+
localWorkflowId,
|
|
98
|
+
// backwards compatibility: enable existing systems to find record
|
|
99
|
+
localExchangerId: localWorkflowId,
|
|
85
100
|
meta,
|
|
86
|
-
// possible states are: `pending`, `complete`, or `invalid`
|
|
101
|
+
// possible states are: `pending`, `active`, `complete`, or `invalid`
|
|
87
102
|
exchange: {...exchange, sequence: 0, state: 'pending'}
|
|
88
103
|
};
|
|
89
104
|
|
|
@@ -111,7 +126,7 @@ export async function insert({exchangerId, exchange}) {
|
|
|
111
126
|
* Gets an exchange record.
|
|
112
127
|
*
|
|
113
128
|
* @param {object} options - The options to use.
|
|
114
|
-
* @param {string} options.
|
|
129
|
+
* @param {string} options.workflowId - The ID of the workflow that the
|
|
115
130
|
* exchange is associated with.
|
|
116
131
|
* @param {string} options.id - The ID of the exchange to retrieve.
|
|
117
132
|
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
@@ -119,18 +134,23 @@ export async function insert({exchangerId, exchange}) {
|
|
|
119
134
|
* @returns {Promise<object | ExplainObject>} Resolves with the record that
|
|
120
135
|
* matches the query or an ExplainObject if `explain=true`.
|
|
121
136
|
*/
|
|
122
|
-
export async function get({
|
|
123
|
-
assert.string(
|
|
137
|
+
export async function get({workflowId, id, explain = false} = {}) {
|
|
138
|
+
assert.string(workflowId, 'workflowId');
|
|
124
139
|
assert.string(id, 'id');
|
|
125
140
|
|
|
126
|
-
const {localId:
|
|
141
|
+
const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
|
|
127
142
|
const collection = database.collections[COLLECTION_NAME];
|
|
128
143
|
const query = {
|
|
129
|
-
|
|
144
|
+
localWorkflowId,
|
|
130
145
|
'exchange.id': id,
|
|
131
146
|
// treat exchange as not found if invalid
|
|
132
147
|
'exchange.state': {$ne: 'invalid'}
|
|
133
148
|
};
|
|
149
|
+
// backwards compatibility: query on `localExchangerId`
|
|
150
|
+
if(base.endsWith('/exchangers')) {
|
|
151
|
+
query.localWorkflowId = {$in: [null, localWorkflowId]};
|
|
152
|
+
query.localExchangerId = localWorkflowId;
|
|
153
|
+
}
|
|
134
154
|
const projection = {_id: 0, exchange: 1, meta: 1};
|
|
135
155
|
|
|
136
156
|
if(explain) {
|
|
@@ -145,7 +165,9 @@ export async function get({exchangerId, id, explain = false} = {}) {
|
|
|
145
165
|
throw new BedrockError('Exchange not found.', {
|
|
146
166
|
name: 'NotFoundError',
|
|
147
167
|
details: {
|
|
148
|
-
|
|
168
|
+
workflow: workflowId,
|
|
169
|
+
// backwards compatibility
|
|
170
|
+
exchanger: workflowId,
|
|
149
171
|
exchange: id,
|
|
150
172
|
httpStatusCode: 404,
|
|
151
173
|
public: true
|
|
@@ -155,11 +177,18 @@ export async function get({exchangerId, id, explain = false} = {}) {
|
|
|
155
177
|
|
|
156
178
|
// backwards compatibility; initialize `sequence`
|
|
157
179
|
if(record.exchange.sequence === undefined) {
|
|
158
|
-
|
|
159
|
-
|
|
180
|
+
const query = {
|
|
181
|
+
localWorkflowId,
|
|
160
182
|
'exchange.id': id,
|
|
161
183
|
'exchange.sequence': null
|
|
162
|
-
}
|
|
184
|
+
};
|
|
185
|
+
// backwards compatibility: query on `localExchangerId`
|
|
186
|
+
if(base.endsWith('/exchangers')) {
|
|
187
|
+
query.localWorkflowId = {$in: [null, localWorkflowId]};
|
|
188
|
+
query.localExchangerId = localWorkflowId;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await collection.updateOne(query, {$set: {'exchange.sequence': 0}});
|
|
163
192
|
record.exchange.sequence = 0;
|
|
164
193
|
}
|
|
165
194
|
|
|
@@ -167,10 +196,11 @@ export async function get({exchangerId, id, explain = false} = {}) {
|
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
/**
|
|
170
|
-
* Updates a pending exchange with new variables, step, and TTL
|
|
199
|
+
* Updates a pending exchange with new state, variables, step, and TTL
|
|
200
|
+
* information.
|
|
171
201
|
*
|
|
172
202
|
* @param {object} options - The options to use.
|
|
173
|
-
* @param {string} options.
|
|
203
|
+
* @param {string} options.workflowId - The ID of the workflow the exchange
|
|
174
204
|
* is associated with.
|
|
175
205
|
* @param {object} options.exchange - The exchange to update.
|
|
176
206
|
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
@@ -178,8 +208,8 @@ export async function get({exchangerId, id, explain = false} = {}) {
|
|
|
178
208
|
* @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
|
|
179
209
|
* success or an ExplainObject if `explain=true`.
|
|
180
210
|
*/
|
|
181
|
-
export async function update({
|
|
182
|
-
assert.string(
|
|
211
|
+
export async function update({workflowId, exchange, explain = false} = {}) {
|
|
212
|
+
assert.string(workflowId, 'workflowId');
|
|
183
213
|
assert.object(exchange, 'exchange');
|
|
184
214
|
const {id} = exchange;
|
|
185
215
|
|
|
@@ -187,7 +217,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
|
|
|
187
217
|
const now = Date.now();
|
|
188
218
|
const update = {
|
|
189
219
|
$inc: {'exchange.sequence': 1},
|
|
190
|
-
$set: {'meta.updated': now}
|
|
220
|
+
$set: {'exchange.state': exchange.state, 'meta.updated': now}
|
|
191
221
|
};
|
|
192
222
|
// update exchange `variables`, `step`, and `ttl`
|
|
193
223
|
if(exchange.variables) {
|
|
@@ -200,17 +230,22 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
|
|
|
200
230
|
update.$set['exchange.ttl'] = exchange.ttl;
|
|
201
231
|
}
|
|
202
232
|
|
|
203
|
-
const {localId:
|
|
233
|
+
const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
|
|
204
234
|
|
|
205
235
|
const collection = database.collections[COLLECTION_NAME];
|
|
206
236
|
const query = {
|
|
207
|
-
|
|
237
|
+
localWorkflowId,
|
|
208
238
|
'exchange.id': id,
|
|
209
239
|
// exchange sequence must match previous sequence
|
|
210
240
|
'exchange.sequence': exchange.sequence - 1,
|
|
211
|
-
// previous state must be `pending` in order to update it
|
|
212
|
-
'exchange.state': 'pending'
|
|
241
|
+
// previous state must be `pending` or `active` in order to update it
|
|
242
|
+
'exchange.state': {$in: ['pending', 'active']}
|
|
213
243
|
};
|
|
244
|
+
// backwards compatibility: query on `localExchangerId`
|
|
245
|
+
if(base.endsWith('/exchangers')) {
|
|
246
|
+
query.localWorkflowId = {$in: [null, localWorkflowId]};
|
|
247
|
+
query.localExchangerId = localWorkflowId;
|
|
248
|
+
}
|
|
214
249
|
|
|
215
250
|
if(explain) {
|
|
216
251
|
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
|
|
@@ -238,7 +273,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
|
|
|
238
273
|
|
|
239
274
|
// if no document was matched, try to get an existing exchange; if the
|
|
240
275
|
// exchange does not exist, a not found error will be automatically thrown
|
|
241
|
-
await get({
|
|
276
|
+
await get({workflowId, id});
|
|
242
277
|
|
|
243
278
|
/* Note: Here the exchange *does* exist, but the step or state did not
|
|
244
279
|
match which is a conflict error. */
|
|
@@ -258,7 +293,7 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
|
|
|
258
293
|
* Marks an exchange as complete.
|
|
259
294
|
*
|
|
260
295
|
* @param {object} options - The options to use.
|
|
261
|
-
* @param {string} options.
|
|
296
|
+
* @param {string} options.workflowId - The ID of the workflow the exchange
|
|
262
297
|
* is associated with.
|
|
263
298
|
* @param {object} options.exchange - The exchange to mark as complete.
|
|
264
299
|
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
@@ -266,8 +301,8 @@ export async function update({exchangerId, exchange, explain = false} = {}) {
|
|
|
266
301
|
* @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
|
|
267
302
|
* success or an ExplainObject if `explain=true`.
|
|
268
303
|
*/
|
|
269
|
-
export async function complete({
|
|
270
|
-
assert.string(
|
|
304
|
+
export async function complete({workflowId, exchange, explain = false} = {}) {
|
|
305
|
+
assert.string(workflowId, 'workflowId');
|
|
271
306
|
assert.object(exchange, 'exchange');
|
|
272
307
|
const {id} = exchange;
|
|
273
308
|
|
|
@@ -291,17 +326,23 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
|
|
|
291
326
|
update.$set['exchange.ttl'] = exchange.ttl;
|
|
292
327
|
}
|
|
293
328
|
|
|
294
|
-
const {localId:
|
|
329
|
+
const {base, localId: localWorkflowId} = parseLocalId({id: workflowId});
|
|
295
330
|
|
|
296
331
|
const collection = database.collections[COLLECTION_NAME];
|
|
297
332
|
const query = {
|
|
298
|
-
|
|
333
|
+
localWorkflowId,
|
|
299
334
|
'exchange.id': id,
|
|
300
335
|
// exchange sequence must match previous sequence
|
|
301
336
|
'exchange.sequence': exchange.sequence - 1,
|
|
302
|
-
// previous state must be `pending` in order to change to
|
|
303
|
-
|
|
337
|
+
// previous state must be `pending` or `active` in order to change to
|
|
338
|
+
// `complete`
|
|
339
|
+
'exchange.state': {$in: ['pending', 'active']}
|
|
304
340
|
};
|
|
341
|
+
// backwards compatibility: query on `localExchangerId`
|
|
342
|
+
if(base.endsWith('/exchangers')) {
|
|
343
|
+
query.localWorkflowId = {$in: [null, localWorkflowId]};
|
|
344
|
+
query.localExchangerId = localWorkflowId;
|
|
345
|
+
}
|
|
305
346
|
|
|
306
347
|
if(explain) {
|
|
307
348
|
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
|
|
@@ -329,13 +370,14 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
|
|
|
329
370
|
|
|
330
371
|
// if no document was matched, try to get an existing exchange; if the
|
|
331
372
|
// exchange does not exist, a not found error will be automatically thrown
|
|
332
|
-
const record = await get({
|
|
373
|
+
const record = await get({workflowId, id});
|
|
333
374
|
|
|
334
375
|
/* Note: Here the exchange *does* exist, but couldn't be updated because
|
|
335
376
|
another process changed it. That change either left it in a still pending
|
|
336
377
|
state or it was already completed. If it was already completed, it is an
|
|
337
378
|
error condition that must result in invalidating the exchange. */
|
|
338
|
-
if(record.state === 'pending'
|
|
379
|
+
if(record.exchange.state === 'pending' ||
|
|
380
|
+
record.exchange.state === 'active') {
|
|
339
381
|
// exchange still pending, another process updated it
|
|
340
382
|
throw new BedrockError('Could not update exchange; conflict error.', {
|
|
341
383
|
name: 'InvalidStateError',
|
|
@@ -350,7 +392,7 @@ export async function complete({exchangerId, exchange, explain = false} = {}) {
|
|
|
350
392
|
// state is either `complete` or `invalid`, so throw duplicate completed
|
|
351
393
|
// exchange error and invalidate exchange if needed, but do not throw any
|
|
352
394
|
// error to client; only log it
|
|
353
|
-
if(record.state !== 'invalid') {
|
|
395
|
+
if(record.exchange.state !== 'invalid') {
|
|
354
396
|
_invalidateExchange({record}).catch(
|
|
355
397
|
error => logger.error(`Could not invalidate exchange "${id}".`, {error}));
|
|
356
398
|
}
|
|
@@ -387,9 +429,13 @@ async function _markExchangeInvalid({record}) {
|
|
|
387
429
|
// mark exchange invalid
|
|
388
430
|
try {
|
|
389
431
|
const query = {
|
|
390
|
-
|
|
432
|
+
localWorkflowId: record.localWorkflowId,
|
|
391
433
|
'exchange.id': record.exchange.id
|
|
392
434
|
};
|
|
435
|
+
// backwards compatibility: query on `localExchangerId`
|
|
436
|
+
if(!record.localWorkflowId) {
|
|
437
|
+
query.localExchangerId = record.localExchangerId;
|
|
438
|
+
}
|
|
393
439
|
const update = {
|
|
394
440
|
$set: {
|
|
395
441
|
'exchange.state': 'invalid',
|
package/lib/helpers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
5
|
import {decodeId, generateId} from 'bnid';
|
|
@@ -12,7 +12,7 @@ import {ZcapClient} from '@digitalbazaar/ezcap';
|
|
|
12
12
|
const {config} = bedrock;
|
|
13
13
|
|
|
14
14
|
export async function evaluateTemplate({
|
|
15
|
-
|
|
15
|
+
workflow, exchange, typedTemplate
|
|
16
16
|
} = {}) {
|
|
17
17
|
// run jsonata compiler; only `jsonata` template type is supported and this
|
|
18
18
|
// assumes only this template type will be passed in
|
|
@@ -20,8 +20,12 @@ export async function evaluateTemplate({
|
|
|
20
20
|
const {variables = {}} = exchange;
|
|
21
21
|
// always include `globals` as keyword for self-referencing exchange info
|
|
22
22
|
variables.globals = {
|
|
23
|
+
workflow: {
|
|
24
|
+
id: workflow.id
|
|
25
|
+
},
|
|
26
|
+
// backwards compatibility
|
|
23
27
|
exchanger: {
|
|
24
|
-
id:
|
|
28
|
+
id: workflow.id
|
|
25
29
|
},
|
|
26
30
|
exchange: {
|
|
27
31
|
id: exchange.id
|
|
@@ -30,7 +34,7 @@ export async function evaluateTemplate({
|
|
|
30
34
|
return jsonata(template).evaluate(variables, variables);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
export function
|
|
37
|
+
export function getWorkflowId({routePrefix, localId} = {}) {
|
|
34
38
|
return `${config.server.baseUri}${routePrefix}/${localId}`;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -44,12 +48,15 @@ export async function generateRandom() {
|
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
export async function getZcapClient({
|
|
51
|
+
export async function getZcapClient({workflow} = {}) {
|
|
48
52
|
// get service agent for communicating with the issuer instance
|
|
49
|
-
const {
|
|
50
|
-
|
|
53
|
+
const {pathname} = new URL(workflow.id);
|
|
54
|
+
// backwards-compatibility: support deprecated `vc-exchanger`
|
|
55
|
+
const serviceType = pathname.startsWith('/workflows/') ?
|
|
56
|
+
'vc-workflow' : 'vc-exchanger';
|
|
57
|
+
const {serviceAgent} = await serviceAgents.get({serviceType});
|
|
51
58
|
const {capabilityAgent, zcaps} = await serviceAgents.getEphemeralAgent(
|
|
52
|
-
{config:
|
|
59
|
+
{config: workflow, serviceAgent});
|
|
53
60
|
|
|
54
61
|
// create zcap client for issuing VCs
|
|
55
62
|
const zcapClient = new ZcapClient({
|
package/lib/http.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Copyright (c) 2018-
|
|
2
|
+
* Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as _openId from './openId.js';
|
|
5
5
|
import * as bedrock from '@bedrock/core';
|
|
6
6
|
import * as exchanges from './exchanges.js';
|
|
7
|
-
import {createChallenge as _createChallenge, verify} from './verify.js';
|
|
8
7
|
import {
|
|
9
8
|
createExchangeBody, useExchangeBody
|
|
10
|
-
} from '../schemas/bedrock-vc-
|
|
11
|
-
import {evaluateTemplate, generateRandom, getExchangerId} from './helpers.js';
|
|
9
|
+
} from '../schemas/bedrock-vc-workflow.js';
|
|
12
10
|
import {exportJWK, generateKeyPair, importJWK} from 'jose';
|
|
11
|
+
import {generateRandom, getWorkflowId} from './helpers.js';
|
|
13
12
|
import {metering, middleware} from '@bedrock/service-core';
|
|
14
13
|
import {asyncHandler} from '@bedrock/express';
|
|
15
14
|
import bodyParser from 'body-parser';
|
|
16
15
|
import cors from 'cors';
|
|
17
|
-
import {issue} from './issue.js';
|
|
18
|
-
import {klona} from 'klona';
|
|
19
16
|
import {logger} from './logger.js';
|
|
17
|
+
import {processExchange} from './vcapi.js';
|
|
20
18
|
import {createValidateMiddleware as validate} from '@bedrock/validation';
|
|
21
19
|
|
|
22
20
|
const {util: {BedrockError}} = bedrock;
|
|
@@ -31,8 +29,6 @@ bedrock.events.on('bedrock-express.configure.bodyParser', app => {
|
|
|
31
29
|
}));
|
|
32
30
|
});
|
|
33
31
|
|
|
34
|
-
const MAXIMUM_STEPS = 100;
|
|
35
|
-
|
|
36
32
|
export async function addRoutes({app, service} = {}) {
|
|
37
33
|
const {routePrefix} = service;
|
|
38
34
|
|
|
@@ -42,15 +38,15 @@ export async function addRoutes({app, service} = {}) {
|
|
|
42
38
|
exchange: `${baseUrl}/exchanges/:exchangeId`
|
|
43
39
|
};
|
|
44
40
|
|
|
45
|
-
// used to retrieve service object (
|
|
41
|
+
// used to retrieve service object (workflow) config
|
|
46
42
|
const getConfigMiddleware = middleware.createGetConfigMiddleware({service});
|
|
47
43
|
|
|
48
44
|
// used to fetch exchange record in parallel
|
|
49
45
|
const getExchange = asyncHandler(async (req, res, next) => {
|
|
50
46
|
const {localId, exchangeId: id} = req.params;
|
|
51
|
-
const
|
|
47
|
+
const workflowId = getWorkflowId({routePrefix, localId});
|
|
52
48
|
// expose access to result via `req`; do not wait for it to settle here
|
|
53
|
-
const exchangePromise = exchanges.get({
|
|
49
|
+
const exchangePromise = exchanges.get({workflowId, id}).catch(e => e);
|
|
54
50
|
req.getExchange = async () => {
|
|
55
51
|
const record = await exchangePromise;
|
|
56
52
|
if(record instanceof Error) {
|
|
@@ -121,7 +117,7 @@ export async function addRoutes({app, service} = {}) {
|
|
|
121
117
|
}
|
|
122
118
|
|
|
123
119
|
// insert exchange
|
|
124
|
-
const {id:
|
|
120
|
+
const {id: workflowId} = config;
|
|
125
121
|
const exchange = {
|
|
126
122
|
id: await generateRandom(),
|
|
127
123
|
ttl,
|
|
@@ -129,8 +125,8 @@ export async function addRoutes({app, service} = {}) {
|
|
|
129
125
|
openId,
|
|
130
126
|
step
|
|
131
127
|
};
|
|
132
|
-
await exchanges.insert({
|
|
133
|
-
const location = `${
|
|
128
|
+
await exchanges.insert({workflowId, exchange});
|
|
129
|
+
const location = `${workflowId}/exchanges/${exchange.id}`;
|
|
134
130
|
res.status(204).location(location).send();
|
|
135
131
|
} catch(error) {
|
|
136
132
|
logger.error(error.message, {error});
|
|
@@ -164,147 +160,9 @@ export async function addRoutes({app, service} = {}) {
|
|
|
164
160
|
getExchange,
|
|
165
161
|
getConfigMiddleware,
|
|
166
162
|
asyncHandler(async (req, res) => {
|
|
167
|
-
const {config:
|
|
163
|
+
const {config: workflow} = req.serviceObject;
|
|
168
164
|
const {exchange} = await req.getExchange();
|
|
169
|
-
|
|
170
|
-
// get any `verifiablePresentation` from the body...
|
|
171
|
-
let receivedPresentation = req?.body?.verifiablePresentation;
|
|
172
|
-
|
|
173
|
-
// process exchange step(s)
|
|
174
|
-
let i = 0;
|
|
175
|
-
let currentStep = exchange.step;
|
|
176
|
-
let step;
|
|
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
|
-
|
|
185
|
-
// no step present, break out to complete exchange
|
|
186
|
-
if(!currentStep) {
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// get current step details
|
|
191
|
-
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
|
-
{exchanger, 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
|
-
}
|
|
212
|
-
|
|
213
|
-
// handle VPR: if step requires it, then `verifiablePresentation` must
|
|
214
|
-
// be in the request
|
|
215
|
-
if(step.verifiablePresentationRequest) {
|
|
216
|
-
const {createChallenge} = step;
|
|
217
|
-
const isInitialStep = exchange.step === exchanger.initialStep;
|
|
218
|
-
|
|
219
|
-
// if no presentation was received in the body...
|
|
220
|
-
if(!receivedPresentation) {
|
|
221
|
-
const verifiablePresentationRequest = klona(
|
|
222
|
-
step.verifiablePresentationRequest);
|
|
223
|
-
if(createChallenge) {
|
|
224
|
-
/* Note: When creating a challenge, the initial step always
|
|
225
|
-
uses the local exchange ID because the initial step itself
|
|
226
|
-
is one-time use. Subsequent steps, which only VC-API (as opposed
|
|
227
|
-
to other protocols) supports creating additional challenges via
|
|
228
|
-
the VC-API verifier API. */
|
|
229
|
-
let challenge;
|
|
230
|
-
if(isInitialStep) {
|
|
231
|
-
challenge = exchange.id;
|
|
232
|
-
} else {
|
|
233
|
-
// generate a new challenge using verifier API
|
|
234
|
-
({challenge} = await _createChallenge({exchanger}));
|
|
235
|
-
}
|
|
236
|
-
verifiablePresentationRequest.challenge = challenge;
|
|
237
|
-
}
|
|
238
|
-
// send VPR and return
|
|
239
|
-
res.json({verifiablePresentationRequest});
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// verify the received VP
|
|
244
|
-
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
245
|
-
const {verificationMethod} = await verify({
|
|
246
|
-
exchanger,
|
|
247
|
-
verifiablePresentationRequest: step.verifiablePresentationRequest,
|
|
248
|
-
presentation: receivedPresentation, expectedChallenge
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// store VP results in variables associated with current step
|
|
252
|
-
if(!exchange.variables.results) {
|
|
253
|
-
exchange.variables.results = {};
|
|
254
|
-
}
|
|
255
|
-
exchange.variables.results[currentStep] = {
|
|
256
|
-
// common use case of DID Authentication; provide `did` for ease
|
|
257
|
-
// of use in templates and consistency with OID4VCI which only
|
|
258
|
-
// receives `did` not verification method nor VP
|
|
259
|
-
did: verificationMethod.controller,
|
|
260
|
-
verificationMethod,
|
|
261
|
-
verifiablePresentation: receivedPresentation
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
// clear received presentation as it has been processed
|
|
265
|
-
receivedPresentation = null;
|
|
266
|
-
|
|
267
|
-
// if there is no next step, break out to complete exchange
|
|
268
|
-
if(!step.nextStep) {
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// update the exchange to go to the next step, then loop to send
|
|
273
|
-
// next VPR
|
|
274
|
-
currentStep = exchange.step = step.nextStep;
|
|
275
|
-
exchange.sequence++;
|
|
276
|
-
await exchanges.update({exchangerId: exchanger.id, exchange});
|
|
277
|
-
|
|
278
|
-
// FIXME: there may be VCs to issue during this step, do so before
|
|
279
|
-
// sending the VPR above
|
|
280
|
-
} else if(step.nextStep) {
|
|
281
|
-
// next steps without VPRs are prohibited
|
|
282
|
-
throw new BedrockError(
|
|
283
|
-
'Invalid step detected; continuing exchanges must include VPRs.', {
|
|
284
|
-
name: 'DataError',
|
|
285
|
-
details: {httpStatusCode: 500, public: true}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// mark exchange complete
|
|
291
|
-
exchange.sequence++;
|
|
292
|
-
await exchanges.complete({exchangerId: exchanger.id, exchange});
|
|
293
|
-
|
|
294
|
-
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
295
|
-
// replay attack detected) after exchange has been marked complete
|
|
296
|
-
|
|
297
|
-
// issue any VCs; may return an empty result if the step defines no
|
|
298
|
-
// VCs to issue
|
|
299
|
-
const result = await issue({exchanger, exchange});
|
|
300
|
-
|
|
301
|
-
// if last `step` has a redirect URL, include it in the response
|
|
302
|
-
if(step?.redirectUrl) {
|
|
303
|
-
result.redirectUrl = step.redirectUrl;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// send result
|
|
307
|
-
res.json(result);
|
|
165
|
+
await processExchange({req, res, workflow, exchange});
|
|
308
166
|
}));
|
|
309
167
|
|
|
310
168
|
// create OID4VCI routes to be used with each individual exchange
|