@bedrock/vc-delivery 7.11.1 → 7.11.3

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.
@@ -0,0 +1,556 @@
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.
292
+ await prepareStep?.({
293
+ workflow, exchange, step, receivedPresentation
294
+ });
295
+
296
+ // 4.3. If `receivedPresentation` is set, then call the
297
+ // `validateReceivedPresentation` sub-algorithm, passing `workflow`,
298
+ // `exchange`, `step`, and `receivedPresentation`.
299
+ if(receivedPresentation) {
300
+ await this._validateReceivedPresentation({
301
+ workflow, exchange, step, receivedPresentation,
302
+ verify: this.verify
303
+ });
304
+ }
305
+
306
+ // 4.4. If `receivedPresentationRequest` is set, call the
307
+ // `validateReceivedPresentationRequest` sub-algorithm, passing
308
+ // `exchange`, `step`, and `receivedPresentationRequest`.
309
+ if(receivedPresentationRequest) {
310
+ await this._validateReceivedPresentationRequest({
311
+ exchange, step, receivedPresentationRequest
312
+ });
313
+ }
314
+
315
+ // 4.5. Set `isInputRequired` to the result of calling
316
+ // `inputRequired({step, receivedPresentation})`.
317
+ const isInputRequired = await inputRequired?.({
318
+ workflow, exchange, step, receivedPresentation
319
+ }) ?? false;
320
+
321
+ // 4.6. If `isInputRequired` is true:
322
+ if(isInputRequired) {
323
+ // 4.6.1. If `response` is `null`, set it to an empty object.
324
+ if(!response) {
325
+ response = {};
326
+ }
327
+
328
+ // 4.6.2. If `step.verifiablePresentationRequest` is set, call
329
+ // the `createVerifiablePresentationRequest` sub-algorithm, passing
330
+ // `workflow`, `exchange`, `step`, and `response`.
331
+ if(step.verifiablePresentationRequest) {
332
+ await _createVerifiablePresentationRequest({
333
+ workflow, exchange, step, response
334
+ });
335
+ }
336
+
337
+ // 4.6.3. Save the exchange and return `response`.
338
+ await _updateExchange({workflow, exchange, meta, step});
339
+ return response;
340
+ }
341
+
342
+ // 4.7. Set `issueToClient` to `true` if `step.issueRequests` includes
343
+ // any issuer requests for VCs that are to be sent to the client
344
+ // (`issueRequest.result` is NOT set), otherwise set it to `false`.
345
+ const issueRequestsParams = getIssueRequestsParams({
346
+ workflow, exchange, step
347
+ });
348
+ const issueToClient = issueRequestsParams.some(p => !p.result);
349
+
350
+ // 4.8. If `step.verifiablePresentation` is set or `issueToClient` is
351
+ // `true`:
352
+ if(step.verifiablePresentation || issueToClient) {
353
+ // 4.8.1. If `response` is not `null`
354
+ if(response) {
355
+ // 4.8.1.1. If `response.verifiablePresentationRequest` is not set,
356
+ // set it to an empty object (to indicate that the exchange is
357
+ // not yet complete).
358
+ if(!response.verifiablePresentationRequest) {
359
+ response.verifiablePresentationRequest = {};
360
+ }
361
+ // 4.8.1.2. Save the exchange.
362
+ await _updateExchange({workflow, exchange, meta, step});
363
+ // 4.8.1.3. Return `response`.
364
+ return response;
365
+ }
366
+
367
+ // 4.8.2. Set `response` to an empty object.
368
+ response = {};
369
+
370
+ // 4.8.3. If `step.verifiablePresentation` is set, set
371
+ // `response.verifiablePresentation` to a copy of it, otherwise
372
+ // set `response.verifiablePresentation` to a new, empty,
373
+ // Verifiable Presentation (using VCDM 2.0 by default, but a custom
374
+ // configuration could specify another version).
375
+ response.verifiablePresentation =
376
+ structuredClone(step.verifiablePresentation) ??
377
+ createPresentation();
378
+ }
379
+
380
+ // issuance has been triggered
381
+ issuanceTriggered = true;
382
+
383
+ // 4.9. Perform every issue request (optionally in parallel), returning
384
+ // an error response to the client if any fails (note: implementations
385
+ // can optionally implement failure recovery or retry issue requests at
386
+ // their own discretion):
387
+ // 4.9.1. For each issue request where `result` is set to an exchange
388
+ // variable path or name, save the issued credential in the referenced
389
+ // exchange variable.
390
+ // 4.9.2. For each issue request where `result` is not specified, save
391
+ // the issued credential in `response.verifiablePresentation`, i.e.,
392
+ // for a VCDM presentation, append the issued credential to
393
+ // `response.verifiablePresentation.verifiableCredential`.
394
+ await this.issue({
395
+ workflow, exchange, step, issueRequestsParams,
396
+ verifiablePresentation: response?.verifiablePresentation
397
+ });
398
+
399
+ // 4.10. If `response.verifiablePresentation` is set and the step
400
+ // configuration indicates it should be signed, sign the presentation
401
+ // (e.g., by using a VCALM holder instance's `/presentations/create`
402
+ // endpoint).
403
+ // FIXME: implement
404
+ //if(response?.verifiablePresentation) {}
405
+
406
+ // 4.11. If `step.redirectUrl` is set:
407
+ if(step.redirectUrl) {
408
+ // 4.11.1. If `response` is `null` then set it to an empty object.
409
+ if(!response) {
410
+ response = {};
411
+ }
412
+ // 4.11.2. Set `response.redirectUrl` to `step.redirectUrl`.
413
+ response.redirectUrl = step.redirectUrl;
414
+ }
415
+
416
+ // 4.12. If `step.nextStep` is not set then set `exchange.state` to
417
+ // `complete`.
418
+ if(!step.nextStep) {
419
+ exchange.state = 'complete';
420
+ } else {
421
+ // 4.13. Otherwise, delete `exchange.variables.results[step.nextStep]`
422
+ // if it exists, and set `exchange.step` to `step.nextStep`.
423
+ delete exchange.variables.results[step.nextStep];
424
+ exchange.step = step.nextStep;
425
+ }
426
+
427
+ // 4.14. Save the exchange.
428
+ await _updateExchange({workflow, exchange, meta, step});
429
+
430
+ // 4.15. If `exchange.state` is `complete`, return `response` if it is
431
+ // not `null`, otherwise return an empty object.
432
+ if(exchange.state === 'complete') {
433
+ return response ?? {};
434
+ }
435
+
436
+ // 4.16. Set `receivedPresentation` to `null`.
437
+ receivedPresentation = null;
438
+ }
439
+ } catch(e) {
440
+ if(e.name === 'InvalidStateError') {
441
+ retryState.canRetry = !issuanceTriggered;
442
+ throw e;
443
+ }
444
+ // write last error if exchange hasn't been frequently updated
445
+ const {id: workflowId} = workflow;
446
+ const copy = {...exchange};
447
+ copy.sequence++;
448
+ copy.lastError = e;
449
+ await exchanges.setLastError({
450
+ workflowId, exchange: copy, lastUpdated: meta.updated
451
+ }).catch(error => logger.error(
452
+ 'Could not set last exchange error: ' + error.message, {error}));
453
+ await emitExchangeUpdated({workflow, exchange, step});
454
+ throw e;
455
+ }
456
+ }
457
+ }
458
+
459
+ async function _getStep({workflow, exchange}) {
460
+ const currentStep = exchange.step;
461
+
462
+ if(!currentStep) {
463
+ // return default empty step and set dummy stepname for exchange
464
+ exchange.step = 'initial';
465
+ return {};
466
+ }
467
+
468
+ const step = await evaluateExchangeStep({
469
+ workflow, exchange, stepName: currentStep
470
+ });
471
+
472
+ // if next step is the same as the current step, throw an error
473
+ if(step.nextStep === currentStep) {
474
+ throw new BedrockError('Cyclical step detected.', {
475
+ name: 'DataError',
476
+ details: {httpStatusCode: 500, public: true}
477
+ });
478
+ }
479
+
480
+ // if `step.nextStep` and `step.redirectUrl` and are both set, throw an error
481
+ if(step.nextStep && step.redirectUrl) {
482
+ throw new BedrockError(
483
+ 'Only the last step of a workflow can use "redirectUrl".', {
484
+ name: 'DataError',
485
+ details: {httpStatusCode: 500, public: true}
486
+ });
487
+ }
488
+
489
+ return step;
490
+ }
491
+
492
+ function _createTimeoutSignal({exchange, meta}) {
493
+ const expires = exchange.expires !== undefined ?
494
+ new Date(exchange.expires).getTime() :
495
+ new Date(meta.created + DEFAULT_TTL).getTime();
496
+ const timeout = Math.max(expires - Date.now(), 0);
497
+ const signal = AbortSignal.timeout(timeout);
498
+ return signal;
499
+ }
500
+
501
+ async function _createVerifiablePresentationRequest({
502
+ workflow, exchange, step, response
503
+ }) {
504
+ // 1. Set `response.verifiablePresentationRequest` to
505
+ // a copy of `step.verifiablePresentationRequest`.
506
+ response.verifiablePresentationRequest =
507
+ structuredClone(step.verifiablePresentationRequest);
508
+
509
+ // 2. If `step.createChallenge` is `false`, return.
510
+ if(!step.createChallenge) {
511
+ return;
512
+ }
513
+
514
+ // 3. Set `response.verifiablePresentationRequest.challenge` to an
515
+ // appropriate challenge value (e.g., use a configured VCALM verifier
516
+ // instance's `/challenges` endpoint).
517
+ /* Note: When setting the challenge, if the exchange on the initial step,
518
+ the challenge is the local exchange ID. Any subsequent step requires
519
+ a different challenge value. */
520
+ let challenge;
521
+ const isInitialStep = _isInitialStep({workflow, exchange});
522
+ if(isInitialStep) {
523
+ challenge = exchange.id;
524
+ } else {
525
+ // generate a new challenge using verifier API
526
+ ({challenge} = await createChallenge({workflow}));
527
+ }
528
+ response.verifiablePresentationRequest.challenge = challenge;
529
+
530
+ // 4. Set `exchange.variables.results[exchange.step]` to an object with the
531
+ // property `responsePresentationRequest` set to
532
+ // `response.verifiablePresentationRequest`.
533
+ exchange.variables.results[exchange.step] = {
534
+ responsePresentationRequest: response.verifiablePresentationRequest
535
+ };
536
+ }
537
+
538
+ function _isInitialStep({workflow, exchange}) {
539
+ return !workflow.initialStep || exchange.step === workflow.initialStep;
540
+ }
541
+
542
+ async function _updateExchange({workflow, exchange, meta, step}) {
543
+ try {
544
+ exchange.sequence++;
545
+ if(exchange.state === 'complete') {
546
+ await exchanges.complete({workflowId: workflow.id, exchange});
547
+ } else {
548
+ await exchanges.update({workflowId: workflow.id, exchange});
549
+ }
550
+ meta.updated = Date.now();
551
+ await emitExchangeUpdated({workflow, exchange, step});
552
+ } catch(e) {
553
+ exchange.sequence--;
554
+ throw e;
555
+ }
556
+ }
package/lib/helpers.js CHANGED
@@ -128,8 +128,9 @@ export async function evaluateExchangeStep({
128
128
  }) {
129
129
  let step = workflow.steps[stepName];
130
130
  if(step.stepTemplate) {
131
- step = await evaluateTemplate(
132
- {workflow, exchange, typedTemplate: step.stepTemplate});
131
+ step = await evaluateTemplate({
132
+ workflow, exchange, typedTemplate: step.stepTemplate
133
+ });
133
134
  }
134
135
  await validateStep({step});
135
136
  return step;
@@ -284,7 +285,7 @@ export function createVerifyOptions({
284
285
  // update `challenge`
285
286
  if(options.challenge === undefined) {
286
287
  options.challenge = expectedChallenge ??
287
- verifiablePresentationRequest.challenge ??
288
+ verifiablePresentationRequest?.challenge ??
288
289
  presentation?.proof?.challenge;
289
290
  }
290
291
 
@@ -455,3 +456,13 @@ export function validateVerifiablePresentation({schema, presentation}) {
455
456
  throw error;
456
457
  }
457
458
  }
459
+
460
+ export function validateVerifiablePresentationRequest({
461
+ schema, presentationRequest
462
+ }) {
463
+ const validate = compile({schema});
464
+ const {valid, error} = validate(presentationRequest);
465
+ if(!valid) {
466
+ throw error;
467
+ }
468
+ }