@bedrock/vc-delivery 7.11.2 → 7.12.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/ExchangeProcessor.js +577 -0
- package/lib/helpers.js +13 -77
- package/lib/inviteRequest/inviteRequest.js +22 -72
- package/lib/issue.js +35 -23
- package/lib/oid4/authorizationRequest.js +10 -2
- package/lib/oid4/authorizationResponse.js +20 -31
- package/lib/oid4/http.js +1 -1
- package/lib/oid4/oid4vci.js +59 -81
- package/lib/oid4/oid4vp.js +180 -230
- package/lib/vcapi.js +15 -230
- package/lib/verify.js +10 -8
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +8 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as exchanges from './storage/exchanges.js';
|
|
6
|
+
import {
|
|
7
|
+
buildPresentationFromResults,
|
|
8
|
+
buildVerifyPresentationResults,
|
|
9
|
+
emitExchangeUpdated,
|
|
10
|
+
evaluateExchangeStep,
|
|
11
|
+
validateVerifiablePresentation,
|
|
12
|
+
validateVerifiablePresentationRequest
|
|
13
|
+
} from './helpers.js';
|
|
14
|
+
import {createChallenge, verify as defaultVerify} from './verify.js';
|
|
15
|
+
import {issue as defaultIssue, getIssueRequestsParams} from './issue.js';
|
|
16
|
+
import {createPresentation} from '@digitalbazaar/vc';
|
|
17
|
+
import {logger} from './logger.js';
|
|
18
|
+
|
|
19
|
+
const {util: {BedrockError}} = bedrock;
|
|
20
|
+
|
|
21
|
+
// 15 minute default TTL for exchanges
|
|
22
|
+
const DEFAULT_TTL = 1000 * 60 * 60 * 15;
|
|
23
|
+
// maximum steps while looping
|
|
24
|
+
const MAXIMUM_STEP_COUNT = 100;
|
|
25
|
+
|
|
26
|
+
export class ExchangeProcessor {
|
|
27
|
+
/**
|
|
28
|
+
* Creates an `ExchangeProcessor`.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} options - The options to use.
|
|
31
|
+
* @param {object} options.workflow - The workflow.
|
|
32
|
+
* @param {object} options.exchangeRecord - The exchange record.
|
|
33
|
+
* @param {Function} [options.prepareStep] - The `prepareStep` handler.
|
|
34
|
+
* @param {Function} [options.inputRequired] - The `inputRequired` handler.
|
|
35
|
+
* @param {Function} [options.issue] - The `issue` handler.
|
|
36
|
+
* @param {Function} [options.verify] - The `verify` handler.
|
|
37
|
+
*
|
|
38
|
+
* @returns {ExchangeProcessor} An `ExchangeProcessor` instance.
|
|
39
|
+
*/
|
|
40
|
+
constructor({
|
|
41
|
+
workflow, exchangeRecord, prepareStep, inputRequired,
|
|
42
|
+
issue, verify
|
|
43
|
+
} = {}) {
|
|
44
|
+
this.workflow = workflow;
|
|
45
|
+
this.exchangeRecord = exchangeRecord;
|
|
46
|
+
this.prepareStep = prepareStep?.bind(this);
|
|
47
|
+
this.inputRequired = inputRequired?.bind(this);
|
|
48
|
+
this.issue = issue ?? defaultIssue.bind(this);
|
|
49
|
+
this.verify = verify ?? defaultVerify.bind(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Processes the exchange until either a response to be used with the
|
|
54
|
+
* exchange client is generated or input is required from the exchange client
|
|
55
|
+
* to continue.
|
|
56
|
+
*
|
|
57
|
+
* Note: An exchange is an particular running instantation of a workflow. The
|
|
58
|
+
* exchange processor will walkthrough the step(s) of the workflow to
|
|
59
|
+
* produce a respnose that can be used to communicate with the exchange
|
|
60
|
+
* client according to the protocol being used to execute the exchange. Not
|
|
61
|
+
* every step of a workflow produces a response and some steps produce a
|
|
62
|
+
* partial response, whereby the next step adds information to complete
|
|
63
|
+
* the response.
|
|
64
|
+
*
|
|
65
|
+
* It is up to the caller of `process()` to use the response generated to
|
|
66
|
+
* appropriately communicate with the exchange client according to the rules
|
|
67
|
+
* of the operating protocol.
|
|
68
|
+
*
|
|
69
|
+
* Sometimes errors may occur during an exchange. Not every possible error
|
|
70
|
+
* is recoverable. Some information that is generated on a workflow server
|
|
71
|
+
* may not be intended to be seen by a coordinator system that can poll the
|
|
72
|
+
* exchange state and some information may not be able to be regenerated
|
|
73
|
+
* without a new exchange.
|
|
74
|
+
*
|
|
75
|
+
* In rare cases, it is possible that a generated response will not reach the
|
|
76
|
+
* client due to a network connection error that is not perceived by the
|
|
77
|
+
* server. In this case, the expectation is that another exchange will have
|
|
78
|
+
* to be started to attempt the interaction again.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} options - The options to use.
|
|
81
|
+
* @param {object} [options.receivedPresentation] - A verifiable
|
|
82
|
+
* presentation received from the exchange client in the most recent
|
|
83
|
+
* protocol message of choice.
|
|
84
|
+
* @param {object} [options.receivedPresentationRequest] - A verifiable
|
|
85
|
+
* presentation request received from the exchange client in the most
|
|
86
|
+
* recent protocol message of choice.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Promise<object>} An object with processing information.
|
|
89
|
+
*/
|
|
90
|
+
async process({receivedPresentation, receivedPresentationRequest} = {}) {
|
|
91
|
+
const retryState = {};
|
|
92
|
+
while(true) {
|
|
93
|
+
try {
|
|
94
|
+
retryState.canRetry = false;
|
|
95
|
+
const response = await this._tryProcess({
|
|
96
|
+
receivedPresentation, receivedPresentationRequest, retryState
|
|
97
|
+
});
|
|
98
|
+
return response;
|
|
99
|
+
} catch(e) {
|
|
100
|
+
if(e.name === 'InvalidStateError' && retryState.canRetry) {
|
|
101
|
+
// get exchange record and loop to try again on `InvalidStateError`
|
|
102
|
+
const {workflow, exchangeRecord: {exchange}} = this;
|
|
103
|
+
this.exchangeRecord = await exchanges.get({
|
|
104
|
+
workflowId: workflow.id, id: exchange.id
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// rethrow in all other cases
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _validateReceivedPresentation({
|
|
115
|
+
workflow, exchange, step, receivedPresentation, verify
|
|
116
|
+
}) {
|
|
117
|
+
// 1. Set `isEnvelopedPresentation` to `true` if the received presentation's
|
|
118
|
+
// type is `EnvelopedVerifiablePresentation`, otherwise set it to `false`.
|
|
119
|
+
const isEnvelopedPresentation =
|
|
120
|
+
receivedPresentation?.type === 'EnvelopedVerifiablePresentation';
|
|
121
|
+
|
|
122
|
+
// 2. If `step.presentationSchema` is set and `isEnvelopedPresentation` is
|
|
123
|
+
// `false`, then use the presentation schema to validate
|
|
124
|
+
// `receivedPresentation`, throwing an error if validation fails.
|
|
125
|
+
const {presentationSchema} = step;
|
|
126
|
+
if(presentationSchema && !isEnvelopedPresentation) {
|
|
127
|
+
validateVerifiablePresentation({
|
|
128
|
+
schema: presentationSchema.jsonSchema,
|
|
129
|
+
presentation: receivedPresentation
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Set `expectedChallenge` to the local exchange ID if the exchange is on
|
|
134
|
+
// the initial step, otherwise set it to the `challenge` property's value in
|
|
135
|
+
// `exchange.variables.results[exchange.step].responsePresentationRequest`
|
|
136
|
+
// if it is set, otherwise set it to `undefined` (for subsequent steps,
|
|
137
|
+
// an implementation may also set the challenge in an
|
|
138
|
+
// implementation-specific way).
|
|
139
|
+
const isInitialStep = _isInitialStep({workflow, exchange});
|
|
140
|
+
const responsePresentationRequest = exchange.variables
|
|
141
|
+
.results?.[exchange.step]?.responsePresentationRequest;
|
|
142
|
+
const expectedChallenge = isInitialStep ?
|
|
143
|
+
exchange.id : responsePresentationRequest?.challenge;
|
|
144
|
+
|
|
145
|
+
// 4. Set `expectedDomain` to the `domain` property's value in
|
|
146
|
+
// `exchange.variables.results[exchange.step].responsePresentationRequest`
|
|
147
|
+
// if it is set, otherwise set it to the `domain` property's value in
|
|
148
|
+
// `step.verifiablePresentationRequest` if it is set, otherwise set it to
|
|
149
|
+
// the origin value for `workflow.id`.
|
|
150
|
+
const expectedDomain = responsePresentationRequest?.domain ??
|
|
151
|
+
step.verifiablePresentationRequest?.domain ?? new URL(workflow.id).origin;
|
|
152
|
+
|
|
153
|
+
// 5. Verify the received presentation (e.g., using a configured VCALM
|
|
154
|
+
// verifier instance):
|
|
155
|
+
const {
|
|
156
|
+
allowUnprotectedPresentation = false,
|
|
157
|
+
verifyPresentationResultSchema
|
|
158
|
+
} = step;
|
|
159
|
+
const verifyPresentationOptions = structuredClone(
|
|
160
|
+
step.verifyPresentationOptions ?? {});
|
|
161
|
+
const verifyResult = await verify({
|
|
162
|
+
workflow, exchange, step,
|
|
163
|
+
verifyPresentationOptions,
|
|
164
|
+
verifyPresentationResultSchema,
|
|
165
|
+
verifiablePresentationRequest: responsePresentationRequest ??
|
|
166
|
+
step.verifiablePresentationRequest,
|
|
167
|
+
presentation: receivedPresentation,
|
|
168
|
+
allowUnprotectedPresentation,
|
|
169
|
+
expectedChallenge,
|
|
170
|
+
expectedDomain
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// build unenveloped verifiable presentation from verification results
|
|
174
|
+
const verifiablePresentation = buildPresentationFromResults({
|
|
175
|
+
presentation: receivedPresentation,
|
|
176
|
+
verifyResult
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 4. If `step.presentationSchema` is set and `isEnvelopedPresentation` is
|
|
180
|
+
// `true`, then use the presentation schema to validate the unenveloped
|
|
181
|
+
// presentation returned from the verification process, throwing an error
|
|
182
|
+
// if validation fails.
|
|
183
|
+
if(presentationSchema && isEnvelopedPresentation) {
|
|
184
|
+
validateVerifiablePresentation({
|
|
185
|
+
schema: presentationSchema.jsonSchema,
|
|
186
|
+
presentation: verifiablePresentation
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// FIXME: check the VP against "allowedIssuer" in VPR, if provided
|
|
191
|
+
|
|
192
|
+
// 5. Set the verification results in
|
|
193
|
+
// `exchange.variables.results[exchange.step]`.
|
|
194
|
+
const {verificationMethod} = verifyResult;
|
|
195
|
+
exchange.variables.results[exchange.step] = {
|
|
196
|
+
...exchange.variables.results[exchange.step],
|
|
197
|
+
// common use case of DID Authentication; provide `did` for ease
|
|
198
|
+
// of use in templates and consistency with OID4VCI which only
|
|
199
|
+
// receives `did` not verification method nor VP
|
|
200
|
+
did: verificationMethod?.controller || null,
|
|
201
|
+
verificationMethod,
|
|
202
|
+
verifiablePresentation,
|
|
203
|
+
verifyPresentationResults: buildVerifyPresentationResults({verifyResult})
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async _validateReceivedPresentationRequest({
|
|
208
|
+
exchange, step, receivedPresentationRequest
|
|
209
|
+
}) {
|
|
210
|
+
// 1. If `step.presentationRequestSchema` is set, then use the presentation
|
|
211
|
+
// request schema to validate `receivedPresentationRequest`, throwing an
|
|
212
|
+
// error if validation fails.
|
|
213
|
+
if(step.presentationRequestSchema) {
|
|
214
|
+
validateVerifiablePresentationRequest({
|
|
215
|
+
schema: step.presentationRequestSchema.jsonSchema,
|
|
216
|
+
presentationRequest: receivedPresentationRequest
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 2. Set the presentation request in
|
|
221
|
+
// `exchange.variables.results[exchange.step].receivedPresentationRequest`
|
|
222
|
+
// so it can be used in subsequent steps.
|
|
223
|
+
exchange.variables.results[exchange.step] = {
|
|
224
|
+
...exchange.variables.results[exchange.step],
|
|
225
|
+
receivedPresentationRequest
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async _tryProcess({
|
|
230
|
+
receivedPresentation, receivedPresentationRequest, retryState
|
|
231
|
+
} = {}) {
|
|
232
|
+
const {workflow, exchangeRecord, prepareStep, inputRequired} = this;
|
|
233
|
+
const {exchange, meta} = exchangeRecord;
|
|
234
|
+
|
|
235
|
+
// initialize exchange results
|
|
236
|
+
if(!exchange.variables.results) {
|
|
237
|
+
exchange.variables.results = {};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 1. Initialize `step` and `response` to `null`.
|
|
241
|
+
let step = null;
|
|
242
|
+
let response = null;
|
|
243
|
+
|
|
244
|
+
// track whether issuance has been triggered yet to set retry capability
|
|
245
|
+
let issuanceTriggered = false;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// 2. If `exchange.state` is `complete` or `invalid`, throw a
|
|
249
|
+
// `NotAllowedError`.
|
|
250
|
+
if(exchange.state === 'complete' || exchange.state === 'invalid') {
|
|
251
|
+
throw new BedrockError(`Exchange is ${exchange.state}`, {
|
|
252
|
+
name: 'NotAllowedError',
|
|
253
|
+
details: {httpStatusCode: 403, public: true}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 3. If `exchange.state` is `pending`, set it to `active`.
|
|
258
|
+
if(exchange.state === 'pending') {
|
|
259
|
+
exchange.state = 'active';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 4. Continuously loop to process exchange steps, optionally saving any
|
|
263
|
+
// error thrown as `exchange.lastError`. Other algorithm steps will
|
|
264
|
+
// return out of the loop when a full response is generated, input from
|
|
265
|
+
// the exchange client is required, or the exchange times out. An
|
|
266
|
+
// implementation specific maximum step count be optionally enforced to
|
|
267
|
+
// prevent misconfigured workflows.
|
|
268
|
+
let stepCount = 0;
|
|
269
|
+
const signal = _createTimeoutSignal({exchange, meta});
|
|
270
|
+
while(true) {
|
|
271
|
+
if(signal.aborted) {
|
|
272
|
+
throw new BedrockError('Exchange has expired.', {
|
|
273
|
+
name: 'DataError',
|
|
274
|
+
details: {httpStatusCode: 500, public: true}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if(stepCount++ > MAXIMUM_STEP_COUNT) {
|
|
278
|
+
throw new BedrockError('Maximum step count exceeded.', {
|
|
279
|
+
name: 'DataError',
|
|
280
|
+
details: {httpStatusCode: 500, public: true}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 4.1. Set `step` to the current step (evaluating a step template as
|
|
285
|
+
// needed).
|
|
286
|
+
step = await _getStep({workflow, exchange});
|
|
287
|
+
|
|
288
|
+
// 4.2. Call subalgorithm `prepareStep`, passing `workflow`,
|
|
289
|
+
// `exchange`, `step`, `receivedPresentation`, and
|
|
290
|
+
// `receivedPresentationRequest` to perform any protocol-specific
|
|
291
|
+
// custom step preparation. If `prepareStep` returns a `prepareResult`
|
|
292
|
+
// with `receivedPresentation` and/or `receivedPresentationRequest` set,
|
|
293
|
+
// then update `receivedPresentation` and/or
|
|
294
|
+
// `receivedPresentationRequest` accordingly.
|
|
295
|
+
const prepareResult = await prepareStep?.({
|
|
296
|
+
workflow, exchange, step,
|
|
297
|
+
receivedPresentation, receivedPresentationRequest
|
|
298
|
+
});
|
|
299
|
+
if(prepareResult?.receivedPresentation !== undefined) {
|
|
300
|
+
receivedPresentation = prepareResult.receivedPresentation;
|
|
301
|
+
}
|
|
302
|
+
if(prepareResult?.receivedPresentationRequest) {
|
|
303
|
+
receivedPresentationRequest =
|
|
304
|
+
prepareResult.receivedPresentationRequest;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 4.3. If `receivedPresentation` is set, then call the
|
|
308
|
+
// `validateReceivedPresentation` sub-algorithm, passing `workflow`,
|
|
309
|
+
// `exchange`, `step`, and `receivedPresentation`.
|
|
310
|
+
if(receivedPresentation) {
|
|
311
|
+
await this._validateReceivedPresentation({
|
|
312
|
+
workflow, exchange, step, receivedPresentation,
|
|
313
|
+
verify: this.verify
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 4.4. If `receivedPresentationRequest` is set, call the
|
|
318
|
+
// `validateReceivedPresentationRequest` sub-algorithm, passing
|
|
319
|
+
// `exchange`, `step`, and `receivedPresentationRequest`.
|
|
320
|
+
if(receivedPresentationRequest) {
|
|
321
|
+
await this._validateReceivedPresentationRequest({
|
|
322
|
+
exchange, step, receivedPresentationRequest
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 4.5. If the implementation supports blocking callbacks that can
|
|
327
|
+
// return results to be added to exchange variables (or return errors),
|
|
328
|
+
// call the callback and store its results in
|
|
329
|
+
// `exchange.variables.results[exchange.step].callbackResults` or
|
|
330
|
+
// throw any error received.
|
|
331
|
+
// FIXME: to be implemented
|
|
332
|
+
|
|
333
|
+
// 4.6. Set `isInputRequired` to the result of calling
|
|
334
|
+
// `inputRequired({step, receivedPresentation})`.
|
|
335
|
+
const isInputRequired = await inputRequired?.({
|
|
336
|
+
workflow, exchange, step, receivedPresentation
|
|
337
|
+
}) ?? false;
|
|
338
|
+
|
|
339
|
+
// 4.7. If `isInputRequired` is true:
|
|
340
|
+
if(isInputRequired) {
|
|
341
|
+
// 4.7.1. If `response` is `null`, set it to an empty object.
|
|
342
|
+
if(!response) {
|
|
343
|
+
response = {};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 4.7.2. If `step.verifiablePresentationRequest` is set, call
|
|
347
|
+
// the `createVerifiablePresentationRequest` sub-algorithm, passing
|
|
348
|
+
// `workflow`, `exchange`, `step`, and `response`.
|
|
349
|
+
if(step.verifiablePresentationRequest) {
|
|
350
|
+
await _createVerifiablePresentationRequest({
|
|
351
|
+
workflow, exchange, step, response
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 4.7.3. Save the exchange (and call any non-blocking callback
|
|
356
|
+
// in the step) and return `response`.
|
|
357
|
+
await _updateExchange({workflow, exchange, meta, step});
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 4.8. Set `issueToClient` to `true` if `step.issueRequests` includes
|
|
362
|
+
// any issuer requests for VCs that are to be sent to the client
|
|
363
|
+
// (`issueRequest.result` is NOT set), otherwise set it to `false`.
|
|
364
|
+
const issueRequestsParams = getIssueRequestsParams({
|
|
365
|
+
workflow, exchange, step
|
|
366
|
+
});
|
|
367
|
+
const issueToClient = issueRequestsParams.some(p => !p.result);
|
|
368
|
+
|
|
369
|
+
// 4.9. If `step.verifiablePresentation` is set or `issueToClient` is
|
|
370
|
+
// `true`:
|
|
371
|
+
if(step.verifiablePresentation || issueToClient) {
|
|
372
|
+
// 4.9.1. If `response` is not `null`
|
|
373
|
+
if(response) {
|
|
374
|
+
// 4.9.1.1. If `response.verifiablePresentationRequest` is not set,
|
|
375
|
+
// set it to an empty object (to indicate that the exchange is
|
|
376
|
+
// not yet complete).
|
|
377
|
+
if(!response.verifiablePresentationRequest) {
|
|
378
|
+
response.verifiablePresentationRequest = {};
|
|
379
|
+
}
|
|
380
|
+
// 4.9.1.2. Save the exchange (and call any non-blocking callback
|
|
381
|
+
// in the step).
|
|
382
|
+
await _updateExchange({workflow, exchange, meta, step});
|
|
383
|
+
// 4.9.1.3. Return `response`.
|
|
384
|
+
return response;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 4.9.2. Set `response` to an empty object.
|
|
388
|
+
response = {};
|
|
389
|
+
|
|
390
|
+
// 4.9.3. If `step.verifiablePresentation` is set, set
|
|
391
|
+
// `response.verifiablePresentation` to a copy of it, otherwise
|
|
392
|
+
// set `response.verifiablePresentation` to a new, empty,
|
|
393
|
+
// Verifiable Presentation (using VCDM 2.0 by default, but a custom
|
|
394
|
+
// configuration could specify another version).
|
|
395
|
+
response.verifiablePresentation =
|
|
396
|
+
structuredClone(step.verifiablePresentation) ??
|
|
397
|
+
createPresentation();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// issuance has been triggered
|
|
401
|
+
issuanceTriggered = true;
|
|
402
|
+
|
|
403
|
+
// 4.10. Perform every issue request (optionally in parallel),
|
|
404
|
+
// returning an error response to the client if any fails (note:
|
|
405
|
+
// implementations can optionally implement failure recovery or retry
|
|
406
|
+
// issue requests at their own discretion):
|
|
407
|
+
// 4.10.1. For each issue request where `result` is set to an exchange
|
|
408
|
+
// variable path or name, save the issued credential in the referenced
|
|
409
|
+
// exchange variable.
|
|
410
|
+
// 4.10.2. For each issue request where `result` is not specified, save
|
|
411
|
+
// the issued credential in `response.verifiablePresentation`, i.e.,
|
|
412
|
+
// for a VCDM presentation, append the issued credential to
|
|
413
|
+
// `response.verifiablePresentation.verifiableCredential`.
|
|
414
|
+
await this.issue({
|
|
415
|
+
workflow, exchange, step, issueRequestsParams,
|
|
416
|
+
verifiablePresentation: response?.verifiablePresentation
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// 4.11. If `response.verifiablePresentation` is set and the step
|
|
420
|
+
// configuration indicates it should be signed, sign the presentation
|
|
421
|
+
// (e.g., by using a VCALM holder instance's `/presentations/create`
|
|
422
|
+
// endpoint).
|
|
423
|
+
// FIXME: implement
|
|
424
|
+
//if(response?.verifiablePresentation) {}
|
|
425
|
+
|
|
426
|
+
// 4.12. If `step.redirectUrl` is set:
|
|
427
|
+
if(step.redirectUrl) {
|
|
428
|
+
// 4.12.1. If `response` is `null` then set it to an empty object.
|
|
429
|
+
if(!response) {
|
|
430
|
+
response = {};
|
|
431
|
+
}
|
|
432
|
+
// 4.12.2. Set `response.redirectUrl` to `step.redirectUrl`.
|
|
433
|
+
response.redirectUrl = step.redirectUrl;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 4.13. If `step.nextStep` is not set then set `exchange.state` to
|
|
437
|
+
// `complete`.
|
|
438
|
+
if(!step.nextStep) {
|
|
439
|
+
exchange.state = 'complete';
|
|
440
|
+
} else {
|
|
441
|
+
// 4.14. Otherwise, delete `exchange.variables.results[step.nextStep]`
|
|
442
|
+
// if it exists, and set `exchange.step` to `step.nextStep`.
|
|
443
|
+
delete exchange.variables.results[step.nextStep];
|
|
444
|
+
exchange.step = step.nextStep;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 4.15. Save the exchange (and call any non-blocking callback in
|
|
448
|
+
// the step).
|
|
449
|
+
await _updateExchange({workflow, exchange, meta, step});
|
|
450
|
+
|
|
451
|
+
// 4.16. If `exchange.state` is `complete`, return `response` if it is
|
|
452
|
+
// not `null`, otherwise return an empty object.
|
|
453
|
+
if(exchange.state === 'complete') {
|
|
454
|
+
return response ?? {};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 4.17. Set `receivedPresentation` to `null`.
|
|
458
|
+
receivedPresentation = null;
|
|
459
|
+
}
|
|
460
|
+
} catch(e) {
|
|
461
|
+
if(e.name === 'InvalidStateError') {
|
|
462
|
+
retryState.canRetry = !issuanceTriggered;
|
|
463
|
+
throw e;
|
|
464
|
+
}
|
|
465
|
+
// write last error if exchange hasn't been frequently updated
|
|
466
|
+
const {id: workflowId} = workflow;
|
|
467
|
+
const copy = {...exchange};
|
|
468
|
+
copy.sequence++;
|
|
469
|
+
copy.lastError = e;
|
|
470
|
+
await exchanges.setLastError({
|
|
471
|
+
workflowId, exchange: copy, lastUpdated: meta.updated
|
|
472
|
+
}).catch(error => logger.error(
|
|
473
|
+
'Could not set last exchange error: ' + error.message, {error}));
|
|
474
|
+
await emitExchangeUpdated({workflow, exchange, step});
|
|
475
|
+
throw e;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function _getStep({workflow, exchange}) {
|
|
481
|
+
const currentStep = exchange.step;
|
|
482
|
+
|
|
483
|
+
if(!currentStep) {
|
|
484
|
+
// return default empty step and set dummy stepname for exchange
|
|
485
|
+
exchange.step = 'initial';
|
|
486
|
+
return {};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const step = await evaluateExchangeStep({
|
|
490
|
+
workflow, exchange, stepName: currentStep
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// if next step is the same as the current step, throw an error
|
|
494
|
+
if(step.nextStep === currentStep) {
|
|
495
|
+
throw new BedrockError('Cyclical step detected.', {
|
|
496
|
+
name: 'DataError',
|
|
497
|
+
details: {httpStatusCode: 500, public: true}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// if `step.nextStep` and `step.redirectUrl` and are both set, throw an error
|
|
502
|
+
if(step.nextStep && step.redirectUrl) {
|
|
503
|
+
throw new BedrockError(
|
|
504
|
+
'Only the last step of a workflow can use "redirectUrl".', {
|
|
505
|
+
name: 'DataError',
|
|
506
|
+
details: {httpStatusCode: 500, public: true}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return step;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function _createTimeoutSignal({exchange, meta}) {
|
|
514
|
+
const expires = exchange.expires !== undefined ?
|
|
515
|
+
new Date(exchange.expires).getTime() :
|
|
516
|
+
new Date(meta.created + DEFAULT_TTL).getTime();
|
|
517
|
+
const timeout = Math.max(expires - Date.now(), 0);
|
|
518
|
+
const signal = AbortSignal.timeout(timeout);
|
|
519
|
+
return signal;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function _createVerifiablePresentationRequest({
|
|
523
|
+
workflow, exchange, step, response
|
|
524
|
+
}) {
|
|
525
|
+
// 1. Set `response.verifiablePresentationRequest` to
|
|
526
|
+
// a copy of `step.verifiablePresentationRequest`.
|
|
527
|
+
response.verifiablePresentationRequest =
|
|
528
|
+
structuredClone(step.verifiablePresentationRequest);
|
|
529
|
+
|
|
530
|
+
// 2. If `step.createChallenge` is `false`, return.
|
|
531
|
+
if(!step.createChallenge) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 3. Set `response.verifiablePresentationRequest.challenge` to an
|
|
536
|
+
// appropriate challenge value (e.g., use a configured VCALM verifier
|
|
537
|
+
// instance's `/challenges` endpoint).
|
|
538
|
+
/* Note: When setting the challenge, if the exchange on the initial step,
|
|
539
|
+
the challenge is the local exchange ID. Any subsequent step requires
|
|
540
|
+
a different challenge value. */
|
|
541
|
+
let challenge;
|
|
542
|
+
const isInitialStep = _isInitialStep({workflow, exchange});
|
|
543
|
+
if(isInitialStep) {
|
|
544
|
+
challenge = exchange.id;
|
|
545
|
+
} else {
|
|
546
|
+
// generate a new challenge using verifier API
|
|
547
|
+
({challenge} = await createChallenge({workflow}));
|
|
548
|
+
}
|
|
549
|
+
response.verifiablePresentationRequest.challenge = challenge;
|
|
550
|
+
|
|
551
|
+
// 4. Set `exchange.variables.results[exchange.step]` to an object with the
|
|
552
|
+
// property `responsePresentationRequest` set to
|
|
553
|
+
// `response.verifiablePresentationRequest`.
|
|
554
|
+
exchange.variables.results[exchange.step] = {
|
|
555
|
+
responsePresentationRequest: response.verifiablePresentationRequest
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function _isInitialStep({workflow, exchange}) {
|
|
560
|
+
return !workflow.initialStep || exchange.step === workflow.initialStep;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function _updateExchange({workflow, exchange, meta, step}) {
|
|
564
|
+
try {
|
|
565
|
+
exchange.sequence++;
|
|
566
|
+
if(exchange.state === 'complete') {
|
|
567
|
+
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
568
|
+
} else {
|
|
569
|
+
await exchanges.update({workflowId: workflow.id, exchange});
|
|
570
|
+
}
|
|
571
|
+
meta.updated = Date.now();
|
|
572
|
+
await emitExchangeUpdated({workflow, exchange, step});
|
|
573
|
+
} catch(e) {
|
|
574
|
+
exchange.sequence--;
|
|
575
|
+
throw e;
|
|
576
|
+
}
|
|
577
|
+
}
|