@sd-jwt/sd-jwt-vc 0.1.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.
@@ -0,0 +1,684 @@
1
+ import Crypto from 'node:crypto';
2
+ import { afterEach } from 'node:test';
3
+ import { hasher as digest, generateSalt } from '@owf/crypto';
4
+ import type { DisclosureFrame, Signer, Verifier } from '@sd-jwt/core';
5
+ import { HttpResponse, http } from 'msw';
6
+ import { setupServer } from 'msw/node';
7
+ import { afterAll, beforeAll, describe, expect, test, vitest } from 'vitest';
8
+ import { SDJwtVcInstance } from '..';
9
+ import type { SdJwtVcPayload } from '../sd-jwt-vc-payload';
10
+ import type { TypeMetadataFormat } from '../sd-jwt-vc-type-metadata-format';
11
+
12
+ const exampleVctm = {
13
+ vct: 'http://example.com/example',
14
+ name: 'ExampleCredentialType',
15
+ description: 'An example credential type',
16
+ };
17
+
18
+ const baseVctm: TypeMetadataFormat = {
19
+ vct: 'http://example.com/base',
20
+ name: 'BaseCredentialType',
21
+ description: 'A base credential type',
22
+ claims: [
23
+ {
24
+ path: ['firstName'],
25
+ display: [{ locale: 'en', label: 'First Name' }],
26
+ },
27
+ ],
28
+ display: [
29
+ {
30
+ locale: 'en',
31
+ name: 'Base Credential',
32
+ description: 'Base description',
33
+ },
34
+ ],
35
+ };
36
+
37
+ const extendingVctm: TypeMetadataFormat = {
38
+ vct: 'http://example.com/extending',
39
+ name: 'ExtendingCredentialType',
40
+ description: 'A credential type that extends the base',
41
+ extends: 'http://example.com/base',
42
+ claims: [
43
+ {
44
+ path: ['lastName'],
45
+ display: [{ locale: 'en', label: 'Last Name' }],
46
+ },
47
+ ],
48
+ display: [
49
+ {
50
+ locale: 'en',
51
+ name: 'Extended Credential',
52
+ description: 'Extended description',
53
+ },
54
+ {
55
+ locale: 'de',
56
+ name: 'Erweiterte Berechtigung',
57
+ description: 'Erweiterte Beschreibung',
58
+ },
59
+ ],
60
+ };
61
+
62
+ const middleVctm: TypeMetadataFormat = {
63
+ vct: 'http://example.com/middle',
64
+ name: 'MiddleCredentialType',
65
+ description: 'Middle type in chain',
66
+ extends: 'http://example.com/extending',
67
+ claims: [
68
+ {
69
+ path: ['age'],
70
+ display: [{ locale: 'en', label: 'Age' }],
71
+ },
72
+ ],
73
+ };
74
+
75
+ const overridingVctm: TypeMetadataFormat = {
76
+ vct: 'http://example.com/overriding',
77
+ name: 'OverridingCredentialType',
78
+ description: 'A credential type that overrides a claim from the base',
79
+ extends: 'http://example.com/base',
80
+ claims: [
81
+ {
82
+ path: ['firstName'],
83
+ display: [{ locale: 'en', label: 'Given Name' }], // Override with different label
84
+ sd: 'always' as const,
85
+ },
86
+ {
87
+ path: ['middleName'],
88
+ display: [{ locale: 'en', label: 'Middle Name' }],
89
+ },
90
+ ],
91
+ };
92
+
93
+ const circularVctm: TypeMetadataFormat = {
94
+ vct: 'http://example.com/circular',
95
+ name: 'CircularCredentialType',
96
+ extends: 'http://example.com/circular',
97
+ };
98
+
99
+ const deepVctm: TypeMetadataFormat = {
100
+ vct: 'http://example.com/deep',
101
+ name: 'DeepCredentialType',
102
+ extends: 'http://example.com/middle',
103
+ };
104
+
105
+ const baseWithSdAlways: TypeMetadataFormat = {
106
+ vct: 'http://example.com/base-sd-always',
107
+ name: 'BaseWithSdAlways',
108
+ claims: [
109
+ {
110
+ path: ['sensitiveData'],
111
+ sd: 'always' as const,
112
+ display: [{ locale: 'en', label: 'Sensitive Data' }],
113
+ },
114
+ ],
115
+ };
116
+
117
+ const invalidExtendingSdChange: TypeMetadataFormat = {
118
+ vct: 'http://example.com/invalid-sd-change',
119
+ name: 'InvalidSdChange',
120
+ extends: 'http://example.com/base-sd-always',
121
+ claims: [
122
+ {
123
+ path: ['sensitiveData'],
124
+ sd: 'never' as const, // Invalid: trying to change from 'always' to 'never'
125
+ display: [{ locale: 'en', label: 'Sensitive Data' }],
126
+ },
127
+ ],
128
+ };
129
+
130
+ const validExtendingSdChange: TypeMetadataFormat = {
131
+ vct: 'http://example.com/valid-sd-change',
132
+ name: 'ValidSdChange',
133
+ extends: 'http://example.com/base',
134
+ claims: [
135
+ {
136
+ path: ['firstName'],
137
+ sd: 'always' as const, // Valid: base doesn't have sd or has 'allowed'
138
+ display: [{ locale: 'en', label: 'First Name' }],
139
+ },
140
+ ],
141
+ };
142
+
143
+ const vctWithCustomProperties: TypeMetadataFormat = {
144
+ vct: 'http://example.com/custom-properties',
145
+ name: 'CustomProperties',
146
+ claims: [
147
+ {
148
+ path: ['firstName'],
149
+ sd: 'always' as const, // Valid: base doesn't have sd or has 'allowed'
150
+ display: [{ locale: 'en', label: 'First Name' }],
151
+ anotherCustom: 'property',
152
+ },
153
+ ],
154
+ test: 'something',
155
+ };
156
+
157
+ const restHandlers = [
158
+ http.get('http://example.com/example', () => {
159
+ const res: TypeMetadataFormat = exampleVctm;
160
+ return HttpResponse.json(res);
161
+ }),
162
+ http.get('http://example.com/timeout', () => {
163
+ return new Promise((resolve) => {
164
+ setTimeout(() => {
165
+ resolve(HttpResponse.json({}));
166
+ }, 10000);
167
+ });
168
+ }),
169
+ http.get('http://example.com/base', () => {
170
+ return HttpResponse.json(baseVctm);
171
+ }),
172
+ http.get('http://example.com/extending', () => {
173
+ return HttpResponse.json(extendingVctm);
174
+ }),
175
+ http.get('http://example.com/middle', () => {
176
+ return HttpResponse.json(middleVctm);
177
+ }),
178
+ http.get('http://example.com/overriding', () => {
179
+ return HttpResponse.json(overridingVctm);
180
+ }),
181
+ http.get('http://example.com/circular', () => {
182
+ return HttpResponse.json(circularVctm);
183
+ }),
184
+ http.get('http://example.com/deep', () => {
185
+ return HttpResponse.json(deepVctm);
186
+ }),
187
+ http.get('http://example.com/base-sd-always', () => {
188
+ return HttpResponse.json(baseWithSdAlways);
189
+ }),
190
+ http.get('http://example.com/invalid-sd-change', () => {
191
+ return HttpResponse.json(invalidExtendingSdChange);
192
+ }),
193
+ http.get('http://example.com/valid-sd-change', () => {
194
+ return HttpResponse.json(validExtendingSdChange);
195
+ }),
196
+ http.get('http://example.com/custom-properties', () => {
197
+ return HttpResponse.json(vctWithCustomProperties);
198
+ }),
199
+ http.get('http://example.com/invalid', () => {
200
+ // Return invalid type metadata (missing required 'vct' field)
201
+ return HttpResponse.json({
202
+ name: 'InvalidCredentialType',
203
+ description: 'Missing required vct field',
204
+ });
205
+ }),
206
+ ];
207
+
208
+ //this value could be generated on demand to make it easier when changing the values
209
+ const vctIntegrity =
210
+ 'sha256-e8bf419e6b860595f385611fc6172f1e95c18de3c80eef57c865f49e03747637';
211
+
212
+ const server = setupServer(...restHandlers);
213
+
214
+ const iss = 'ExampleIssuer';
215
+ const vct = 'http://example.com/example';
216
+ const iat = Math.floor(Date.now() / 1000); // current time in seconds
217
+
218
+ const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519');
219
+
220
+ const createSignerVerifier = () => {
221
+ const signer: Signer = async (data: string) => {
222
+ const sig = Crypto.sign(null, Buffer.from(data), privateKey);
223
+ return Buffer.from(sig).toString('base64url');
224
+ };
225
+ const verifier: Verifier = async (data: string, sig: string) => {
226
+ return Crypto.verify(
227
+ null,
228
+ Buffer.from(data),
229
+ publicKey,
230
+ Buffer.from(sig, 'base64url'),
231
+ );
232
+ };
233
+ return { signer, verifier };
234
+ };
235
+
236
+ describe('App', () => {
237
+ const { signer, verifier } = createSignerVerifier();
238
+
239
+ const sdjwt = new SDJwtVcInstance({
240
+ signer,
241
+ signAlg: 'EdDSA',
242
+ verifier,
243
+ hasher: digest,
244
+ hashAlg: 'sha-256',
245
+ saltGenerator: generateSalt,
246
+ loadTypeMetadataFormat: true,
247
+ timeout: 1000,
248
+ });
249
+
250
+ const claims = {
251
+ firstname: 'John',
252
+ };
253
+ const disclosureFrame = {
254
+ _sd: ['firstname'],
255
+ };
256
+
257
+ beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
258
+
259
+ afterAll(() => server.close());
260
+
261
+ afterEach(() => server.resetHandlers());
262
+
263
+ test('VCT Validation', async () => {
264
+ // The method is private, so TS complains, but you can use spies on private method just fine.
265
+ // @ts-expect-error
266
+ const validateIntegritySpy = vitest.spyOn(sdjwt, 'validateIntegrity');
267
+
268
+ const expectedPayload: SdJwtVcPayload = {
269
+ iat,
270
+ iss,
271
+ vct,
272
+ 'vct#integrity': vctIntegrity,
273
+ ...claims,
274
+ };
275
+
276
+ const encodedSdjwt = await sdjwt.issue(
277
+ expectedPayload,
278
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
279
+ );
280
+
281
+ await sdjwt.verify(encodedSdjwt);
282
+
283
+ // Ensure validateIntegrity method was called
284
+ expect(validateIntegritySpy).toHaveBeenCalledWith(
285
+ expect.any(Response),
286
+ vct,
287
+ vctIntegrity,
288
+ );
289
+ });
290
+
291
+ test('VCT Validation with timeout', async () => {
292
+ const vct = 'http://example.com/timeout';
293
+ const expectedPayload: SdJwtVcPayload = {
294
+ iat,
295
+ iss,
296
+ vct,
297
+ ...claims,
298
+ };
299
+ const encodedSdjwt = await sdjwt.issue(
300
+ expectedPayload,
301
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
302
+ );
303
+
304
+ await expect(sdjwt.verify(encodedSdjwt)).rejects.toThrowError(
305
+ `Request to ${vct} timed out`,
306
+ );
307
+ });
308
+
309
+ test('VCT Metadata retrieval', async () => {
310
+ const expectedPayload: SdJwtVcPayload = {
311
+ iat,
312
+ iss,
313
+ vct,
314
+ 'vct#Integrity': vctIntegrity,
315
+ ...claims,
316
+ };
317
+ const encodedSdjwt = await sdjwt.issue(
318
+ expectedPayload,
319
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
320
+ );
321
+
322
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
323
+
324
+ // Check mergedTypeMetadata
325
+ expect(resolvedTypeMetadata?.mergedTypeMetadata).to.deep.eq({
326
+ description: 'An example credential type',
327
+ name: 'ExampleCredentialType',
328
+ vct: 'http://example.com/example',
329
+ });
330
+
331
+ // Check typeMetadataChain - should have only one document (no extends)
332
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(1);
333
+ expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
334
+ 'http://example.com/example',
335
+ );
336
+
337
+ // Check vctValues - should have only one value
338
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(1);
339
+ expect(resolvedTypeMetadata?.vctValues[0]).toBe(
340
+ 'http://example.com/example',
341
+ );
342
+ });
343
+
344
+ test('VCT with extends - simple chain', async () => {
345
+ const expectedPayload: SdJwtVcPayload = {
346
+ iat,
347
+ iss,
348
+ vct: 'http://example.com/extending',
349
+ ...claims,
350
+ };
351
+
352
+ const encodedSdjwt = await sdjwt.issue(
353
+ expectedPayload,
354
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
355
+ );
356
+
357
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
358
+
359
+ // Check mergedTypeMetadata - should merge claims from both base and extending types
360
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
361
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
362
+ 'firstName',
363
+ ]);
364
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
365
+ 'lastName',
366
+ ]);
367
+
368
+ // Display from extending type completely replaces base display (section 8.2)
369
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display).toHaveLength(2);
370
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[0]).toEqual({
371
+ locale: 'en',
372
+ name: 'Extended Credential',
373
+ description: 'Extended description',
374
+ });
375
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[1]).toEqual({
376
+ locale: 'de',
377
+ name: 'Erweiterte Berechtigung',
378
+ description: 'Erweiterte Beschreibung',
379
+ });
380
+
381
+ // Top-level properties should come from extending type
382
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.name).toBe(
383
+ 'ExtendingCredentialType',
384
+ );
385
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.description).toBe(
386
+ 'A credential type that extends the base',
387
+ );
388
+
389
+ // Check typeMetadataChain - should have 2 documents in chain
390
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
391
+ expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
392
+ 'http://example.com/extending',
393
+ );
394
+ expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
395
+ 'http://example.com/base',
396
+ );
397
+
398
+ // Check vctValues - should have 2 values
399
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
400
+ expect(resolvedTypeMetadata?.vctValues[0]).toBe(
401
+ 'http://example.com/extending',
402
+ );
403
+ expect(resolvedTypeMetadata?.vctValues[1]).toBe('http://example.com/base');
404
+ });
405
+
406
+ test('VCT with extends - multi-level chain', async () => {
407
+ const expectedPayload: SdJwtVcPayload = {
408
+ iat,
409
+ iss,
410
+ vct: 'http://example.com/middle',
411
+ ...claims,
412
+ };
413
+
414
+ const encodedSdjwt = await sdjwt.issue(
415
+ expectedPayload,
416
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
417
+ );
418
+
419
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
420
+
421
+ // Check mergedTypeMetadata - should merge claims from base -> extending -> middle
422
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(3);
423
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
424
+ 'firstName',
425
+ ]);
426
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
427
+ 'lastName',
428
+ ]);
429
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[2].path).toEqual([
430
+ 'age',
431
+ ]);
432
+
433
+ // Top-level properties should come from the most derived type
434
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.name).toBe(
435
+ 'MiddleCredentialType',
436
+ );
437
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.description).toBe(
438
+ 'Middle type in chain',
439
+ );
440
+
441
+ // Check typeMetadataChain - should have 3 documents in chain
442
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(3);
443
+ expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
444
+ 'http://example.com/middle',
445
+ );
446
+ expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
447
+ 'http://example.com/extending',
448
+ );
449
+ expect(resolvedTypeMetadata?.typeMetadataChain[2].vct).toBe(
450
+ 'http://example.com/base',
451
+ );
452
+
453
+ // Check vctValues - should have 3 values
454
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(3);
455
+ expect(resolvedTypeMetadata?.vctValues[0]).toBe(
456
+ 'http://example.com/middle',
457
+ );
458
+ expect(resolvedTypeMetadata?.vctValues[1]).toBe(
459
+ 'http://example.com/extending',
460
+ );
461
+ expect(resolvedTypeMetadata?.vctValues[2]).toBe('http://example.com/base');
462
+ });
463
+
464
+ test('VCT with circular dependency should throw error', async () => {
465
+ const expectedPayload: SdJwtVcPayload = {
466
+ iat,
467
+ iss,
468
+ vct: 'http://example.com/circular',
469
+ ...claims,
470
+ };
471
+
472
+ const encodedSdjwt = await sdjwt.issue(
473
+ expectedPayload,
474
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
475
+ );
476
+
477
+ await expect(sdjwt.getVct(encodedSdjwt)).rejects.toThrowError(
478
+ 'Circular dependency detected in VCT extends chain: http://example.com/circular',
479
+ );
480
+ });
481
+
482
+ test('VCT with max depth exceeded should throw error', async () => {
483
+ const sdjwtWithShallowDepth = new SDJwtVcInstance({
484
+ signer,
485
+ signAlg: 'EdDSA',
486
+ verifier,
487
+ hasher: digest,
488
+ hashAlg: 'sha-256',
489
+ saltGenerator: generateSalt,
490
+ loadTypeMetadataFormat: true,
491
+ timeout: 1000,
492
+ maxVctExtendsDepth: 1, // Only allow 1 level of extends
493
+ });
494
+
495
+ const expectedPayload: SdJwtVcPayload = {
496
+ iat,
497
+ iss,
498
+ vct: 'http://example.com/middle', // This has 2 levels of extends
499
+ ...claims,
500
+ };
501
+
502
+ const encodedSdjwt = await sdjwtWithShallowDepth.issue(
503
+ expectedPayload,
504
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
505
+ );
506
+
507
+ await expect(
508
+ sdjwtWithShallowDepth.getVct(encodedSdjwt),
509
+ ).rejects.toThrowError('Maximum VCT extends depth of 1 exceeded');
510
+ });
511
+
512
+ test('VCT extends chain should work in verify method', async () => {
513
+ const expectedPayload: SdJwtVcPayload = {
514
+ iat,
515
+ iss,
516
+ vct: 'http://example.com/extending',
517
+ ...claims,
518
+ };
519
+
520
+ const encodedSdjwt = await sdjwt.issue(
521
+ expectedPayload,
522
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
523
+ );
524
+
525
+ // Should not throw and should resolve the extends chain
526
+ const result = await sdjwt.verify(encodedSdjwt);
527
+ expect(result.payload.vct).toBe('http://example.com/extending');
528
+
529
+ // Check that typeMetadata was populated with resolved chain
530
+ expect(result.typeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
531
+ expect(result.typeMetadata?.typeMetadataChain).toHaveLength(2);
532
+ expect(result.typeMetadata?.vctValues).toHaveLength(2);
533
+ });
534
+
535
+ test('VCT with overriding claim metadata', async () => {
536
+ const expectedPayload: SdJwtVcPayload = {
537
+ iat,
538
+ iss,
539
+ vct: 'http://example.com/overriding',
540
+ ...claims,
541
+ };
542
+
543
+ const encodedSdjwt = await sdjwt.issue(
544
+ expectedPayload,
545
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
546
+ );
547
+
548
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
549
+
550
+ // Check mergedTypeMetadata - should have 2 claims: overridden firstName and new middleName
551
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(2);
552
+
553
+ // First claim should be the overridden firstName with new label and sd property
554
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
555
+ 'firstName',
556
+ ]);
557
+ expect(
558
+ resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].display?.[0].label,
559
+ ).toBe('Given Name');
560
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].sd).toBe(
561
+ 'always',
562
+ );
563
+
564
+ // Second claim should be the new middleName
565
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].path).toEqual([
566
+ 'middleName',
567
+ ]);
568
+ expect(
569
+ resolvedTypeMetadata?.mergedTypeMetadata.claims?.[1].display?.[0].label,
570
+ ).toBe('Middle Name');
571
+
572
+ // Check typeMetadataChain - should have 2 documents
573
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
574
+ expect(resolvedTypeMetadata?.typeMetadataChain[0].vct).toBe(
575
+ 'http://example.com/overriding',
576
+ );
577
+ expect(resolvedTypeMetadata?.typeMetadataChain[1].vct).toBe(
578
+ 'http://example.com/base',
579
+ );
580
+
581
+ // Check vctValues
582
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
583
+ expect(resolvedTypeMetadata?.vctValues[0]).toBe(
584
+ 'http://example.com/overriding',
585
+ );
586
+ expect(resolvedTypeMetadata?.vctValues[1]).toBe('http://example.com/base');
587
+ });
588
+
589
+ test('VCT with valid sd property change (allowed to always)', async () => {
590
+ const expectedPayload: SdJwtVcPayload = {
591
+ iat,
592
+ iss,
593
+ vct: 'http://example.com/valid-sd-change',
594
+ ...claims,
595
+ };
596
+
597
+ const encodedSdjwt = await sdjwt.issue(
598
+ expectedPayload,
599
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
600
+ );
601
+
602
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
603
+
604
+ // Check mergedTypeMetadata - should successfully merge - changing from undefined/allowed to always is valid
605
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims).toHaveLength(1);
606
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].path).toEqual([
607
+ 'firstName',
608
+ ]);
609
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.claims?.[0].sd).toBe(
610
+ 'always',
611
+ );
612
+
613
+ // Check typeMetadataChain
614
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(2);
615
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(2);
616
+ });
617
+
618
+ test('VCT with invalid sd property change (always to never) should throw error', async () => {
619
+ const expectedPayload: SdJwtVcPayload = {
620
+ iat,
621
+ iss,
622
+ vct: 'http://example.com/invalid-sd-change',
623
+ ...claims,
624
+ };
625
+
626
+ const encodedSdjwt = await sdjwt.issue(
627
+ expectedPayload,
628
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
629
+ );
630
+
631
+ await expect(sdjwt.getVct(encodedSdjwt)).rejects.toThrowError(
632
+ "Cannot change 'sd' property from 'always' to 'never' for claim at path [\"sensitiveData\"]",
633
+ );
634
+ });
635
+
636
+ test('VCT extending type without display should inherit base display', async () => {
637
+ const expectedPayload: SdJwtVcPayload = {
638
+ iat,
639
+ iss,
640
+ vct: 'http://example.com/middle', // middle doesn't define display
641
+ ...claims,
642
+ };
643
+
644
+ const encodedSdjwt = await sdjwt.issue(
645
+ expectedPayload,
646
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
647
+ );
648
+
649
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
650
+
651
+ // Check mergedTypeMetadata - since middle doesn't define display, it should inherit from extending which has display
652
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display).toHaveLength(2);
653
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[0].locale).toBe(
654
+ 'en',
655
+ );
656
+ expect(resolvedTypeMetadata?.mergedTypeMetadata.display?.[1].locale).toBe(
657
+ 'de',
658
+ );
659
+
660
+ // Check typeMetadataChain - should have 3 documents
661
+ expect(resolvedTypeMetadata?.typeMetadataChain).toHaveLength(3);
662
+ expect(resolvedTypeMetadata?.vctValues).toHaveLength(3);
663
+ });
664
+
665
+ test('VCT with custom properties are kept', async () => {
666
+ const expectedPayload: SdJwtVcPayload = {
667
+ iat,
668
+ iss,
669
+ vct: 'http://example.com/custom-properties',
670
+ ...claims,
671
+ };
672
+
673
+ const encodedSdjwt = await sdjwt.issue(
674
+ expectedPayload,
675
+ disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
676
+ );
677
+
678
+ const resolvedTypeMetadata = await sdjwt.getVct(encodedSdjwt);
679
+
680
+ expect(resolvedTypeMetadata?.mergedTypeMetadata).toEqual(
681
+ vctWithCustomProperties,
682
+ );
683
+ });
684
+ });
@@ -0,0 +1,16 @@
1
+ import type { kbHeader, kbPayload } from '@sd-jwt/core';
2
+ import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
3
+ import type { ResolvedTypeMetadata } from './sd-jwt-vc-type-metadata-format';
4
+
5
+ export type VerificationResult = {
6
+ payload: SdJwtVcPayload;
7
+ header: Record<string, unknown> | undefined;
8
+ kb:
9
+ | {
10
+ payload: kbPayload;
11
+ header: kbHeader;
12
+ }
13
+ | undefined;
14
+
15
+ typeMetadata?: ResolvedTypeMetadata;
16
+ };