@learncard/credential-library 1.0.17 → 2.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.
Files changed (37) hide show
  1. package/README.md +58 -3
  2. package/dist/__tests__/sd-jwt-vc.test.d.ts +2 -0
  3. package/dist/__tests__/sd-jwt-vc.test.d.ts.map +1 -0
  4. package/dist/credential-library.cjs.development.js +8951 -272
  5. package/dist/credential-library.cjs.development.js.map +3 -3
  6. package/dist/credential-library.cjs.production.min.js +1 -1
  7. package/dist/credential-library.cjs.production.min.js.map +4 -4
  8. package/dist/credential-library.esm.js +8951 -272
  9. package/dist/credential-library.esm.js.map +3 -3
  10. package/dist/fixtures/clr/demo-isd-diploma-assessments.d.ts +3 -0
  11. package/dist/fixtures/clr/demo-isd-diploma-assessments.d.ts.map +1 -0
  12. package/dist/fixtures/index.d.ts +5 -3
  13. package/dist/fixtures/index.d.ts.map +1 -1
  14. package/dist/fixtures/sd-jwt-vc/course-completion.d.ts +3 -0
  15. package/dist/fixtures/sd-jwt-vc/course-completion.d.ts.map +1 -0
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/materialize-sd-jwt-vc.d.ts +21 -0
  19. package/dist/materialize-sd-jwt-vc.d.ts.map +1 -0
  20. package/dist/prepare.d.ts +2 -2
  21. package/dist/prepare.d.ts.map +1 -1
  22. package/dist/registry.d.ts +11 -9
  23. package/dist/registry.d.ts.map +1 -1
  24. package/dist/types.d.ts +31 -5
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +7 -4
  27. package/src/__tests__/issuance.test.ts +5 -2
  28. package/src/__tests__/registry.test.ts +195 -7
  29. package/src/__tests__/sd-jwt-vc.test.ts +220 -0
  30. package/src/fixtures/clr/demo-isd-diploma-assessments.ts +9183 -0
  31. package/src/fixtures/index.ts +12 -2
  32. package/src/fixtures/sd-jwt-vc/course-completion.ts +27 -0
  33. package/src/index.ts +17 -0
  34. package/src/materialize-sd-jwt-vc.ts +159 -0
  35. package/src/prepare.ts +9 -2
  36. package/src/registry.ts +88 -27
  37. package/src/types.ts +51 -10
@@ -0,0 +1,220 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { exportJWK, generateKeyPair, type JWK } from 'jose';
3
+ import {
4
+ createEd25519KbSigner,
5
+ parseSdJwtVc,
6
+ presentSdJwtVc,
7
+ verifySdJwtVc,
8
+ } from '@learncard/sd-jwt-vc-plugin';
9
+
10
+ import {
11
+ getFixture,
12
+ isSdJwtVcFixture,
13
+ materializeSdJwtVcFixture,
14
+ type SdJwtVcFixture,
15
+ } from '../index';
16
+
17
+ const ISSUED_AT = 1_787_616_000;
18
+
19
+ const fixture = getFixture('sd-jwt-vc/course-completion');
20
+ if (!isSdJwtVcFixture(fixture)) throw new Error('Expected SD-JWT VC fixture');
21
+
22
+ const makeKeypair = async (): Promise<{
23
+ privateJwk: JWK;
24
+ publicJwk: JWK;
25
+ }> => {
26
+ const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true });
27
+ return {
28
+ privateJwk: await exportJWK(pair.privateKey),
29
+ publicJwk: await exportJWK(pair.publicKey),
30
+ };
31
+ };
32
+
33
+ const toDidJwk = (jwk: JWK): string =>
34
+ `did:jwk:${Buffer.from(JSON.stringify(jwk)).toString('base64url')}`;
35
+
36
+ const makeMaterializerSigner = async (privateJwk: JWK) => {
37
+ const signer = await createEd25519KbSigner({ privateJwk });
38
+ return async (signingInput: string): Promise<string> => signer(signingInput);
39
+ };
40
+
41
+ const materializeFixture = async () => {
42
+ const issuer = await makeKeypair();
43
+ const holder = await makeKeypair();
44
+ const issuerDid = toDidJwk(issuer.publicJwk);
45
+ const issuerKid = `${issuerDid}#0`;
46
+ const result = await materializeSdJwtVcFixture(fixture, {
47
+ issuerDid,
48
+ issuerKid,
49
+ issuerSigner: await makeMaterializerSigner(issuer.privateJwk),
50
+ holderPublicJwk: { ...holder.publicJwk },
51
+ issuedAt: ISSUED_AT,
52
+ });
53
+
54
+ return { issuer, holder, issuerDid, issuerKid, result };
55
+ };
56
+
57
+ describe('materializeSdJwtVcFixture', () => {
58
+ it('issues a parseable, holder-bound dc+sd-jwt envelope that verifies against its issuer DID', async () => {
59
+ const { issuer, holder, issuerDid, issuerKid, result } = await materializeFixture();
60
+
61
+ expect(result.vct).toBe(fixture.template.vct);
62
+ expect(result.envelope).toEqual({ format: 'dc+sd-jwt', data: result.compact });
63
+ expect(result.compact).toContain('~');
64
+
65
+ const parsed = await parseSdJwtVc(result.compact);
66
+ expect(parsed.vct).toBe(fixture.template.vct);
67
+ expect(parsed.issuer).toBe(issuerDid);
68
+ expect(parsed.issuedAt?.getTime()).toBe(ISSUED_AT * 1_000);
69
+ expect(parsed.holderPublicKey).toEqual({ ...holder.publicJwk });
70
+ expect(parsed.disclosureKeys.sort()).toEqual(
71
+ fixture.template.selectivelyDisclosable.slice().sort()
72
+ );
73
+ expect(parsed.hasKeyBinding).toBe(false);
74
+ expect(parsed.header).toMatchObject({ typ: 'dc+sd-jwt', alg: 'EdDSA', kid: issuerKid });
75
+
76
+ const verifierLearnCard = {
77
+ invoke: {
78
+ resolveDid: async () => ({
79
+ '@context': ['https://www.w3.org/ns/did/v1'],
80
+ id: issuerDid,
81
+ verificationMethod: [
82
+ {
83
+ id: issuerKid,
84
+ type: 'JsonWebKey2020',
85
+ controller: issuerDid,
86
+ publicKeyJwk: { ...issuer.publicJwk, alg: 'EdDSA' },
87
+ },
88
+ ],
89
+ assertionMethod: [issuerKid],
90
+ authentication: [issuerKid],
91
+ }),
92
+ },
93
+ };
94
+
95
+ const verification = await verifySdJwtVc(verifierLearnCard as never, result.compact, {
96
+ expectedVct: fixture.template.vct,
97
+ });
98
+ expect(verification.errors).toEqual([]);
99
+
100
+ const presentation = await presentSdJwtVc(result.compact, {
101
+ audience: 'https://verifier.example.com',
102
+ nonce: 'phase-0-nonce',
103
+ kbSigner: await createEd25519KbSigner({ privateJwk: holder.privateJwk }),
104
+ activeHolderPublicJwk: { ...holder.publicJwk },
105
+ verify: async () => verification,
106
+ now: () => ISSUED_AT,
107
+ });
108
+
109
+ expect(presentation.hasKeyBinding).toBe(true);
110
+ const kbJwt = presentation.compact.split('~').filter(Boolean).at(-1)!;
111
+ const [, payloadSegment] = kbJwt.split('.');
112
+ const kbPayload = JSON.parse(
113
+ Buffer.from(payloadSegment!, 'base64url').toString('utf8')
114
+ ) as Record<string, unknown>;
115
+ expect(kbPayload.aud).toBe('https://verifier.example.com');
116
+ expect(kbPayload.nonce).toBe('phase-0-nonce');
117
+ });
118
+
119
+ it('rejects fixture templates that override reserved SD-JWT claims', async () => {
120
+ const issuer = await makeKeypair();
121
+ const holder = await makeKeypair();
122
+ const issuerDid = toDidJwk(issuer.publicJwk);
123
+ const reservedClaimFixture: SdJwtVcFixture = {
124
+ ...fixture,
125
+ template: {
126
+ ...fixture.template,
127
+ claims: { ...fixture.template.claims, iss: 'did:example:override' },
128
+ },
129
+ };
130
+
131
+ await expect(
132
+ materializeSdJwtVcFixture(reservedClaimFixture, {
133
+ issuerDid,
134
+ issuerKid: `${issuerDid}#0`,
135
+ issuerSigner: await makeMaterializerSigner(issuer.privateJwk),
136
+ holderPublicJwk: { ...holder.publicJwk },
137
+ })
138
+ ).rejects.toThrow(/reserved/i);
139
+ });
140
+
141
+ it('rejects selectively disclosable claim names that do not exist in the fixture claims', async () => {
142
+ const issuer = await makeKeypair();
143
+ const holder = await makeKeypair();
144
+ const issuerDid = toDidJwk(issuer.publicJwk);
145
+ const mismatchedDisclosureFixture: SdJwtVcFixture = {
146
+ ...fixture,
147
+ template: {
148
+ ...fixture.template,
149
+ selectivelyDisclosable: [
150
+ ...fixture.template.selectivelyDisclosable,
151
+ 'missing_claim',
152
+ ],
153
+ },
154
+ };
155
+
156
+ await expect(
157
+ materializeSdJwtVcFixture(mismatchedDisclosureFixture, {
158
+ issuerDid,
159
+ issuerKid: `${issuerDid}#0`,
160
+ issuerSigner: await makeMaterializerSigner(issuer.privateJwk),
161
+ holderPublicJwk: { ...holder.publicJwk },
162
+ })
163
+ ).rejects.toThrow(
164
+ 'SD-JWT fixture declares selectively disclosable claims that do not exist: missing_claim'
165
+ );
166
+ });
167
+
168
+ it.each([
169
+ { crv: 'Ed25519', x: 'abc' },
170
+ { kty: 'OKP', x: 'abc' },
171
+ { kty: 'OKP', crv: 'Ed25519' },
172
+ { kty: 'OKP', crv: 'Ed25519', x: 'abc' },
173
+ { kty: 'OKP', crv: 'Ed25519', x: `${'A'.repeat(42)}+` },
174
+ { kty: 'OKP', crv: 'Ed25519', x: `${'A'.repeat(42)}B` },
175
+ { kty: 'OKP', crv: 'Ed25519', x: `${'A'.repeat(43)}=` },
176
+ ])('rejects malformed holder public JWK %j', async holderPublicJwk => {
177
+ const issuer = await makeKeypair();
178
+ const issuerDid = toDidJwk(issuer.publicJwk);
179
+
180
+ await expect(
181
+ materializeSdJwtVcFixture(fixture, {
182
+ issuerDid,
183
+ issuerKid: `${issuerDid}#0`,
184
+ issuerSigner: await makeMaterializerSigner(issuer.privateJwk),
185
+ holderPublicJwk,
186
+ })
187
+ ).rejects.toThrow(/holder.*JWK|Ed25519/i);
188
+ });
189
+
190
+ it('rejects a holder JWK containing private key material', async () => {
191
+ const issuer = await makeKeypair();
192
+ const holder = await makeKeypair();
193
+ const issuerDid = toDidJwk(issuer.publicJwk);
194
+
195
+ await expect(
196
+ materializeSdJwtVcFixture(fixture, {
197
+ issuerDid,
198
+ issuerKid: `${issuerDid}#0`,
199
+ issuerSigner: await makeMaterializerSigner(issuer.privateJwk),
200
+ holderPublicJwk: { ...holder.privateJwk },
201
+ })
202
+ ).rejects.toThrow('holder key must be public and must not include private key material');
203
+ });
204
+
205
+ it.each([
206
+ ['issuer DID', { issuerDid: '', issuerKid: 'did:example:issuer#0' }],
207
+ ['issuer KID', { issuerDid: 'did:example:issuer', issuerKid: '' }],
208
+ ] as const)('rejects an empty %s', async (_name, issuer) => {
209
+ const keypair = await makeKeypair();
210
+ const holder = await makeKeypair();
211
+
212
+ await expect(
213
+ materializeSdJwtVcFixture(fixture, {
214
+ ...issuer,
215
+ issuerSigner: await makeMaterializerSigner(keypair.privateJwk),
216
+ holderPublicJwk: { ...holder.publicJwk },
217
+ })
218
+ ).rejects.toThrow(/issuer/i);
219
+ });
220
+ });