@learncard/credential-library 1.0.10 → 1.0.11
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/README.md +104 -98
- package/package.json +49 -39
- package/src/__tests__/issuance.test.ts +51 -0
- package/src/__tests__/registry.test.ts +457 -0
- package/src/fixtures/boost/basic.ts +50 -0
- package/src/fixtures/boost/boost-id.ts +59 -0
- package/src/fixtures/boost/community-award.ts +96 -0
- package/src/fixtures/boost/delegate.ts +60 -0
- package/src/fixtures/boost/with-skills.ts +80 -0
- package/src/fixtures/clr/competency-aligned.ts +737 -0
- package/src/fixtures/clr/great-plains-full.ts +7841 -0
- package/src/fixtures/clr/minimal.ts +53 -0
- package/src/fixtures/clr/multi-achievement.ts +125 -0
- package/src/fixtures/clr/nd-student-transcript.ts +411 -0
- package/src/fixtures/clr/university-transcript.ts +328 -0
- package/src/fixtures/clr/westbridge-full.ts +2524 -0
- package/src/fixtures/index.ts +159 -0
- package/src/fixtures/invalid/empty-type.ts +27 -0
- package/src/fixtures/invalid/missing-context.ts +27 -0
- package/src/fixtures/invalid/missing-issuer.ts +27 -0
- package/src/fixtures/obv3/1edtech-full.ts +237 -0
- package/src/fixtures/obv3/course-completion.ts +82 -0
- package/src/fixtures/obv3/endorsement.ts +58 -0
- package/src/fixtures/obv3/full-badge.ts +121 -0
- package/src/fixtures/obv3/k12-diploma.ts +92 -0
- package/src/fixtures/obv3/micro-credential.ts +170 -0
- package/src/fixtures/obv3/minimal-badge.ts +40 -0
- package/src/fixtures/obv3/plugfest-jff2.ts +57 -0
- package/src/fixtures/obv3/professional-cert.ts +102 -0
- package/src/fixtures/obv3/with-alignment.ts +62 -0
- package/src/fixtures/obv3/with-endorsement.ts +62 -0
- package/src/fixtures/render-method/render-method-example.ts +45 -0
- package/src/fixtures/vc-v1/alumni-credential.ts +37 -0
- package/src/fixtures/vc-v1/basic.ts +27 -0
- package/src/fixtures/vc-v1/with-status.ts +44 -0
- package/src/fixtures/vc-v2/basic.ts +27 -0
- package/src/fixtures/vc-v2/digital-id.ts +78 -0
- package/src/fixtures/vc-v2/education-degree.ts +77 -0
- package/src/fixtures/vc-v2/employment-credential.ts +64 -0
- package/src/fixtures/vc-v2/license-credential.ts +79 -0
- package/src/fixtures/vc-v2/membership-credential.ts +62 -0
- package/src/fixtures/vc-v2/multiple-subjects.ts +39 -0
- package/src/fixtures/vc-v2/with-evidence.ts +50 -0
- package/src/index.ts +45 -0
- package/src/prepare.ts +212 -0
- package/src/registry.ts +209 -0
- 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);
|
package/src/registry.ts
ADDED
|
@@ -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
|
+
}
|