@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
@@ -1,4 +1,4 @@
1
- import type { CredentialFixture } from '../types';
1
+ import type { LibraryFixture } from '../types';
2
2
 
3
3
  // VC v1
4
4
  import { vcV1Basic } from './vc-v1/basic';
@@ -56,12 +56,16 @@ import { vcV2LicenseCredential } from './vc-v2/license-credential';
56
56
  import { clrGreatPlainsFull } from './clr/great-plains-full';
57
57
  import { clrWestbridgeFull } from './clr/westbridge-full';
58
58
  import { clrCompetencyAligned } from './clr/competency-aligned';
59
+ import { clrDemoIsdDiplomaAssessments } from './clr/demo-isd-diploma-assessments';
60
+
61
+ // SD-JWT VC
62
+ import { sdJwtVcCourseCompletion } from './sd-jwt-vc/course-completion';
59
63
 
60
64
  // ---------------------------------------------------------------------------
61
65
  // All fixtures — collected for auto-registration
62
66
  // ---------------------------------------------------------------------------
63
67
 
64
- export const ALL_FIXTURES: CredentialFixture[] = [
68
+ export const ALL_FIXTURES: LibraryFixture[] = [
65
69
  // VC v1
66
70
  vcV1Basic,
67
71
  vcV1WithStatus,
@@ -116,6 +120,10 @@ export const ALL_FIXTURES: CredentialFixture[] = [
116
120
  clrGreatPlainsFull,
117
121
  clrWestbridgeFull,
118
122
  clrCompetencyAligned,
123
+ clrDemoIsdDiplomaAssessments,
124
+
125
+ // SD-JWT VC
126
+ sdJwtVcCourseCompletion,
119
127
  ];
120
128
 
121
129
  // Re-export individual fixtures for direct import
@@ -159,4 +167,6 @@ export {
159
167
  clrGreatPlainsFull,
160
168
  clrWestbridgeFull,
161
169
  clrCompetencyAligned,
170
+ clrDemoIsdDiplomaAssessments,
171
+ sdJwtVcCourseCompletion,
162
172
  };
@@ -0,0 +1,27 @@
1
+ import type { SdJwtVcFixture } from '../../types';
2
+
3
+ export const sdJwtVcCourseCompletion: SdJwtVcFixture = {
4
+ kind: 'sd-jwt-vc',
5
+ id: 'sd-jwt-vc/course-completion',
6
+ name: 'SD-JWT VC Course Completion',
7
+ description:
8
+ 'Synthetic holder-bound course completion credential for Digital Credentials API testing',
9
+ spec: 'sd-jwt-vc',
10
+ profile: 'course',
11
+ features: ['skills', 'selective-disclosure', 'holder-binding'],
12
+ source: 'synthetic',
13
+ signed: false,
14
+ validity: 'valid',
15
+ tags: ['sd-jwt', 'dcql', 'android-digital-credentials', 'holder-bound'],
16
+ template: {
17
+ format: 'dc+sd-jwt',
18
+ vct: 'https://credentials.learncard.com/vct/course-completion',
19
+ claims: {
20
+ learner_name: 'Ada Lovelace',
21
+ course_name: 'Introduction to Verifiable Credentials',
22
+ completion_date: '2026-08-25',
23
+ skills: ['Digital Identity', 'Verifiable Credentials'],
24
+ },
25
+ selectivelyDisclosable: ['learner_name', 'course_name', 'completion_date', 'skills'],
26
+ },
27
+ };
package/src/index.ts CHANGED
@@ -3,9 +3,14 @@ export type {
3
3
  CredentialSpec,
4
4
  CredentialProfile,
5
5
  CredentialFeature,
6
+ FixtureKind,
6
7
  FixtureSource,
7
8
  FixtureValidity,
9
+ BaseCredentialFixture,
8
10
  CredentialFixture,
11
+ SdJwtVcTemplate,
12
+ SdJwtVcFixture,
13
+ LibraryFixture,
9
14
  FixtureFilter,
10
15
  InvalidCredential,
11
16
  } from './types';
@@ -14,8 +19,11 @@ export {
14
19
  CREDENTIAL_SPECS,
15
20
  CREDENTIAL_PROFILES,
16
21
  CREDENTIAL_FEATURES,
22
+ FIXTURE_KINDS,
17
23
  FIXTURE_SOURCES,
18
24
  FIXTURE_VALIDITIES,
25
+ isCredentialFixture,
26
+ isSdJwtVcFixture,
19
27
  } from './types';
20
28
 
21
29
  // Registry (query API + mutation)
@@ -41,5 +49,14 @@ export { prepareFixture, prepareFixtureById } from './prepare';
41
49
 
42
50
  export type { PrepareOptions } from './prepare';
43
51
 
52
+ // SD-JWT VC materialization
53
+ export { materializeSdJwtVcFixture } from './materialize-sd-jwt-vc';
54
+
55
+ export type {
56
+ SdJwtVcSigner,
57
+ MaterializeSdJwtVcOptions,
58
+ MaterializedSdJwtVcFixture,
59
+ } from './materialize-sd-jwt-vc';
60
+
44
61
  // Fixtures — importing this module registers all fixtures in the registry
45
62
  export * from './fixtures';
@@ -0,0 +1,159 @@
1
+ import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
2
+ import { randomSalt, sha256Hasher } from '@learncard/sd-jwt-vc-plugin';
3
+ import type { StoredCredentialEnvelope } from '@learncard/types';
4
+
5
+ import type { SdJwtVcFixture } from './types';
6
+
7
+ export type SdJwtVcSigner = (signingInput: string) => Promise<string>;
8
+
9
+ export interface MaterializeSdJwtVcOptions {
10
+ issuerDid: string;
11
+ issuerKid: string;
12
+ issuerSigner: SdJwtVcSigner;
13
+ holderPublicJwk: Record<string, unknown>;
14
+ issuedAt?: number;
15
+ }
16
+
17
+ export interface MaterializedSdJwtVcFixture {
18
+ compact: string;
19
+ envelope: StoredCredentialEnvelope;
20
+ vct: string;
21
+ }
22
+
23
+ const RESERVED_CLAIMS = new Set(['iss', 'iat', 'nbf', 'exp', 'vct', 'cnf', '_sd', '_sd_alg']);
24
+
25
+ type MaterializedPayload = Record<string, unknown> & {
26
+ iss: string;
27
+ iat: number;
28
+ vct: string;
29
+ cnf: { jwk: Record<string, unknown> };
30
+ };
31
+
32
+ const validateNonEmptyIssuerValue = (value: string, name: string): void => {
33
+ if (typeof value !== 'string' || value.trim().length === 0) {
34
+ throw new Error(`${name} must be a non-empty string`);
35
+ }
36
+ };
37
+
38
+ const isCanonicalEd25519PublicKey = (value: string): boolean => {
39
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return false;
40
+
41
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
42
+ let buffered = 0;
43
+ let bufferedBits = 0;
44
+ let decodedBytes = 0;
45
+
46
+ for (const character of value) {
47
+ buffered = (buffered << 6) | alphabet.indexOf(character);
48
+ bufferedBits += 6;
49
+
50
+ while (bufferedBits >= 8) {
51
+ bufferedBits -= 8;
52
+ decodedBytes += 1;
53
+ buffered &= (1 << bufferedBits) - 1;
54
+ }
55
+ }
56
+
57
+ return decodedBytes === 32 && buffered === 0;
58
+ };
59
+
60
+ const validateHolderPublicJwk = (holderPublicJwk: Record<string, unknown>): void => {
61
+ if (
62
+ holderPublicJwk &&
63
+ typeof holderPublicJwk === 'object' &&
64
+ Object.prototype.hasOwnProperty.call(holderPublicJwk, 'd')
65
+ ) {
66
+ throw new Error('holder key must be public and must not include private key material');
67
+ }
68
+
69
+ if (
70
+ !holderPublicJwk ||
71
+ typeof holderPublicJwk !== 'object' ||
72
+ holderPublicJwk.kty !== 'OKP' ||
73
+ holderPublicJwk.crv !== 'Ed25519' ||
74
+ typeof holderPublicJwk.x !== 'string' ||
75
+ !isCanonicalEd25519PublicKey(holderPublicJwk.x)
76
+ ) {
77
+ throw new Error(
78
+ 'holderPublicJwk must be an Ed25519 OKP public JWK with a canonical 32-byte x'
79
+ );
80
+ }
81
+ };
82
+
83
+ const validateReservedClaims = (claims: Record<string, unknown>): void => {
84
+ for (const claim of Object.keys(claims)) {
85
+ if (RESERVED_CLAIMS.has(claim)) {
86
+ throw new Error(`SD-JWT fixture claims cannot declare reserved claim "${claim}"`);
87
+ }
88
+ }
89
+ };
90
+
91
+ const validateSelectivelyDisclosableClaims = (
92
+ claims: Record<string, unknown>,
93
+ selectivelyDisclosable: string[]
94
+ ): void => {
95
+ const claimKeys = new Set(Object.keys(claims));
96
+ const unknownClaims = selectivelyDisclosable.filter(claim => !claimKeys.has(claim));
97
+
98
+ if (unknownClaims.length > 0) {
99
+ throw new Error(
100
+ `SD-JWT fixture declares selectively disclosable claims that do not exist: ${unknownClaims.join(
101
+ ', '
102
+ )}`
103
+ );
104
+ }
105
+ };
106
+
107
+ /**
108
+ * Materializes an SD-JWT VC fixture with a caller-supplied issuer signer and holder key.
109
+ * The returned compact credential is canonical `dc+sd-jwt` and never includes a KB-JWT.
110
+ */
111
+ export const materializeSdJwtVcFixture = async (
112
+ fixture: SdJwtVcFixture,
113
+ options: MaterializeSdJwtVcOptions
114
+ ): Promise<MaterializedSdJwtVcFixture> => {
115
+ validateNonEmptyIssuerValue(options.issuerDid, 'issuerDid');
116
+ validateNonEmptyIssuerValue(options.issuerKid, 'issuerKid');
117
+ validateHolderPublicJwk(options.holderPublicJwk);
118
+ validateReservedClaims(fixture.template.claims);
119
+ validateSelectivelyDisclosableClaims(
120
+ fixture.template.claims,
121
+ fixture.template.selectivelyDisclosable
122
+ );
123
+
124
+ const instance = new SDJwtVcInstance({
125
+ hasher: sha256Hasher,
126
+ hashAlg: 'sha-256',
127
+ saltGenerator: randomSalt,
128
+ signer: options.issuerSigner,
129
+ signAlg: 'EdDSA',
130
+ });
131
+
132
+ const payload: MaterializedPayload = {
133
+ ...fixture.template.claims,
134
+ iss: options.issuerDid,
135
+ iat: options.issuedAt ?? Math.floor(Date.now() / 1000),
136
+ vct: fixture.template.vct,
137
+ cnf: { jwk: options.holderPublicJwk },
138
+ };
139
+
140
+ const compact = await instance.issue<MaterializedPayload>(
141
+ payload,
142
+ // Fixture claims are intentionally dynamic, so TypeScript cannot statically enumerate
143
+ // their disclosure-frame keys. Runtime fixture validation and the SD-JWT library do.
144
+ { _sd: fixture.template.selectivelyDisclosable } as never,
145
+ {
146
+ header: {
147
+ typ: 'dc+sd-jwt',
148
+ alg: 'EdDSA',
149
+ kid: options.issuerKid,
150
+ },
151
+ }
152
+ );
153
+
154
+ return {
155
+ compact,
156
+ envelope: { format: 'dc+sd-jwt', data: compact },
157
+ vct: fixture.template.vct,
158
+ };
159
+ };
package/src/prepare.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { UnsignedVC } from '@learncard/types';
2
2
 
3
- import type { CredentialFixture } from './types';
3
+ import { isSdJwtVcFixture, type LibraryFixture } from './types';
4
4
  import { getFixture } from './registry';
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -138,7 +138,14 @@ const patchSubject = (subject: unknown, subjectDid: string): unknown => {
138
138
  * await wallet.store.LearnCloud.uploadEncrypted(signed);
139
139
  * ```
140
140
  */
141
- export const prepareFixture = (fixture: CredentialFixture, options: PrepareOptions): UnsignedVC => {
141
+ export const prepareFixture = (fixture: LibraryFixture, options: PrepareOptions): UnsignedVC => {
142
+ if (isSdJwtVcFixture(fixture)) {
143
+ throw new Error(
144
+ `Fixture "${fixture.id}" is an SD-JWT VC template. ` +
145
+ 'Use materializeSdJwtVcFixture() instead of prepareFixture().'
146
+ );
147
+ }
148
+
142
149
  const { issuerDid, subjectDid, validFrom, validUntil, freshIds = true } = options;
143
150
 
144
151
  // Deep clone
package/src/registry.ts CHANGED
@@ -1,15 +1,66 @@
1
1
  import type { UnsignedVC, VC } from '@learncard/types';
2
2
 
3
- import type { CredentialFixture, FixtureFilter } from './types';
3
+ import {
4
+ isCredentialFixture,
5
+ isSdJwtVcFixture,
6
+ type CredentialFixture,
7
+ type FixtureFilter,
8
+ type FixtureKind,
9
+ type LibraryFixture,
10
+ type SdJwtVcFixture,
11
+ } from './types';
4
12
  import { ALL_FIXTURES } from './fixtures';
5
13
 
6
14
  // ---------------------------------------------------------------------------
7
15
  // Internal store — fixtures register themselves here via `registerFixture`
8
16
  // ---------------------------------------------------------------------------
9
17
 
10
- const fixtures: CredentialFixture[] = [];
18
+ const fixtures: LibraryFixture[] = [];
11
19
 
12
- const fixtureIndex = new Map<string, CredentialFixture>();
20
+ const fixtureIndex = new Map<string, LibraryFixture>();
21
+
22
+ const validateFixtureIdentity = (fixture: LibraryFixture): void => {
23
+ // Registration is a runtime boundary and may receive untyped fixture data.
24
+ const runtimeKind = (fixture as { kind?: unknown }).kind;
25
+ const runtimeSpec = fixture.spec as string;
26
+
27
+ if (runtimeKind !== undefined && runtimeKind !== 'w3c-vc' && runtimeKind !== 'sd-jwt-vc') {
28
+ throw new Error(`Unsupported fixture kind "${String(runtimeKind)}".`);
29
+ }
30
+
31
+ if (isSdJwtVcFixture(fixture)) {
32
+ if (runtimeSpec !== 'sd-jwt-vc') {
33
+ throw new Error('SD-JWT VC fixtures must use spec "sd-jwt-vc".');
34
+ }
35
+
36
+ if (!fixture.id.startsWith('sd-jwt-vc/')) {
37
+ throw new Error('SD-JWT VC fixture IDs must start with "sd-jwt-vc/".');
38
+ }
39
+
40
+ return;
41
+ }
42
+
43
+ if (runtimeSpec === 'sd-jwt-vc') {
44
+ throw new Error('W3C VC fixtures cannot use spec "sd-jwt-vc".');
45
+ }
46
+
47
+ if (fixture.id.startsWith('sd-jwt-vc/')) {
48
+ throw new Error('The "sd-jwt-vc/" ID prefix is reserved for SD-JWT VC fixtures.');
49
+ }
50
+ };
51
+
52
+ const addFixture = (fixture: LibraryFixture): void => {
53
+ validateFixtureIdentity(fixture);
54
+
55
+ if (fixtureIndex.has(fixture.id)) {
56
+ throw new Error(
57
+ `Duplicate fixture ID: "${fixture.id}". Each fixture must have a unique id.`
58
+ );
59
+ }
60
+
61
+ fixtures.push(fixture);
62
+ fixtureIndex.set(fixture.id, fixture);
63
+ };
13
64
 
14
65
  // ---------------------------------------------------------------------------
15
66
  // Lazy initialization — populate the registry on first query so consumers
@@ -27,8 +78,7 @@ const ensureInitialized = (): void => {
27
78
 
28
79
  for (const fixture of ALL_FIXTURES) {
29
80
  if (!fixtureIndex.has(fixture.id)) {
30
- fixtures.push(fixture);
31
- fixtureIndex.set(fixture.id, fixture);
81
+ addFixture(fixture);
32
82
  }
33
83
  }
34
84
  };
@@ -37,16 +87,11 @@ const ensureInitialized = (): void => {
37
87
  // Registration
38
88
  // ---------------------------------------------------------------------------
39
89
 
40
- export const registerFixture = <T extends UnsignedVC | VC>(fixture: CredentialFixture<T>): void => {
41
- if (fixtureIndex.has(fixture.id)) {
42
- throw new Error(`Duplicate fixture ID: "${fixture.id}". Each fixture must have a unique id.`);
43
- }
44
-
45
- fixtures.push(fixture as CredentialFixture);
46
- fixtureIndex.set(fixture.id, fixture as CredentialFixture);
90
+ export const registerFixture = (fixture: LibraryFixture): void => {
91
+ addFixture(fixture);
47
92
  };
48
93
 
49
- export const registerFixtures = (batch: CredentialFixture[]): void => {
94
+ export const registerFixtures = (batch: LibraryFixture[]): void => {
50
95
  for (const fixture of batch) {
51
96
  registerFixture(fixture);
52
97
  }
@@ -68,7 +113,15 @@ export const resetRegistry = (): void => {
68
113
 
69
114
  const toArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);
70
115
 
71
- const matchesFilter = (fixture: CredentialFixture, filter: FixtureFilter): boolean => {
116
+ const fixtureKind = (fixture: LibraryFixture): FixtureKind => fixture.kind ?? 'w3c-vc';
117
+
118
+ const matchesFilter = (fixture: LibraryFixture, filter: FixtureFilter): boolean => {
119
+ if (filter.kind !== undefined) {
120
+ const kinds = toArray(filter.kind);
121
+
122
+ if (!kinds.includes(fixtureKind(fixture))) return false;
123
+ }
124
+
72
125
  if (filter.spec !== undefined) {
73
126
  const specs = toArray(filter.spec);
74
127
 
@@ -122,13 +175,20 @@ const matchesFilter = (fixture: CredentialFixture, filter: FixtureFilter): boole
122
175
  // Public query API
123
176
  // ---------------------------------------------------------------------------
124
177
 
125
- export const getAllFixtures = (): readonly CredentialFixture[] => {
178
+ export const getAllFixtures = (): readonly LibraryFixture[] => {
126
179
  ensureInitialized();
127
180
 
128
181
  return fixtures;
129
182
  };
130
183
 
131
- export const getFixture = (id: string): CredentialFixture => {
184
+ type FixtureForId<Id extends string> = Id extends `sd-jwt-vc/${string}`
185
+ ? SdJwtVcFixture
186
+ : string extends Id
187
+ ? LibraryFixture
188
+ : CredentialFixture;
189
+
190
+ export function getFixture<Id extends string>(id: Id): FixtureForId<Id>;
191
+ export function getFixture(id: string): LibraryFixture {
132
192
  ensureInitialized();
133
193
 
134
194
  const fixture = fixtureIndex.get(id);
@@ -140,32 +200,33 @@ export const getFixture = (id: string): CredentialFixture => {
140
200
  }
141
201
 
142
202
  return fixture;
143
- };
203
+ }
144
204
 
145
- export const findFixture = (id: string): CredentialFixture | undefined => {
205
+ export function findFixture<Id extends string>(id: Id): FixtureForId<Id> | undefined;
206
+ export function findFixture(id: string): LibraryFixture | undefined {
146
207
  ensureInitialized();
147
208
 
148
209
  return fixtureIndex.get(id);
149
- };
210
+ }
150
211
 
151
- export const getFixtures = (filter: FixtureFilter): CredentialFixture[] => {
212
+ export const getFixtures = (filter: FixtureFilter): LibraryFixture[] => {
152
213
  ensureInitialized();
153
214
 
154
215
  return fixtures.filter(f => matchesFilter(f, filter));
155
216
  };
156
217
 
157
- export const getUnsignedFixtures = (
158
- filter?: FixtureFilter
159
- ): CredentialFixture<UnsignedVC>[] =>
160
- getFixtures({ ...filter, signed: false }) as CredentialFixture<UnsignedVC>[];
218
+ export const getUnsignedFixtures = (filter?: FixtureFilter): CredentialFixture<UnsignedVC>[] =>
219
+ getFixtures({ ...filter, signed: false }).filter(
220
+ isCredentialFixture
221
+ ) as CredentialFixture<UnsignedVC>[];
161
222
 
162
223
  export const getSignedFixtures = (filter?: FixtureFilter): CredentialFixture<VC>[] =>
163
- getFixtures({ ...filter, signed: true }) as CredentialFixture<VC>[];
224
+ getFixtures({ ...filter, signed: true }).filter(isCredentialFixture) as CredentialFixture<VC>[];
164
225
 
165
- export const getValidFixtures = (filter?: FixtureFilter): CredentialFixture[] =>
226
+ export const getValidFixtures = (filter?: FixtureFilter): LibraryFixture[] =>
166
227
  getFixtures({ ...filter, validity: 'valid' });
167
228
 
168
- export const getInvalidFixtures = (filter?: FixtureFilter): CredentialFixture[] =>
229
+ export const getInvalidFixtures = (filter?: FixtureFilter): LibraryFixture[] =>
169
230
  getFixtures({ ...filter, validity: ['invalid', 'tampered'] });
170
231
 
171
232
  // ---------------------------------------------------------------------------
package/src/types.ts CHANGED
@@ -11,6 +11,7 @@ export const CREDENTIAL_SPECS = [
11
11
  'obv3',
12
12
  'clr-v2',
13
13
  'europass',
14
+ 'sd-jwt-vc',
14
15
  'custom',
15
16
  ] as const;
16
17
 
@@ -63,6 +64,8 @@ export const CREDENTIAL_FEATURES = [
63
64
  'attachments',
64
65
  'associations',
65
66
  'nested-credentials',
67
+ 'selective-disclosure',
68
+ 'holder-binding',
66
69
  ] as const;
67
70
 
68
71
  export type CredentialFeature = (typeof CREDENTIAL_FEATURES)[number];
@@ -71,12 +74,7 @@ export type CredentialFeature = (typeof CREDENTIAL_FEATURES)[number];
71
74
  // Fixture Source — where did this credential come from?
72
75
  // ---------------------------------------------------------------------------
73
76
 
74
- export const FIXTURE_SOURCES = [
75
- 'spec-example',
76
- 'plugfest',
77
- 'real-world',
78
- 'synthetic',
79
- ] as const;
77
+ export const FIXTURE_SOURCES = ['spec-example', 'plugfest', 'real-world', 'synthetic'] as const;
80
78
 
81
79
  export type FixtureSource = (typeof FIXTURE_SOURCES)[number];
82
80
 
@@ -88,14 +86,18 @@ export const FIXTURE_VALIDITIES = ['valid', 'invalid', 'tampered'] as const;
88
86
 
89
87
  export type FixtureValidity = (typeof FIXTURE_VALIDITIES)[number];
90
88
 
89
+ export const FIXTURE_KINDS = ['w3c-vc', 'sd-jwt-vc'] as const;
90
+
91
+ export type FixtureKind = (typeof FIXTURE_KINDS)[number];
92
+
91
93
  // Used for intentionally malformed fixtures that violate the UnsignedVC type
92
94
  export type InvalidCredential = Record<string, unknown>;
93
95
 
94
96
  // ---------------------------------------------------------------------------
95
- // CredentialFixture — the core type wrapping a credential with metadata
97
+ // BaseCredentialFixture — metadata shared by all fixture formats
96
98
  // ---------------------------------------------------------------------------
97
99
 
98
- export interface CredentialFixture<T extends UnsignedVC | VC = UnsignedVC> {
100
+ export interface BaseCredentialFixture {
99
101
  /** Unique fixture ID, e.g. 'obv3/minimal-badge' */
100
102
  id: string;
101
103
 
@@ -123,21 +125,60 @@ export interface CredentialFixture<T extends UnsignedVC | VC = UnsignedVC> {
123
125
  /** Whether this fixture is intentionally valid, invalid, or tampered */
124
126
  validity: FixtureValidity;
125
127
 
128
+ /** Additional free-form tags for ad-hoc filtering */
129
+ tags?: string[];
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // CredentialFixture — a W3C Verifiable Credential fixture
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export interface CredentialFixture<T extends UnsignedVC | VC = UnsignedVC>
137
+ extends BaseCredentialFixture {
138
+ /** Omitted for backwards compatibility; W3C VC is the default fixture kind */
139
+ kind?: 'w3c-vc';
140
+
141
+ /** SD-JWT VC uses a separate template fixture variant */
142
+ spec: Exclude<CredentialSpec, 'sd-jwt-vc'>;
143
+
126
144
  /** The actual credential JSON */
127
145
  credential: T;
128
146
 
129
147
  /** Zod validator this credential should pass (if valid) or fail (if invalid) */
130
148
  validator?: z.ZodType;
149
+ }
131
150
 
132
- /** Additional free-form tags for ad-hoc filtering */
133
- tags?: string[];
151
+ export interface SdJwtVcTemplate {
152
+ format: 'dc+sd-jwt';
153
+ vct: string;
154
+ claims: Record<string, unknown>;
155
+ selectivelyDisclosable: string[];
156
+ }
157
+
158
+ export interface SdJwtVcFixture extends BaseCredentialFixture {
159
+ kind: 'sd-jwt-vc';
160
+ id: `sd-jwt-vc/${string}`;
161
+ spec: 'sd-jwt-vc';
162
+ signed: false;
163
+ template: SdJwtVcTemplate;
134
164
  }
135
165
 
166
+ export type LibraryFixture = CredentialFixture | SdJwtVcFixture;
167
+
168
+ export const isSdJwtVcFixture = (fixture: LibraryFixture): fixture is SdJwtVcFixture =>
169
+ fixture.kind === 'sd-jwt-vc';
170
+
171
+ export const isCredentialFixture = (fixture: LibraryFixture): fixture is CredentialFixture =>
172
+ !isSdJwtVcFixture(fixture);
173
+
136
174
  // ---------------------------------------------------------------------------
137
175
  // Filter — used to query the registry
138
176
  // ---------------------------------------------------------------------------
139
177
 
140
178
  export interface FixtureFilter {
179
+ /** Match any of the given fixture kinds (omitted W3C kind is treated as w3c-vc) */
180
+ kind?: FixtureKind | FixtureKind[];
181
+
141
182
  /** Match any of the given specs */
142
183
  spec?: CredentialSpec | CredentialSpec[];
143
184