@learncard/credential-library 1.0.10 → 1.0.12

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 (47) hide show
  1. package/README.md +104 -98
  2. package/package.json +49 -39
  3. package/src/__tests__/issuance.test.ts +51 -0
  4. package/src/__tests__/registry.test.ts +457 -0
  5. package/src/fixtures/boost/basic.ts +50 -0
  6. package/src/fixtures/boost/boost-id.ts +59 -0
  7. package/src/fixtures/boost/community-award.ts +96 -0
  8. package/src/fixtures/boost/delegate.ts +60 -0
  9. package/src/fixtures/boost/with-skills.ts +80 -0
  10. package/src/fixtures/clr/competency-aligned.ts +737 -0
  11. package/src/fixtures/clr/great-plains-full.ts +7841 -0
  12. package/src/fixtures/clr/minimal.ts +53 -0
  13. package/src/fixtures/clr/multi-achievement.ts +125 -0
  14. package/src/fixtures/clr/nd-student-transcript.ts +411 -0
  15. package/src/fixtures/clr/university-transcript.ts +328 -0
  16. package/src/fixtures/clr/westbridge-full.ts +2524 -0
  17. package/src/fixtures/index.ts +159 -0
  18. package/src/fixtures/invalid/empty-type.ts +27 -0
  19. package/src/fixtures/invalid/missing-context.ts +27 -0
  20. package/src/fixtures/invalid/missing-issuer.ts +27 -0
  21. package/src/fixtures/obv3/1edtech-full.ts +237 -0
  22. package/src/fixtures/obv3/course-completion.ts +82 -0
  23. package/src/fixtures/obv3/endorsement.ts +58 -0
  24. package/src/fixtures/obv3/full-badge.ts +121 -0
  25. package/src/fixtures/obv3/k12-diploma.ts +92 -0
  26. package/src/fixtures/obv3/micro-credential.ts +170 -0
  27. package/src/fixtures/obv3/minimal-badge.ts +40 -0
  28. package/src/fixtures/obv3/plugfest-jff2.ts +57 -0
  29. package/src/fixtures/obv3/professional-cert.ts +102 -0
  30. package/src/fixtures/obv3/with-alignment.ts +62 -0
  31. package/src/fixtures/obv3/with-endorsement.ts +62 -0
  32. package/src/fixtures/render-method/render-method-example.ts +45 -0
  33. package/src/fixtures/vc-v1/alumni-credential.ts +37 -0
  34. package/src/fixtures/vc-v1/basic.ts +27 -0
  35. package/src/fixtures/vc-v1/with-status.ts +44 -0
  36. package/src/fixtures/vc-v2/basic.ts +27 -0
  37. package/src/fixtures/vc-v2/digital-id.ts +78 -0
  38. package/src/fixtures/vc-v2/education-degree.ts +77 -0
  39. package/src/fixtures/vc-v2/employment-credential.ts +64 -0
  40. package/src/fixtures/vc-v2/license-credential.ts +79 -0
  41. package/src/fixtures/vc-v2/membership-credential.ts +62 -0
  42. package/src/fixtures/vc-v2/multiple-subjects.ts +39 -0
  43. package/src/fixtures/vc-v2/with-evidence.ts +50 -0
  44. package/src/index.ts +45 -0
  45. package/src/prepare.ts +212 -0
  46. package/src/registry.ts +209 -0
  47. package/src/types.ts +164 -0
@@ -0,0 +1,50 @@
1
+ import { UnsignedVCValidator } from '@learncard/types';
2
+
3
+ import type { CredentialFixture } from '../../types';
4
+
5
+ export const vcV2WithEvidence: CredentialFixture = {
6
+ id: 'vc-v2/with-evidence',
7
+ name: 'VC v2 with Evidence',
8
+ description: 'W3C VCDM v2 credential with evidence array documenting supporting artifacts',
9
+ spec: 'vc-v2',
10
+ profile: 'generic',
11
+ features: ['evidence'],
12
+ source: 'synthetic',
13
+ signed: false,
14
+ validity: 'valid',
15
+ validator: UnsignedVCValidator,
16
+
17
+ credential: {
18
+ '@context': [
19
+ 'https://www.w3.org/ns/credentials/v2',
20
+ 'https://www.w3.org/ns/credentials/examples/v2',
21
+ ],
22
+ id: 'urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890',
23
+ type: ['VerifiableCredential'],
24
+ issuer: {
25
+ id: 'did:example:issuer123',
26
+ name: 'Example University',
27
+ },
28
+ validFrom: '2023-09-01T00:00:00Z',
29
+ credentialSubject: {
30
+ id: 'did:example:student789',
31
+ degree: {
32
+ type: 'MasterDegree',
33
+ name: 'Master of Science in Data Engineering',
34
+ },
35
+ },
36
+ evidence: [
37
+ {
38
+ type: ['Evidence', 'DocumentVerification'],
39
+ name: 'Transcript Review',
40
+ description: 'Official transcript reviewed and verified by registrar office.',
41
+ },
42
+ {
43
+ type: ['Evidence', 'ThesisDefense'],
44
+ name: 'Thesis Defense',
45
+ narrative:
46
+ 'Student successfully defended thesis titled "Scalable Data Pipelines" on 2023-06-20.',
47
+ },
48
+ ],
49
+ },
50
+ };
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // Types
2
+ export type {
3
+ CredentialSpec,
4
+ CredentialProfile,
5
+ CredentialFeature,
6
+ FixtureSource,
7
+ FixtureValidity,
8
+ CredentialFixture,
9
+ FixtureFilter,
10
+ InvalidCredential,
11
+ } from './types';
12
+
13
+ export {
14
+ CREDENTIAL_SPECS,
15
+ CREDENTIAL_PROFILES,
16
+ CREDENTIAL_FEATURES,
17
+ FIXTURE_SOURCES,
18
+ FIXTURE_VALIDITIES,
19
+ } from './types';
20
+
21
+ // Registry (query API + mutation)
22
+ export {
23
+ registerFixture,
24
+ registerFixtures,
25
+ resetRegistry,
26
+ getAllFixtures,
27
+ getFixture,
28
+ findFixture,
29
+ getFixtures,
30
+ getUnsignedFixtures,
31
+ getSignedFixtures,
32
+ getValidFixtures,
33
+ getInvalidFixtures,
34
+ getStats,
35
+ } from './registry';
36
+
37
+ export type { RegistryStats } from './registry';
38
+
39
+ // Prepare — bridge fixtures to wallet issuance
40
+ export { prepareFixture, prepareFixtureById } from './prepare';
41
+
42
+ export type { PrepareOptions } from './prepare';
43
+
44
+ // Fixtures — importing this module registers all fixtures in the registry
45
+ export * from './fixtures';
package/src/prepare.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type { UnsignedVC } from '@learncard/types';
2
+
3
+ import type { CredentialFixture } from './types';
4
+ import { getFixture } from './registry';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // PrepareOptions — what to inject into a fixture for real issuance
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface PrepareOptions {
11
+ /** Issuer DID — e.g. from wallet.id.did() */
12
+ issuerDid: string;
13
+
14
+ /** Recipient DID — who the credential is being issued to */
15
+ subjectDid?: string;
16
+
17
+ /** Override validFrom date (defaults to now) */
18
+ validFrom?: string;
19
+
20
+ /** Override validUntil / expirationDate */
21
+ validUntil?: string;
22
+
23
+ /** Generate fresh UUIDs for id fields (defaults to true) */
24
+ freshIds?: boolean;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Deep clone + patch helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const generateUuid = (): string => {
32
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
33
+ return crypto.randomUUID();
34
+ }
35
+
36
+ throw new Error(
37
+ 'crypto.randomUUID is not available in this environment. ' +
38
+ 'Please use a modern runtime with Web Crypto API support for secure UUID generation.'
39
+ );
40
+ };
41
+
42
+ const patchIds = (
43
+ obj: Record<string, unknown>,
44
+ idMap: Map<string, string>
45
+ ): Record<string, unknown> => {
46
+ const result: Record<string, unknown> = {};
47
+
48
+ for (const [key, value] of Object.entries(obj)) {
49
+ if (key === 'id' && typeof value === 'string' && value.startsWith('urn:uuid:')) {
50
+ // Reuse the same replacement for every occurrence of the same source id so
51
+ // internal CLR references stay aligned after regeneration.
52
+ const patchedId = idMap.get(value) ?? `urn:uuid:${generateUuid()}`;
53
+ if (!idMap.has(value)) {
54
+ idMap.set(value, patchedId);
55
+ }
56
+ result[key] = patchedId;
57
+ } else if (Array.isArray(value)) {
58
+ result[key] = value.map(item =>
59
+ item && typeof item === 'object' && !Array.isArray(item)
60
+ ? patchIds(item as Record<string, unknown>, idMap)
61
+ : item
62
+ );
63
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
64
+ result[key] = patchIds(value as Record<string, unknown>, idMap);
65
+ } else {
66
+ result[key] = value;
67
+ }
68
+ }
69
+
70
+ return result;
71
+ };
72
+
73
+ const remapUuidReferences = (value: unknown, idMap: Map<string, string>): unknown => {
74
+ if (typeof value === 'string') {
75
+ // Rewrite explicit references that still point at the original ids.
76
+ return idMap.get(value) ?? value;
77
+ }
78
+
79
+ if (Array.isArray(value)) {
80
+ return value.map(item => remapUuidReferences(item, idMap));
81
+ }
82
+
83
+ if (value && typeof value === 'object') {
84
+ return Object.fromEntries(
85
+ Object.entries(value as Record<string, unknown>).map(([key, nestedValue]) => [
86
+ key,
87
+ remapUuidReferences(nestedValue, idMap),
88
+ ])
89
+ );
90
+ }
91
+
92
+ return value;
93
+ };
94
+
95
+ const patchIssuer = (issuer: unknown, issuerDid: string): string | Record<string, unknown> => {
96
+ if (typeof issuer === 'string') {
97
+ return issuerDid;
98
+ }
99
+
100
+ if (issuer && typeof issuer === 'object') {
101
+ return { ...(issuer as Record<string, unknown>), id: issuerDid };
102
+ }
103
+
104
+ return issuerDid;
105
+ };
106
+
107
+ const patchSubject = (subject: unknown, subjectDid: string): unknown => {
108
+ if (Array.isArray(subject)) {
109
+ return subject.map(s => patchSubject(s, subjectDid));
110
+ }
111
+
112
+ if (subject && typeof subject === 'object') {
113
+ return { ...(subject as Record<string, unknown>), id: subjectDid };
114
+ }
115
+
116
+ return subject;
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // prepareFixture — make a fixture ready for wallet.invoke.issueCredential()
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Takes a fixture's credential and returns a fresh unsigned VC with real DIDs,
125
+ * current timestamps, and fresh UUIDs — ready to pass to
126
+ * `wallet.invoke.issueCredential()`.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * import { getFixture, prepareFixture } from '@learncard/credential-library';
131
+ *
132
+ * const fixture = getFixture('obv3/full-badge');
133
+ * const unsigned = prepareFixture(fixture, {
134
+ * issuerDid: wallet.id.did(),
135
+ * subjectDid: recipientDid,
136
+ * });
137
+ * const signed = await wallet.invoke.issueCredential(unsigned);
138
+ * await wallet.store.LearnCloud.uploadEncrypted(signed);
139
+ * ```
140
+ */
141
+ export const prepareFixture = (fixture: CredentialFixture, options: PrepareOptions): UnsignedVC => {
142
+ const { issuerDid, subjectDid, validFrom, validUntil, freshIds = true } = options;
143
+
144
+ // Deep clone
145
+ let credential = JSON.parse(JSON.stringify(fixture.credential)) as Record<string, unknown>;
146
+
147
+ // Patch UUIDs
148
+ if (freshIds) {
149
+ const idMap = new Map<string, string>();
150
+ // First replace generated ids, then walk the credential again to update
151
+ // any references that target those ids.
152
+ credential = patchIds(credential, idMap);
153
+ credential = remapUuidReferences(credential, idMap) as Record<string, unknown>;
154
+ }
155
+
156
+ // Patch issuer
157
+ credential.issuer = patchIssuer(credential.issuer, issuerDid);
158
+
159
+ // Patch subject DID
160
+ if (subjectDid && credential.credentialSubject) {
161
+ credential.credentialSubject = patchSubject(credential.credentialSubject, subjectDid);
162
+ }
163
+
164
+ // Patch dates
165
+ const now = new Date().toISOString();
166
+
167
+ if (credential.validFrom !== undefined || credential.issuanceDate !== undefined) {
168
+ if (credential.validFrom !== undefined) {
169
+ credential.validFrom = validFrom ?? now;
170
+ }
171
+
172
+ if (credential.issuanceDate !== undefined) {
173
+ credential.issuanceDate = validFrom ?? now;
174
+ }
175
+ } else {
176
+ // Default to validFrom for v2-style credentials
177
+ credential.validFrom = validFrom ?? now;
178
+ }
179
+
180
+ if (validUntil !== undefined) {
181
+ if (credential.validUntil !== undefined) {
182
+ credential.validUntil = validUntil;
183
+ } else if (credential.expirationDate !== undefined) {
184
+ credential.expirationDate = validUntil;
185
+ } else {
186
+ credential.validUntil = validUntil;
187
+ }
188
+ }
189
+
190
+ return credential as UnsignedVC;
191
+ };
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // prepareFixtureById — convenience wrapper
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /**
198
+ * Shorthand that combines getFixture + prepareFixture.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * import { prepareFixtureById } from '@learncard/credential-library';
203
+ *
204
+ * const unsigned = prepareFixtureById('boost/basic', {
205
+ * issuerDid: wallet.id.did(),
206
+ * subjectDid: recipientDid,
207
+ * });
208
+ * const signed = await wallet.invoke.issueCredential(unsigned);
209
+ * ```
210
+ */
211
+ export const prepareFixtureById = (fixtureId: string, options: PrepareOptions): UnsignedVC =>
212
+ prepareFixture(getFixture(fixtureId), options);
@@ -0,0 +1,209 @@
1
+ import type { UnsignedVC, VC } from '@learncard/types';
2
+
3
+ import type { CredentialFixture, FixtureFilter } from './types';
4
+ import { ALL_FIXTURES } from './fixtures';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Internal store — fixtures register themselves here via `registerFixture`
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const fixtures: CredentialFixture[] = [];
11
+
12
+ const fixtureIndex = new Map<string, CredentialFixture>();
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Lazy initialization — populate the registry on first query so consumers
16
+ // don't need to rely on import side effects (keeps `sideEffects: false`
17
+ // truthful). The ALL_FIXTURES import above is pure data — no mutation
18
+ // happens until ensureInitialized() is called.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ let initialized = false;
22
+
23
+ const ensureInitialized = (): void => {
24
+ if (initialized) return;
25
+
26
+ initialized = true;
27
+
28
+ for (const fixture of ALL_FIXTURES) {
29
+ if (!fixtureIndex.has(fixture.id)) {
30
+ fixtures.push(fixture);
31
+ fixtureIndex.set(fixture.id, fixture);
32
+ }
33
+ }
34
+ };
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Registration
38
+ // ---------------------------------------------------------------------------
39
+
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);
47
+ };
48
+
49
+ export const registerFixtures = (batch: CredentialFixture[]): void => {
50
+ for (const fixture of batch) {
51
+ registerFixture(fixture);
52
+ }
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Reset — for test isolation (vitest watch, jest --watch)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export const resetRegistry = (): void => {
60
+ fixtures.length = 0;
61
+ fixtureIndex.clear();
62
+ initialized = false;
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const toArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);
70
+
71
+ const matchesFilter = (fixture: CredentialFixture, filter: FixtureFilter): boolean => {
72
+ if (filter.spec !== undefined) {
73
+ const specs = toArray(filter.spec);
74
+
75
+ if (!specs.includes(fixture.spec)) return false;
76
+ }
77
+
78
+ if (filter.profile !== undefined) {
79
+ const profiles = toArray(filter.profile);
80
+
81
+ if (!profiles.includes(fixture.profile)) return false;
82
+ }
83
+
84
+ if (filter.features !== undefined) {
85
+ for (const feat of filter.features) {
86
+ if (!fixture.features.includes(feat)) return false;
87
+ }
88
+ }
89
+
90
+ if (filter.featuresAny !== undefined) {
91
+ const hasAny = filter.featuresAny.some(feat => fixture.features.includes(feat));
92
+
93
+ if (!hasAny) return false;
94
+ }
95
+
96
+ if (filter.signed !== undefined) {
97
+ if (fixture.signed !== filter.signed) return false;
98
+ }
99
+
100
+ if (filter.validity !== undefined) {
101
+ const validities = toArray(filter.validity);
102
+
103
+ if (!validities.includes(fixture.validity)) return false;
104
+ }
105
+
106
+ if (filter.source !== undefined) {
107
+ const sources = toArray(filter.source);
108
+
109
+ if (!sources.includes(fixture.source)) return false;
110
+ }
111
+
112
+ if (filter.tags !== undefined) {
113
+ for (const tag of filter.tags) {
114
+ if (!fixture.tags?.includes(tag)) return false;
115
+ }
116
+ }
117
+
118
+ return true;
119
+ };
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Public query API
123
+ // ---------------------------------------------------------------------------
124
+
125
+ export const getAllFixtures = (): readonly CredentialFixture[] => {
126
+ ensureInitialized();
127
+
128
+ return fixtures;
129
+ };
130
+
131
+ export const getFixture = (id: string): CredentialFixture => {
132
+ ensureInitialized();
133
+
134
+ const fixture = fixtureIndex.get(id);
135
+
136
+ if (!fixture) {
137
+ throw new Error(
138
+ `Fixture "${id}" not found. Available: ${[...fixtureIndex.keys()].join(', ')}`
139
+ );
140
+ }
141
+
142
+ return fixture;
143
+ };
144
+
145
+ export const findFixture = (id: string): CredentialFixture | undefined => {
146
+ ensureInitialized();
147
+
148
+ return fixtureIndex.get(id);
149
+ };
150
+
151
+ export const getFixtures = (filter: FixtureFilter): CredentialFixture[] => {
152
+ ensureInitialized();
153
+
154
+ return fixtures.filter(f => matchesFilter(f, filter));
155
+ };
156
+
157
+ export const getUnsignedFixtures = (
158
+ filter?: FixtureFilter
159
+ ): CredentialFixture<UnsignedVC>[] =>
160
+ getFixtures({ ...filter, signed: false }) as CredentialFixture<UnsignedVC>[];
161
+
162
+ export const getSignedFixtures = (filter?: FixtureFilter): CredentialFixture<VC>[] =>
163
+ getFixtures({ ...filter, signed: true }) as CredentialFixture<VC>[];
164
+
165
+ export const getValidFixtures = (filter?: FixtureFilter): CredentialFixture[] =>
166
+ getFixtures({ ...filter, validity: 'valid' });
167
+
168
+ export const getInvalidFixtures = (filter?: FixtureFilter): CredentialFixture[] =>
169
+ getFixtures({ ...filter, validity: ['invalid', 'tampered'] });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Stats — useful for debugging / README generation
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export interface RegistryStats {
176
+ total: number;
177
+ bySpec: Record<string, number>;
178
+ byProfile: Record<string, number>;
179
+ byValidity: Record<string, number>;
180
+ signed: number;
181
+ unsigned: number;
182
+ }
183
+
184
+ export const getStats = (): RegistryStats => {
185
+ ensureInitialized();
186
+
187
+ const stats: RegistryStats = {
188
+ total: fixtures.length,
189
+ bySpec: {},
190
+ byProfile: {},
191
+ byValidity: {},
192
+ signed: 0,
193
+ unsigned: 0,
194
+ };
195
+
196
+ for (const f of fixtures) {
197
+ stats.bySpec[f.spec] = (stats.bySpec[f.spec] ?? 0) + 1;
198
+ stats.byProfile[f.profile] = (stats.byProfile[f.profile] ?? 0) + 1;
199
+ stats.byValidity[f.validity] = (stats.byValidity[f.validity] ?? 0) + 1;
200
+
201
+ if (f.signed) {
202
+ stats.signed++;
203
+ } else {
204
+ stats.unsigned++;
205
+ }
206
+ }
207
+
208
+ return stats;
209
+ };
package/src/types.ts ADDED
@@ -0,0 +1,164 @@
1
+ import type { z } from 'zod';
2
+ import type { UnsignedVC, VC } from '@learncard/types';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Credential Spec — which standard does this credential conform to?
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export const CREDENTIAL_SPECS = [
9
+ 'vc-v1',
10
+ 'vc-v2',
11
+ 'obv3',
12
+ 'clr-v2',
13
+ 'europass',
14
+ 'custom',
15
+ ] as const;
16
+
17
+ export type CredentialSpec = (typeof CREDENTIAL_SPECS)[number];
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Credential Profile — what kind of credential is it?
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export const CREDENTIAL_PROFILES = [
24
+ 'badge',
25
+ 'diploma',
26
+ 'certificate',
27
+ 'id',
28
+ 'membership',
29
+ 'license',
30
+ 'micro-credential',
31
+ 'course',
32
+ 'degree',
33
+ 'boost',
34
+ 'boost-id',
35
+ 'delegate',
36
+ 'endorsement',
37
+ 'learner-record',
38
+ 'generic',
39
+ ] as const;
40
+
41
+ export type CredentialProfile = (typeof CREDENTIAL_PROFILES)[number];
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Credential Feature — what optional VC features does this fixture exercise?
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export const CREDENTIAL_FEATURES = [
48
+ 'evidence',
49
+ 'alignment',
50
+ 'endorsement',
51
+ 'expiration',
52
+ 'status',
53
+ 'multiple-subjects',
54
+ 'multiple-proofs',
55
+ 'refresh-service',
56
+ 'terms-of-use',
57
+ 'credential-schema',
58
+ 'image',
59
+ 'results',
60
+ 'source',
61
+ 'skills',
62
+ 'display',
63
+ 'attachments',
64
+ 'associations',
65
+ 'nested-credentials',
66
+ ] as const;
67
+
68
+ export type CredentialFeature = (typeof CREDENTIAL_FEATURES)[number];
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Fixture Source — where did this credential come from?
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export const FIXTURE_SOURCES = [
75
+ 'spec-example',
76
+ 'plugfest',
77
+ 'real-world',
78
+ 'synthetic',
79
+ ] as const;
80
+
81
+ export type FixtureSource = (typeof FIXTURE_SOURCES)[number];
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Fixture Validity — is this intentionally valid or invalid?
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export const FIXTURE_VALIDITIES = ['valid', 'invalid', 'tampered'] as const;
88
+
89
+ export type FixtureValidity = (typeof FIXTURE_VALIDITIES)[number];
90
+
91
+ // Used for intentionally malformed fixtures that violate the UnsignedVC type
92
+ export type InvalidCredential = Record<string, unknown>;
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // CredentialFixture — the core type wrapping a credential with metadata
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export interface CredentialFixture<T extends UnsignedVC | VC = UnsignedVC> {
99
+ /** Unique fixture ID, e.g. 'obv3/minimal-badge' */
100
+ id: string;
101
+
102
+ /** Human-readable name */
103
+ name: string;
104
+
105
+ /** What this fixture tests or demonstrates */
106
+ description: string;
107
+
108
+ /** Which spec it conforms to */
109
+ spec: CredentialSpec;
110
+
111
+ /** What kind of credential this represents */
112
+ profile: CredentialProfile;
113
+
114
+ /** Which optional VC features this fixture exercises */
115
+ features: CredentialFeature[];
116
+
117
+ /** Where this credential came from */
118
+ source: FixtureSource;
119
+
120
+ /** Whether this is a signed (VC with proof) or unsigned credential */
121
+ signed: boolean;
122
+
123
+ /** Whether this fixture is intentionally valid, invalid, or tampered */
124
+ validity: FixtureValidity;
125
+
126
+ /** The actual credential JSON */
127
+ credential: T;
128
+
129
+ /** Zod validator this credential should pass (if valid) or fail (if invalid) */
130
+ validator?: z.ZodType;
131
+
132
+ /** Additional free-form tags for ad-hoc filtering */
133
+ tags?: string[];
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Filter — used to query the registry
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export interface FixtureFilter {
141
+ /** Match any of the given specs */
142
+ spec?: CredentialSpec | CredentialSpec[];
143
+
144
+ /** Match any of the given profiles */
145
+ profile?: CredentialProfile | CredentialProfile[];
146
+
147
+ /** Must have ALL of these features */
148
+ features?: CredentialFeature[];
149
+
150
+ /** Must have ANY of these features */
151
+ featuresAny?: CredentialFeature[];
152
+
153
+ /** Filter by signed status */
154
+ signed?: boolean;
155
+
156
+ /** Filter by validity */
157
+ validity?: FixtureValidity | FixtureValidity[];
158
+
159
+ /** Match any of the given sources */
160
+ source?: FixtureSource | FixtureSource[];
161
+
162
+ /** Must have ALL of these tags */
163
+ tags?: string[];
164
+ }