@bedrock/vc-delivery 5.0.0 → 5.0.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/lib/helpers.js CHANGED
@@ -2,15 +2,15 @@
2
2
  * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
+ import * as vcjwt from './vcjwt.js';
5
6
  import {decodeId, generateId} from 'bnid';
6
- import {decodeJwt} from 'jose';
7
7
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
8
8
  import {httpsAgent} from '@bedrock/https-agent';
9
9
  import jsonata from 'jsonata';
10
10
  import {serviceAgents} from '@bedrock/service-agent';
11
11
  import {ZcapClient} from '@digitalbazaar/ezcap';
12
12
 
13
- const {config} = bedrock;
13
+ const {config, util: {BedrockError}} = bedrock;
14
14
 
15
15
  export async function evaluateTemplate({
16
16
  workflow, exchange, typedTemplate
@@ -104,29 +104,66 @@ export function decodeLocalId({localId} = {}) {
104
104
  }));
105
105
  }
106
106
 
107
- export async function unenvelopeCredential({envelopedCredential} = {}) {
108
- let credential;
109
- const {id} = envelopedCredential;
110
- if(id?.startsWith('data:application/jwt,')) {
111
- const format = 'application/jwt';
112
- const jwt = id.slice('data:application/jwt,'.length);
113
- const claimset = decodeJwt(jwt);
114
- // FIXME: perform various field mappings as needed
115
- console.log('VC-JWT claimset', credential);
116
- return {credential: claimset.vc, format};
107
+ export async function unenvelopeCredential({
108
+ envelopedCredential, format
109
+ } = {}) {
110
+ const result = _getEnvelope({envelope: envelopedCredential, format});
111
+
112
+ // only supported format is VC-JWT at this time
113
+ const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
114
+ return {credential, ...result};
115
+ }
116
+
117
+ export async function unenvelopePresentation({
118
+ envelopedPresentation, format
119
+ } = {}) {
120
+ const result = _getEnvelope({envelope: envelopedPresentation, format});
121
+
122
+ // only supported format is VC-JWT at this time
123
+ const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});
124
+
125
+ // unenvelope any VCs in the presentation
126
+ let {verifiableCredential = []} = presentation;
127
+ if(!Array.isArray(verifiableCredential)) {
128
+ verifiableCredential = [verifiableCredential];
117
129
  }
118
- throw new Error('Not implemented.');
130
+ if(verifiableCredential.length > 0) {
131
+ presentation.verifiableCredential = await Promise.all(
132
+ verifiableCredential.map(async vc => {
133
+ if(vc?.type !== 'EnvelopedVerifiableCredential') {
134
+ return vc;
135
+ }
136
+ const {credential} = await unenvelopeCredential({
137
+ envelopedCredential: vc
138
+ });
139
+ return credential;
140
+ }));
141
+ }
142
+ return {presentation, ...result};
119
143
  }
120
144
 
121
- export async function unenvelopePresentation({envelopedPresentation} = {}) {
122
- const {id} = envelopedPresentation;
123
- if(id?.startsWith('data:application/jwt,')) {
124
- const format = 'application/jwt';
125
- const jwt = id.slice('data:application/jwt,'.length);
126
- const claimset = decodeJwt(jwt);
127
- // FIXME: perform various field mappings as needed
128
- console.log('VC-JWT claimset', claimset);
129
- return {presentation: claimset.vp, format};
145
+ function _getEnvelope({envelope, format}) {
146
+ const isString = typeof envelope === 'string';
147
+ if(isString) {
148
+ // supported formats
149
+ if(format === 'application/jwt' || format === 'jwt_vc_json-ld') {
150
+ format = 'application/jwt';
151
+ }
152
+ } else {
153
+ const {id} = envelope;
154
+ if(id?.startsWith('data:application/jwt,')) {
155
+ format = 'application/jwt';
156
+ envelope = id.slice('data:application/jwt,'.length);
157
+ }
158
+ }
159
+
160
+ if(format === 'application/jwt' && envelope !== undefined) {
161
+ return {envelope, format};
130
162
  }
131
- throw new Error('Not implemented.');
163
+
164
+ throw new BedrockError(
165
+ `Unsupported credential or presentation envelope format "${format}".`, {
166
+ name: 'NotSupportedError',
167
+ details: {httpStatusCode: 400, public: true}
168
+ });
132
169
  }
package/lib/openId.js CHANGED
@@ -6,7 +6,9 @@ import * as exchanges from './exchanges.js';
6
6
  import {
7
7
  compile, createValidateMiddleware as validate
8
8
  } from '@bedrock/validation';
9
- import {evaluateTemplate, getWorkflowIssuerInstances} from './helpers.js';
9
+ import {
10
+ evaluateTemplate, getWorkflowIssuerInstances, unenvelopePresentation
11
+ } from './helpers.js';
10
12
  import {importJWK, SignJWT} from 'jose';
11
13
  import {
12
14
  openIdAuthorizationResponseBody,
@@ -57,6 +59,8 @@ instantiating a new authorization server instance per VC exchange. */
57
59
  const PRE_AUTH_GRANT_TYPE =
58
60
  'urn:ietf:params:oauth:grant-type:pre-authorized_code';
59
61
 
62
+ const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
63
+
60
64
  // creates OID4VCI Authorization Server + Credential Delivery Server
61
65
  // endpoints for each individual exchange
62
66
  export async function createRoutes({
@@ -379,14 +383,33 @@ export async function createRoutes({
379
383
  const {vp_token, presentation_submission} = req.body;
380
384
 
381
385
  // JSON parse and validate `vp_token` and `presentation_submission`
382
- const presentation = _jsonParse(vp_token, 'vp_token');
386
+ let presentation = _jsonParse(vp_token, 'vp_token');
383
387
  const presentationSubmission = _jsonParse(
384
388
  presentation_submission, 'presentation_submission');
385
389
  _validate(validatePresentationSubmission, presentationSubmission);
386
- _validate(validatePresentation, presentation);
387
-
388
- const result = await _processAuthorizationResponse(
389
- {req, presentation, presentationSubmission});
390
+ let envelope;
391
+ if(typeof presentation === 'string') {
392
+ // handle enveloped presentation
393
+ const {
394
+ envelope: raw, presentation: contents, format
395
+ } = await unenvelopePresentation({
396
+ envelopedPresentation: presentation,
397
+ // FIXME: check presentationSubmission for VP format
398
+ format: 'jwt_vc_json-ld'
399
+ });
400
+ _validate(validatePresentation, contents);
401
+ presentation = {
402
+ '@context': VC_CONTEXT_2,
403
+ id: `data:${format},${raw}`,
404
+ type: 'EnvelopedVerifiablePresentation'
405
+ };
406
+ envelope = {raw, contents, format};
407
+ } else {
408
+ _validate(validatePresentation, presentation);
409
+ }
410
+ const result = await _processAuthorizationResponse({
411
+ req, presentation, envelope, presentationSubmission
412
+ });
390
413
  res.json(result);
391
414
  }));
392
415
 
@@ -906,7 +929,7 @@ function _matchCredentialRequest(expected, cr) {
906
929
  }
907
930
 
908
931
  async function _processAuthorizationResponse({
909
- req, presentation, presentationSubmission
932
+ req, presentation, envelope, presentationSubmission
910
933
  }) {
911
934
  const {config: workflow} = req.serviceObject;
912
935
  const exchangeRecord = await req.getExchange();
@@ -917,17 +940,17 @@ async function _processAuthorizationResponse({
917
940
  const {authorizationRequest, step} = arRequest;
918
941
  ({exchange} = arRequest);
919
942
 
920
- // FIXME: if the VP is enveloped, remove the envelope to validate or
921
- // run validation code after verification if necessary
922
-
923
943
  // FIXME: check the VP against the presentation submission if requested
924
944
  // FIXME: check the VP against "trustedIssuer" in VPR, if provided
925
945
  const {presentationSchema} = step;
926
946
  if(presentationSchema) {
927
- // validate the received VP
947
+ // if the VP is enveloped, validate the contents of the envelope
948
+ const toValidate = envelope ? envelope.contents : presentation;
949
+
950
+ // validate the received VP / envelope contents
928
951
  const {jsonSchema: schema} = presentationSchema;
929
952
  const validate = compile({schema});
930
- const {valid, error} = validate(presentation);
953
+ const {valid, error} = validate(toValidate);
931
954
  if(!valid) {
932
955
  throw error;
933
956
  }
@@ -937,20 +960,21 @@ async function _processAuthorizationResponse({
937
960
  const {verifiablePresentationRequest} = await oid4vp.toVpr(
938
961
  {authorizationRequest});
939
962
  const {allowUnprotectedPresentation = false} = step;
940
- const {verificationMethod} = await verify({
963
+ const verifyResult = await verify({
941
964
  workflow,
942
965
  verifiablePresentationRequest,
943
966
  presentation,
944
967
  allowUnprotectedPresentation,
945
968
  expectedChallenge: authorizationRequest.nonce
946
969
  });
970
+ const {verificationMethod} = verifyResult;
947
971
 
948
972
  // store VP results in variables associated with current step
949
973
  const currentStep = exchange.step;
950
974
  if(!exchange.variables.results) {
951
975
  exchange.variables.results = {};
952
976
  }
953
- exchange.variables.results[currentStep] = {
977
+ const results = {
954
978
  // common use case of DID Authentication; provide `did` for ease
955
979
  // of use in template
956
980
  did: verificationMethod?.controller || null,
@@ -961,6 +985,13 @@ async function _processAuthorizationResponse({
961
985
  presentationSubmission
962
986
  }
963
987
  };
988
+ if(envelope) {
989
+ // normalize VP from inside envelope to `verifiablePresentation`
990
+ results.envelopedPresentation = presentation;
991
+ results.verifiablePresentation = verifyResult
992
+ .presentationResult.presentation;
993
+ }
994
+ exchange.variables.results[currentStep] = results;
964
995
  exchange.sequence++;
965
996
 
966
997
  // if there is something to issue, update exchange, do not complete it
package/lib/vcapi.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as exchanges from './exchanges.js';
6
6
  import {createChallenge as _createChallenge, verify} from './verify.js';
7
+ import {evaluateTemplate, unenvelopePresentation} from './helpers.js';
7
8
  import {compile} from '@bedrock/validation';
8
- import {evaluateTemplate} from './helpers.js';
9
9
  import {issue} from './issue.js';
10
10
  import {klona} from 'klona';
11
11
  import {logger} from './logger.js';
@@ -96,15 +96,22 @@ export async function processExchange({req, res, workflow, exchange}) {
96
96
  return;
97
97
  }
98
98
 
99
- // FIXME: if the VP is enveloped, remove the envelope to validate or
100
- // run validation code after verification if necessary
101
-
102
99
  const {presentationSchema} = step;
103
100
  if(presentationSchema) {
101
+ // if the VP is enveloped, get the presentation from the envelope
102
+ let presentation;
103
+ if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
104
+ ({presentation} = await unenvelopePresentation({
105
+ envelopedPresentation: receivedPresentation
106
+ }));
107
+ } else {
108
+ presentation = receivedPresentation;
109
+ }
110
+
104
111
  // validate the received VP
105
112
  const {jsonSchema: schema} = presentationSchema;
106
113
  const validate = compile({schema});
107
- const {valid, error} = validate(receivedPresentation);
114
+ const {valid, error} = validate(presentation);
108
115
  if(!valid) {
109
116
  throw error;
110
117
  }
package/lib/vcjwt.js ADDED
@@ -0,0 +1,375 @@
1
+ /*!
2
+ * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import {decodeJwt} from 'jose';
6
+
7
+ const {util: {BedrockError}} = bedrock;
8
+
9
+ const VC_CONTEXT_1 = 'https://www.w3.org/2018/credentials/v1';
10
+ const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';
11
+
12
+ export function decodeVCJWTCredential({jwt} = {}) {
13
+ const payload = decodeJwt(jwt);
14
+
15
+ /* Example:
16
+ {
17
+ "alg": <signer.algorithm>,
18
+ "kid": <signer.id>
19
+ }.
20
+ {
21
+ "iss": <verifiableCredential.issuer>,
22
+ "jti": <verifiableCredential.id>
23
+ "sub": <verifiableCredential.credentialSubject>
24
+ "nbf": <verifiableCredential.[issuanceDate | validFrom]>
25
+ "exp": <verifiableCredential.[expirationDate | validUntil]>
26
+ "vc": <verifiableCredential>
27
+ }
28
+ */
29
+ const {vc} = payload;
30
+ if(!(vc && typeof vc === 'object')) {
31
+ throw new BedrockError('JWT validation failed.', {
32
+ name: 'DataError',
33
+ details: {
34
+ httpStatusCode: 400,
35
+ public: true,
36
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
37
+ reason: 'missing or unexpected "vc" claim value.',
38
+ claim: 'vc'
39
+ }
40
+ });
41
+ }
42
+
43
+ let {'@context': context = []} = vc;
44
+ if(!Array.isArray(context)) {
45
+ context = [context];
46
+ }
47
+ const isVersion1 = context.includes(VC_CONTEXT_1);
48
+ const isVersion2 = context.includes(VC_CONTEXT_2);
49
+ if(!(isVersion1 ^ isVersion2)) {
50
+ throw new BedrockError(
51
+ 'Verifiable credential is neither version "1.x" nor "2.x".', {
52
+ name: 'DataError',
53
+ details: {
54
+ httpStatusCode: 400,
55
+ public: true
56
+ }
57
+ });
58
+ }
59
+
60
+ const credential = {...vc};
61
+ const {iss, jti, sub, nbf, exp} = payload;
62
+
63
+ // inject `issuer` value
64
+ if(vc.issuer === undefined) {
65
+ vc.issuer = iss;
66
+ } else if(vc.issuer && typeof vc.issuer === 'object' &&
67
+ vc.issuer.id === undefined) {
68
+ vc.issuer = {id: iss, ...vc.issuer};
69
+ } else if(iss !== vc.issuer && iss !== vc.issuer?.id) {
70
+ throw new BedrockError(
71
+ 'VC-JWT "iss" claim does not equal nor does it exclusively ' +
72
+ 'provide verifiable credential "issuer" / "issuer.id".', {
73
+ name: 'DataError',
74
+ details: {
75
+ httpStatusCode: 400,
76
+ public: true
77
+ }
78
+ });
79
+ }
80
+
81
+ if(jti !== undefined && jti !== vc.id) {
82
+ // inject `id` value
83
+ if(vc.id === undefined) {
84
+ vc.id = jti;
85
+ } else {
86
+ throw new BedrockError(
87
+ 'VC-JWT "jti" claim does not equal nor does it exclusively ' +
88
+ 'provide verifiable credential "id".', {
89
+ name: 'DataError',
90
+ details: {
91
+ httpStatusCode: 400,
92
+ public: true
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ if(sub !== undefined && sub !== vc.credentialSubject?.id) {
99
+ // inject `credentialSubject.id` value
100
+ if(!vc.credentialSubject) {
101
+ throw new BedrockError(
102
+ 'Verifiable credential has no "credentialSubject".', {
103
+ name: 'DataError',
104
+ details: {
105
+ httpStatusCode: 400,
106
+ public: true
107
+ }
108
+ });
109
+ }
110
+ if(Array.isArray(vc.credentialSubject)) {
111
+ throw new BedrockError(
112
+ 'Verifiable credential has multiple credential subjects, which is ' +
113
+ 'not supported in VC-JWT.', {
114
+ name: 'DataError',
115
+ details: {
116
+ httpStatusCode: 400,
117
+ public: true
118
+ }
119
+ });
120
+ }
121
+ if(vc.credentialSubject?.id === undefined) {
122
+ vc.credentialSubject = {id: sub, ...vc.credentialSubject};
123
+ } else {
124
+ throw new BedrockError(
125
+ 'VC-JWT "sub" claim does not equal nor does it exclusively ' +
126
+ 'provide verifiable credential "credentialSubject.id".', {
127
+ name: 'DataError',
128
+ details: {
129
+ httpStatusCode: 400,
130
+ public: true
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ if(nbf === undefined && isVersion1) {
137
+ throw new BedrockError('JWT validation failed.', {
138
+ name: 'DataError',
139
+ details: {
140
+ httpStatusCode: 400,
141
+ public: true,
142
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
143
+ reason: 'missing "nbf" claim value.',
144
+ claim: 'nbf'
145
+ }
146
+ });
147
+ }
148
+
149
+ if(nbf !== undefined) {
150
+ // fuzzy convert `nbf` into `issuanceDate` / `validFrom`, only require
151
+ // second-level precision
152
+ const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
153
+ const dateProperty = isVersion1 ? 'issuanceDate' : 'validFrom';
154
+ // inject dateProperty value
155
+ if(vc[dateProperty] === undefined) {
156
+ vc[dateProperty] = dateString + 'Z';
157
+ } else if(!(vc[dateProperty].startsWith(dateString) &&
158
+ vc[dateProperty].endsWith('Z'))) {
159
+ throw new BedrockError(
160
+ 'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
161
+ `verifiable credential "${dateProperty}".`, {
162
+ name: 'DataError',
163
+ details: {
164
+ httpStatusCode: 400,
165
+ public: true
166
+ }
167
+ });
168
+ }
169
+ }
170
+
171
+ if(exp !== undefined) {
172
+ // fuzzy convert `exp` into `expirationDate` / `validUntil`, only require
173
+ // second-level precision
174
+ const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
175
+ const dateProperty = isVersion1 ? 'expirationDate' : 'validUntil';
176
+ // inject dateProperty value
177
+ if(vc[dateProperty] === undefined) {
178
+ vc[dateProperty] = dateString + 'Z';
179
+ } else if(!(vc[dateProperty].startsWith(dateString) &&
180
+ vc[dateProperty].endsWith('Z'))) {
181
+ throw new BedrockError(
182
+ 'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
183
+ `verifiable credential "${dateProperty}".`, {
184
+ name: 'DataError',
185
+ details: {
186
+ httpStatusCode: 400,
187
+ public: true
188
+ }
189
+ });
190
+ }
191
+ }
192
+
193
+ return credential;
194
+ }
195
+
196
+ export function decodeVCJWTPresentation({jwt, challenge} = {}) {
197
+ /* Example:
198
+ {
199
+ "alg": <signer.algorithm>,
200
+ "kid": <signer.id>
201
+ }.
202
+ {
203
+ "iss": <verifiablePresentation.holder>,
204
+ "aud": <verifiablePresentation.domain>,
205
+ "nonce": <verifiablePresentation.nonce>,
206
+ "jti": <verifiablePresentation.id>
207
+ "nbf": <verifiablePresentation.[validFrom]>
208
+ "exp": <verifiablePresentation.[validUntil]>
209
+ "vp": <verifiablePresentation>
210
+ }
211
+ */
212
+ const payload = decodeJwt(jwt);
213
+
214
+ const {vp} = payload;
215
+ if(!(vp && typeof vp === 'object')) {
216
+ throw new BedrockError('JWT validation failed.', {
217
+ name: 'DataError',
218
+ details: {
219
+ httpStatusCode: 400,
220
+ public: true,
221
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
222
+ reason: 'missing or unexpected "vp" claim value.',
223
+ claim: 'vp'
224
+ }
225
+ });
226
+ }
227
+
228
+ let {'@context': context = []} = vp;
229
+ if(!Array.isArray(context)) {
230
+ context = [context];
231
+ }
232
+ const isVersion1 = context.includes(VC_CONTEXT_1);
233
+ const isVersion2 = context.includes(VC_CONTEXT_2);
234
+ if(!(isVersion1 ^ isVersion2)) {
235
+ throw new BedrockError(
236
+ 'Verifiable presentation is not either version "1.x" or "2.x".', {
237
+ name: 'DataError',
238
+ details: {
239
+ httpStatusCode: 400,
240
+ public: true
241
+ }
242
+ });
243
+ }
244
+
245
+ const presentation = {...vp};
246
+ const {iss, nonce, jti, nbf, exp} = payload;
247
+
248
+ // inject `holder` value
249
+ if(vp.holder === undefined) {
250
+ vp.holder = iss;
251
+ } else if(vp.holder && typeof vp.holder === 'object' &&
252
+ vp.holder.id === undefined) {
253
+ vp.holder = {id: iss, ...vp.holder};
254
+ } else if(iss !== vp.holder && iss !== vp.holder?.id) {
255
+ throw new BedrockError(
256
+ 'VC-JWT "iss" claim does not equal nor does it exclusively ' +
257
+ 'provide verifiable presentation "holder" / "holder.id".', {
258
+ name: 'DataError',
259
+ details: {
260
+ httpStatusCode: 400,
261
+ public: true
262
+ }
263
+ });
264
+ }
265
+
266
+ if(jti !== undefined && jti !== vp.id) {
267
+ // inject `id` value
268
+ if(vp.id === undefined) {
269
+ vp.id = jti;
270
+ } else {
271
+ throw new BedrockError(
272
+ 'VC-JWT "jti" claim does not equal nor does it exclusively ' +
273
+ 'provide verifiable presentation "id".', {
274
+ name: 'DataError',
275
+ details: {
276
+ httpStatusCode: 400,
277
+ public: true
278
+ }
279
+ });
280
+ }
281
+ }
282
+
283
+ // version 1.x VPs do not support `validFrom`/`validUntil`
284
+ if(nbf !== undefined && isVersion2) {
285
+ // fuzzy convert `nbf` into `validFrom`, only require
286
+ // second-level precision
287
+ const dateString = new Date(nbf * 1000).toISOString().slice(0, -5);
288
+
289
+ // inject `validFrom` value
290
+ if(vp.validFrom === undefined) {
291
+ vp.validFrom = dateString + 'Z';
292
+ } else if(!(vp.validFrom?.startsWith(dateString) &&
293
+ vp.validFrom.endsWith('Z'))) {
294
+ throw new BedrockError(
295
+ 'VC-JWT "nbf" claim does not equal nor does it exclusively provide ' +
296
+ 'verifiable presentation "validFrom".', {
297
+ name: 'DataError',
298
+ details: {
299
+ httpStatusCode: 400,
300
+ public: true
301
+ }
302
+ });
303
+ }
304
+ }
305
+ if(exp !== undefined && isVersion2) {
306
+ // fuzzy convert `exp` into `validUntil`, only require
307
+ // second-level precision
308
+ const dateString = new Date(exp * 1000).toISOString().slice(0, -5);
309
+
310
+ // inject `validUntil` value
311
+ if(vp.validUntil === undefined) {
312
+ vp.validUntil = dateString + 'Z';
313
+ } else if(!(vp.validUntil?.startsWith(dateString) &&
314
+ vp.validUntil?.endsWith('Z'))) {
315
+ throw new BedrockError(
316
+ 'VC-JWT "exp" claim does not equal nor does it exclusively provide ' +
317
+ 'verifiable presentation "validUntil".', {
318
+ name: 'DataError',
319
+ details: {
320
+ httpStatusCode: 400,
321
+ public: true
322
+ }
323
+ });
324
+ }
325
+ }
326
+
327
+ if(challenge !== undefined && nonce !== challenge) {
328
+ throw new BedrockError('JWT validation failed.', {
329
+ name: 'DataError',
330
+ details: {
331
+ httpStatusCode: 400,
332
+ public: true,
333
+ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
334
+ reason: 'missing or unexpected "nonce" claim value.',
335
+ claim: 'nonce'
336
+ }
337
+ });
338
+ }
339
+
340
+ // do some validation on `verifiableCredential`
341
+ let {verifiableCredential = []} = presentation;
342
+ if(!Array.isArray(verifiableCredential)) {
343
+ verifiableCredential = [verifiableCredential];
344
+ }
345
+
346
+ // ensure version 2 VPs only have objects in `verifiableCredential`
347
+ const hasVCJWTs = verifiableCredential.some(vc => typeof vc !== 'object');
348
+ if(isVersion2 && hasVCJWTs) {
349
+ throw new BedrockError(
350
+ 'Version 2.x verifiable presentations must only use objects in the ' +
351
+ '"verifiableCredential" field.', {
352
+ name: 'DataError',
353
+ details: {
354
+ httpStatusCode: 400,
355
+ public: true
356
+ }
357
+ });
358
+ }
359
+
360
+ // transform any VC-JWT VCs to enveloped VCs
361
+ if(presentation.verifiableCredential && hasVCJWTs) {
362
+ presentation.verifiableCredential = verifiableCredential.map(vc => {
363
+ if(typeof vc !== 'string') {
364
+ return vc;
365
+ }
366
+ return {
367
+ '@context': VC_CONTEXT_2,
368
+ id: `data:application/jwt,${vc}`,
369
+ type: 'EnvelopedVerifiableCredential',
370
+ };
371
+ });
372
+ }
373
+
374
+ return presentation;
375
+ }
package/lib/verify.js CHANGED
@@ -87,9 +87,13 @@ export async function verify({
87
87
 
88
88
  // generate useful error to return to client
89
89
  const {name, errors, message} = cause.data.error;
90
+ const causeError = _stripStacktrace({...cause.data.error});
91
+ delete causeError.errors;
90
92
  const error = new BedrockError(message ?? 'Verification error.', {
91
- name: name === 'VerificationError' ? 'DataError' : 'OperationError',
93
+ name: (name === 'VerificationError' || name === 'DataError') ?
94
+ 'DataError' : 'OperationError',
92
95
  details: {
96
+ error: causeError,
93
97
  verified,
94
98
  credentialResults,
95
99
  presentationResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/vc-delivery",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "type": "module",
5
5
  "description": "Bedrock Verifiable Credential Delivery",
6
6
  "main": "./lib/index.js",
@@ -303,7 +303,7 @@ const vcFormats = {
303
303
  const issuerInstance = {
304
304
  title: 'Issuer Instance',
305
305
  type: 'object',
306
- required: ['zcapReferenceIds'],
306
+ required: ['supportedFormats', 'zcapReferenceIds'],
307
307
  additionalProperties: false,
308
308
  properties: {
309
309
  id: {