@bedrock/vc-delivery 3.0.2 → 3.1.1
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/README.md +1 -1
- package/lib/exchanges.js +122 -4
- package/lib/http.js +65 -26
- package/lib/index.js +5 -3
- package/lib/openId.js +14 -4
- package/package.json +2 -2
- package/schemas/bedrock-vc-exchanger.js +3 -4
package/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# bedrock-
|
|
1
|
+
# bedrock-vc-delivery
|
package/lib/exchanges.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 * as database from '@bedrock/mongodb';
|
|
@@ -156,21 +156,123 @@ export async function get({exchangerId, id, explain = false} = {}) {
|
|
|
156
156
|
return record;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Updates a pending exchange with new variables, step, and TTL information.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} options - The options to use.
|
|
163
|
+
* @param {string} options.exchangerId - The ID of the exchanger the exchange
|
|
164
|
+
* is associated with.
|
|
165
|
+
* @param {object} options.exchange - The exchange to update.
|
|
166
|
+
* @param {string} [options.expectedStep] - The expected current step in the
|
|
167
|
+
* exchange.
|
|
168
|
+
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
169
|
+
*
|
|
170
|
+
* @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
|
|
171
|
+
* success or an ExplainObject if `explain=true`.
|
|
172
|
+
*/
|
|
173
|
+
export async function update({
|
|
174
|
+
exchangerId, exchange, expectedStep, explain = false
|
|
175
|
+
} = {}) {
|
|
176
|
+
assert.string(exchangerId, 'exchangerId');
|
|
177
|
+
assert.object(exchange, 'exchange');
|
|
178
|
+
assert.optionalString(expectedStep, expectedStep);
|
|
179
|
+
const {id} = exchange;
|
|
180
|
+
|
|
181
|
+
// build update
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
const update = {
|
|
184
|
+
$set: {'meta.updated': now}
|
|
185
|
+
};
|
|
186
|
+
// update exchange `variables.results[step]`, `step`, and `ttl`
|
|
187
|
+
if(exchange.variables?.results) {
|
|
188
|
+
update.$set['exchange.variables.results'] = exchange.variables.results;
|
|
189
|
+
}
|
|
190
|
+
if(exchange.step !== undefined) {
|
|
191
|
+
update.$set['exchange.step'] = exchange.step;
|
|
192
|
+
}
|
|
193
|
+
if(exchange.ttl !== undefined) {
|
|
194
|
+
update.$set['exchange.ttl'] = exchange.ttl;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const {localId: localExchangerId} = parseLocalId({id: exchangerId});
|
|
198
|
+
|
|
199
|
+
const collection = database.collections[COLLECTION_NAME];
|
|
200
|
+
const query = {
|
|
201
|
+
localExchangerId,
|
|
202
|
+
'exchange.id': id,
|
|
203
|
+
// previous state must be `pending` in order to update it
|
|
204
|
+
'exchange.state': 'pending'
|
|
205
|
+
};
|
|
206
|
+
// current step must match previous step
|
|
207
|
+
if(expectedStep === undefined) {
|
|
208
|
+
query['exchange.step'] = null;
|
|
209
|
+
} else {
|
|
210
|
+
query['exchange.step'] = expectedStep;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if(explain) {
|
|
214
|
+
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
|
|
215
|
+
// cursor which allows the use of the explain function.
|
|
216
|
+
const cursor = await collection.find(query).limit(1);
|
|
217
|
+
return cursor.explain('executionStats');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const result = await collection.updateOne(query, update);
|
|
222
|
+
if(result.result.n > 0) {
|
|
223
|
+
// document modified: success
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
} catch(e) {
|
|
227
|
+
throw new BedrockError('Could not update exchange.', {
|
|
228
|
+
name: 'OperationError',
|
|
229
|
+
details: {
|
|
230
|
+
public: true,
|
|
231
|
+
httpStatusCode: 500
|
|
232
|
+
},
|
|
233
|
+
cause: e
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// if no document was matched, try to get an existing exchange; if the
|
|
238
|
+
// exchange does not exist, a not found error will be automatically thrown
|
|
239
|
+
await get({exchangerId, id});
|
|
240
|
+
|
|
241
|
+
/* Note: Here the exchange *does* exist, but the step or state did not
|
|
242
|
+
match which is a conflict error. */
|
|
243
|
+
|
|
244
|
+
// throw duplicate completed exchange error
|
|
245
|
+
throw new BedrockError('Could not update exchange; conflict error.', {
|
|
246
|
+
name: 'InvalidStateError',
|
|
247
|
+
details: {
|
|
248
|
+
public: true,
|
|
249
|
+
// this is a client-side conflict error
|
|
250
|
+
httpStatusCode: 409
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
159
255
|
/**
|
|
160
256
|
* Marks an exchange as complete.
|
|
161
257
|
*
|
|
162
258
|
* @param {object} options - The options to use.
|
|
163
259
|
* @param {string} options.exchangerId - The ID of the exchanger the exchange
|
|
164
260
|
* is associated with.
|
|
165
|
-
* @param {object} options.
|
|
261
|
+
* @param {object} options.exchange - The exchange to mark as complete.
|
|
262
|
+
* @param {string} [options.expectedStep] - The expected current step in the
|
|
263
|
+
* exchange.
|
|
166
264
|
* @param {boolean} [options.explain=false] - An optional explain boolean.
|
|
167
265
|
*
|
|
168
266
|
* @returns {Promise<boolean | ExplainObject>} Resolves with `true` on update
|
|
169
267
|
* success or an ExplainObject if `explain=true`.
|
|
170
268
|
*/
|
|
171
|
-
export async function complete({
|
|
269
|
+
export async function complete({
|
|
270
|
+
exchangerId, exchange, expectedStep, explain = false
|
|
271
|
+
} = {}) {
|
|
172
272
|
assert.string(exchangerId, 'exchangerId');
|
|
173
|
-
assert.
|
|
273
|
+
assert.object(exchange, 'exchange');
|
|
274
|
+
assert.optionalString(expectedStep, expectedStep);
|
|
275
|
+
const {id} = exchange;
|
|
174
276
|
|
|
175
277
|
// build update
|
|
176
278
|
const now = Date.now();
|
|
@@ -180,6 +282,16 @@ export async function complete({exchangerId, id, explain = false} = {}) {
|
|
|
180
282
|
'meta.updated': now
|
|
181
283
|
}
|
|
182
284
|
};
|
|
285
|
+
// update exchange `variables.results[step]`, `step`, and `ttl`
|
|
286
|
+
if(exchange.variables?.results) {
|
|
287
|
+
update.$set['exchange.variables.results'] = exchange.variables.results;
|
|
288
|
+
}
|
|
289
|
+
if(exchange.step !== undefined) {
|
|
290
|
+
update.$set['exchange.step'] = exchange.step;
|
|
291
|
+
}
|
|
292
|
+
if(exchange.ttl !== undefined) {
|
|
293
|
+
update.$set['exchange.ttl'] = exchange.ttl;
|
|
294
|
+
}
|
|
183
295
|
|
|
184
296
|
const {localId: localExchangerId} = parseLocalId({id: exchangerId});
|
|
185
297
|
|
|
@@ -190,6 +302,12 @@ export async function complete({exchangerId, id, explain = false} = {}) {
|
|
|
190
302
|
// previous state must be `pending` in order to change to `complete`
|
|
191
303
|
'exchange.state': 'pending'
|
|
192
304
|
};
|
|
305
|
+
// current step must match previous step
|
|
306
|
+
if(expectedStep === undefined) {
|
|
307
|
+
query['exchange.step'] = null;
|
|
308
|
+
} else {
|
|
309
|
+
query['exchange.step'] = expectedStep;
|
|
310
|
+
}
|
|
193
311
|
|
|
194
312
|
if(explain) {
|
|
195
313
|
// 'find().limit(1)' is used here because 'updateOne()' doesn't return a
|
package/lib/http.js
CHANGED
|
@@ -133,7 +133,21 @@ export async function addRoutes({app, service} = {}) {
|
|
|
133
133
|
metering.reportOperationUsage({req});
|
|
134
134
|
}));
|
|
135
135
|
|
|
136
|
-
// VC-API exchange endpoint
|
|
136
|
+
// VC-API get exchange endpoint
|
|
137
|
+
app.get(
|
|
138
|
+
routes.exchange,
|
|
139
|
+
cors(),
|
|
140
|
+
getExchange,
|
|
141
|
+
getConfigMiddleware,
|
|
142
|
+
middleware.authorizeServiceObjectRequest(),
|
|
143
|
+
asyncHandler(async (req, res) => {
|
|
144
|
+
const {exchange} = await req.exchange;
|
|
145
|
+
// do not return any oauth2 credentials
|
|
146
|
+
delete exchange.openId?.oauth2?.keyPair?.privateKeyJwk;
|
|
147
|
+
res.json({exchange});
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
// VC-API use exchange endpoint
|
|
137
151
|
app.options(routes.exchange, cors());
|
|
138
152
|
app.post(
|
|
139
153
|
routes.exchange,
|
|
@@ -145,19 +159,28 @@ export async function addRoutes({app, service} = {}) {
|
|
|
145
159
|
const {config: exchanger} = req.serviceObject;
|
|
146
160
|
const {exchange} = await req.exchange;
|
|
147
161
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
const step = exchanger.steps[exchange.step];
|
|
162
|
+
// get any `verifiablePresentation` from the body...
|
|
163
|
+
let receivedPresentation = req?.body?.verifiablePresentation;
|
|
151
164
|
|
|
152
|
-
|
|
165
|
+
// process exchange step(s)
|
|
166
|
+
let currentStep = exchange.step;
|
|
167
|
+
while(true) {
|
|
168
|
+
// no step present, break out to complete exchange
|
|
169
|
+
if(!currentStep) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// get current step details
|
|
174
|
+
const step = exchanger.steps[currentStep];
|
|
175
|
+
|
|
176
|
+
// handle VPR: if step requires it, then `verifiablePresentation` must
|
|
153
177
|
// be in the request
|
|
154
178
|
if(step.verifiablePresentationRequest) {
|
|
155
179
|
const {createChallenge} = step;
|
|
156
180
|
const isInitialStep = exchange.step === exchanger.initialStep;
|
|
157
181
|
|
|
158
|
-
// if
|
|
159
|
-
|
|
160
|
-
if(!presentation) {
|
|
182
|
+
// if no presentation was received in the body...
|
|
183
|
+
if(!receivedPresentation) {
|
|
161
184
|
const verifiablePresentationRequest = klona(
|
|
162
185
|
step.verifiablePresentationRequest);
|
|
163
186
|
if(createChallenge) {
|
|
@@ -175,38 +198,54 @@ export async function addRoutes({app, service} = {}) {
|
|
|
175
198
|
}
|
|
176
199
|
verifiablePresentationRequest.challenge = challenge;
|
|
177
200
|
}
|
|
178
|
-
// send VPR
|
|
201
|
+
// send VPR and return
|
|
179
202
|
res.json({verifiablePresentationRequest});
|
|
180
203
|
return;
|
|
181
204
|
}
|
|
182
205
|
|
|
183
|
-
// verify the VP
|
|
206
|
+
// verify the received VP
|
|
184
207
|
const expectedChallenge = isInitialStep ? exchange.id : undefined;
|
|
185
|
-
const {verificationMethod} = await verify(
|
|
186
|
-
|
|
208
|
+
const {verificationMethod} = await verify({
|
|
209
|
+
exchanger, presentation: receivedPresentation, expectedChallenge
|
|
210
|
+
});
|
|
187
211
|
|
|
188
|
-
// FIXME: ensure VP satisfies step VPR
|
|
189
|
-
// to convert VPR responses to more exchange variables
|
|
212
|
+
// FIXME: ensure VP satisfies step VPR
|
|
190
213
|
|
|
191
|
-
// store VP in variables
|
|
192
|
-
exchange.variables
|
|
214
|
+
// store VP results in variables associated with current step
|
|
215
|
+
if(!exchange.variables.results) {
|
|
216
|
+
exchange.variables.results = {};
|
|
217
|
+
}
|
|
218
|
+
exchange.variables.results[currentStep] = {
|
|
219
|
+
// common use case of DID Authentication; provide `did` for ease
|
|
220
|
+
// of use in templates and consistency with OID4VCI which only
|
|
221
|
+
// receives `did` not verification method nor VP
|
|
193
222
|
did: verificationMethod.controller,
|
|
194
|
-
|
|
223
|
+
verificationMethod,
|
|
224
|
+
verifiablePresentation: receivedPresentation
|
|
195
225
|
};
|
|
196
226
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
// clear received presentation as it has been processed
|
|
228
|
+
receivedPresentation = null;
|
|
229
|
+
|
|
230
|
+
// if there is no next step, break out to complete exchange
|
|
231
|
+
if(!step.nextStep) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// update the exchange to go to the next step, then loop to send
|
|
236
|
+
// next VPR
|
|
237
|
+
exchange.step = step.nextStep;
|
|
238
|
+
await exchanges.update({
|
|
239
|
+
exchangerId: exchanger.id, exchange, expectedStep: currentStep
|
|
240
|
+
});
|
|
241
|
+
currentStep = exchange.step;
|
|
205
242
|
}
|
|
206
243
|
}
|
|
207
244
|
|
|
208
245
|
// mark exchange complete
|
|
209
|
-
await exchanges.complete({
|
|
246
|
+
await exchanges.complete({
|
|
247
|
+
exchangerId: exchanger.id, exchange, expectedStep: currentStep
|
|
248
|
+
});
|
|
210
249
|
|
|
211
250
|
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
212
251
|
// replay attack detected) after exchange has been marked complete
|
package/lib/index.js
CHANGED
|
@@ -58,7 +58,9 @@ bedrock.events.on('bedrock.init', async () => {
|
|
|
58
58
|
required: false
|
|
59
59
|
}]
|
|
60
60
|
},
|
|
61
|
-
usageAggregator
|
|
61
|
+
async usageAggregator({meter, signal} = {}) {
|
|
62
|
+
return usageAggregator({meter, signal, service});
|
|
63
|
+
}
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
bedrock.events.on('bedrock-express.configure.routes', async app => {
|
|
@@ -100,8 +102,8 @@ async function validateConfigFn({config} = {}) {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// if `steps` are specified, then `initialStep` MUST be included
|
|
103
|
-
const {steps
|
|
104
|
-
if(steps
|
|
105
|
+
const {steps, initialStep} = config;
|
|
106
|
+
if(steps && initialStep === undefined) {
|
|
105
107
|
throw new BedrockError(
|
|
106
108
|
'"initialStep" is required when "steps" are provided.', {
|
|
107
109
|
name: 'DataError',
|
package/lib/openId.js
CHANGED
|
@@ -394,7 +394,8 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
|
|
|
394
394
|
{credentialRequests, expectedCredentialRequests});
|
|
395
395
|
|
|
396
396
|
// process exchange step if present
|
|
397
|
-
|
|
397
|
+
const currentStep = exchange.step;
|
|
398
|
+
if(currentStep) {
|
|
398
399
|
const step = exchanger.steps[exchange.step];
|
|
399
400
|
|
|
400
401
|
// handle JWT DID Proof request; if step requires it, then `proof` must
|
|
@@ -418,13 +419,22 @@ async function _processCredentialRequests({req, res, isBatchRequest}) {
|
|
|
418
419
|
// FIXME: improve error
|
|
419
420
|
throw new Error('every DID must be the same');
|
|
420
421
|
}
|
|
421
|
-
//
|
|
422
|
-
exchange.variables
|
|
422
|
+
// store did results in variables associated with current step
|
|
423
|
+
if(!exchange.variables.results) {
|
|
424
|
+
exchange.variables.results = {};
|
|
425
|
+
}
|
|
426
|
+
exchange.variables.results[currentStep] = {
|
|
427
|
+
// common use case of DID Authentication; provide `did` for ease
|
|
428
|
+
// of use in templates
|
|
429
|
+
did
|
|
430
|
+
};
|
|
423
431
|
}
|
|
424
432
|
}
|
|
425
433
|
|
|
426
434
|
// mark exchange complete
|
|
427
|
-
await exchanges.complete({
|
|
435
|
+
await exchanges.complete({
|
|
436
|
+
exchangerId: exchanger.id, exchange, expectedStep: currentStep
|
|
437
|
+
});
|
|
428
438
|
|
|
429
439
|
// FIXME: decide what the best recovery path is if delivery fails (but no
|
|
430
440
|
// replay attack detected) after exchange has been marked complete
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/vc-delivery",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock Verifiable Credential Delivery",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@bedrock/mongodb": "^10.0.0",
|
|
57
57
|
"@bedrock/oauth2-verifier": "^1.0.0",
|
|
58
58
|
"@bedrock/service-agent": "^7.0.0",
|
|
59
|
-
"@bedrock/service-core": "^8.0.
|
|
59
|
+
"@bedrock/service-core": "^8.0.2",
|
|
60
60
|
"@bedrock/validation": "^7.1.0"
|
|
61
61
|
},
|
|
62
62
|
"directories": {
|
|
@@ -172,13 +172,12 @@ const step = {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
},
|
|
175
|
+
nextStep: {
|
|
176
|
+
type: 'string'
|
|
177
|
+
}
|
|
175
178
|
// FIXME: add jsonata template to convert VPR or
|
|
176
179
|
// `jwtDidProofRequest` to more variables to be
|
|
177
180
|
// used when issuing VCs
|
|
178
|
-
// FIXME: `nextStep` feature not yet implemented
|
|
179
|
-
// nextStep: {
|
|
180
|
-
// type: 'string'
|
|
181
|
-
// }
|
|
182
181
|
}
|
|
183
182
|
};
|
|
184
183
|
|