@learncard/credential-library 2.0.4 → 2.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.
- package/README.md +30 -1
- package/SOURCES.md +63 -0
- package/dist/bundles/index.d.ts +5 -0
- package/dist/bundles/index.d.ts.map +1 -0
- package/dist/bundles/student.d.ts +3 -0
- package/dist/bundles/student.d.ts.map +1 -0
- package/dist/bundles/types.d.ts +24 -0
- package/dist/bundles/types.d.ts.map +1 -0
- package/dist/credential-library.cjs.development.js +12244 -713
- package/dist/credential-library.cjs.development.js.map +4 -4
- package/dist/credential-library.cjs.production.min.js +11 -1
- package/dist/credential-library.cjs.production.min.js.map +4 -4
- package/dist/credential-library.esm.js +12242 -709
- package/dist/credential-library.esm.js.map +4 -4
- package/dist/fixtures/clr/achievement-id-associations.d.ts +3 -0
- package/dist/fixtures/clr/achievement-id-associations.d.ts.map +1 -0
- package/dist/fixtures/clr/employment-record.d.ts +8 -0
- package/dist/fixtures/clr/employment-record.d.ts.map +1 -0
- package/dist/fixtures/clr/licensing-regulatory-record.d.ts +8 -0
- package/dist/fixtures/clr/licensing-regulatory-record.d.ts.map +1 -0
- package/dist/fixtures/clr/military-training-record.d.ts +8 -0
- package/dist/fixtures/clr/military-training-record.d.ts.map +1 -0
- package/dist/fixtures/clr/mixed-career-record.d.ts +8 -0
- package/dist/fixtures/clr/mixed-career-record.d.ts.map +1 -0
- package/dist/fixtures/clr/mixed-role-portfolio.d.ts +3 -0
- package/dist/fixtures/clr/mixed-role-portfolio.d.ts.map +1 -0
- package/dist/fixtures/clr/professional-organization-record.d.ts +8 -0
- package/dist/fixtures/clr/professional-organization-record.d.ts.map +1 -0
- package/dist/fixtures/clr/student-official-academic-transcript.d.ts +3 -0
- package/dist/fixtures/clr/student-official-academic-transcript.d.ts.map +1 -0
- package/dist/fixtures/clr/training-provider-record.d.ts +8 -0
- package/dist/fixtures/clr/training-provider-record.d.ts.map +1 -0
- package/dist/fixtures/custom/course-enrollment.d.ts +3 -0
- package/dist/fixtures/custom/course-enrollment.d.ts.map +1 -0
- package/dist/fixtures/custom/exam-accommodation.d.ts +3 -0
- package/dist/fixtures/custom/exam-accommodation.d.ts.map +1 -0
- package/dist/fixtures/index.d.ts +25 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/obv3/food-allergen-facilitator.d.ts +3 -0
- package/dist/fixtures/obv3/food-allergen-facilitator.d.ts.map +1 -0
- package/dist/fixtures/obv3/food-allergen-practice-fr.d.ts +3 -0
- package/dist/fixtures/obv3/food-allergen-practice-fr.d.ts.map +1 -0
- package/dist/fixtures/obv3/food-allergen-practice.d.ts +3 -0
- package/dist/fixtures/obv3/food-allergen-practice.d.ts.map +1 -0
- package/dist/fixtures/obv3/phishing-transfer.d.ts +3 -0
- package/dist/fixtures/obv3/phishing-transfer.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-apprenticeship.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-apprenticeship.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-badge-override.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-badge-override.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-certification.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-certification.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-expired.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-expired.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-journeyman.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-journeyman.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-license.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-license.d.ts.map +1 -0
- package/dist/fixtures/obv3/qualification-master.d.ts +3 -0
- package/dist/fixtures/obv3/qualification-master.d.ts.map +1 -0
- package/dist/fixtures/obv3/student-additional-credentials.d.ts +3 -0
- package/dist/fixtures/obv3/student-additional-credentials.d.ts.map +1 -0
- package/dist/fixtures/obv3/student-sample-badges.d.ts +586 -0
- package/dist/fixtures/obv3/student-sample-badges.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/bundles.test.ts +234 -0
- package/src/__tests__/clr-non-academic.test.ts +220 -0
- package/src/__tests__/curated-examples.test.ts +248 -0
- package/src/__tests__/issuance.test.ts +35 -0
- package/src/__tests__/registry.test.ts +31 -0
- package/src/bundles/index.ts +19 -0
- package/src/bundles/student.ts +251 -0
- package/src/bundles/types.ts +25 -0
- package/src/fixtures/clr/README.md +142 -0
- package/src/fixtures/clr/achievement-id-associations.ts +342 -0
- package/src/fixtures/clr/employment-record.ts +487 -0
- package/src/fixtures/clr/licensing-regulatory-record.ts +388 -0
- package/src/fixtures/clr/military-training-record.ts +468 -0
- package/src/fixtures/clr/mixed-career-record.ts +814 -0
- package/src/fixtures/clr/mixed-role-portfolio.ts +490 -0
- package/src/fixtures/clr/professional-organization-record.ts +403 -0
- package/src/fixtures/clr/student-official-academic-transcript.ts +3202 -0
- package/src/fixtures/clr/training-provider-record.ts +510 -0
- package/src/fixtures/custom/course-enrollment.ts +56 -0
- package/src/fixtures/custom/exam-accommodation.ts +56 -0
- package/src/fixtures/index.ts +87 -0
- package/src/fixtures/obv3/food-allergen-facilitator.ts +108 -0
- package/src/fixtures/obv3/food-allergen-practice-fr.ts +107 -0
- package/src/fixtures/obv3/food-allergen-practice.ts +105 -0
- package/src/fixtures/obv3/phishing-transfer.ts +163 -0
- package/src/fixtures/obv3/qualification-apprenticeship.ts +58 -0
- package/src/fixtures/obv3/qualification-badge-override.ts +64 -0
- package/src/fixtures/obv3/qualification-certification.ts +59 -0
- package/src/fixtures/obv3/qualification-expired.ts +59 -0
- package/src/fixtures/obv3/qualification-journeyman.ts +58 -0
- package/src/fixtures/obv3/qualification-license.ts +58 -0
- package/src/fixtures/obv3/qualification-master.ts +58 -0
- package/src/fixtures/obv3/student-additional-credentials.ts +3912 -0
- package/src/fixtures/obv3/student-sample-badges.ts +424 -0
- package/src/index.ts +2 -0
- package/src/prepare.ts +55 -11
- package/dist/__tests__/docs-json.test.d.ts +0 -2
- package/dist/__tests__/docs-json.test.d.ts.map +0 -1
- package/dist/__tests__/issuance.test.d.ts +0 -2
- package/dist/__tests__/issuance.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/sd-jwt-vc.test.d.ts +0 -2
- package/dist/__tests__/sd-jwt-vc.test.d.ts.map +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { UnsignedAchievementCredentialValidator } from '@learncard/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ALL_FIXTURES,
|
|
6
|
+
obv3FoodAllergenPractice,
|
|
7
|
+
obv3FoodAllergenFacilitator,
|
|
8
|
+
obv3FoodAllergenPracticeFr,
|
|
9
|
+
obv3PhishingTransfer,
|
|
10
|
+
customCourseEnrollment,
|
|
11
|
+
customExamAccommodation,
|
|
12
|
+
clrMixedRolePortfolio,
|
|
13
|
+
} from '../fixtures';
|
|
14
|
+
import { prepareFixture } from '../prepare';
|
|
15
|
+
|
|
16
|
+
type JsonRecord = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
const asRecord = (value: unknown): JsonRecord => {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
20
|
+
throw new Error('Expected a credential object');
|
|
21
|
+
}
|
|
22
|
+
return value as JsonRecord;
|
|
23
|
+
};
|
|
24
|
+
const asRecords = (value: unknown): JsonRecord[] => {
|
|
25
|
+
if (!Array.isArray(value)) throw new Error('Expected a credential array');
|
|
26
|
+
return value.map(asRecord);
|
|
27
|
+
};
|
|
28
|
+
const subjectOf = (credential: JsonRecord): JsonRecord => asRecord(credential.credentialSubject);
|
|
29
|
+
const achievementOf = (credential: JsonRecord): JsonRecord =>
|
|
30
|
+
asRecord(subjectOf(credential).achievement);
|
|
31
|
+
|
|
32
|
+
const fixtures = [
|
|
33
|
+
obv3FoodAllergenPractice,
|
|
34
|
+
obv3FoodAllergenFacilitator,
|
|
35
|
+
obv3FoodAllergenPracticeFr,
|
|
36
|
+
obv3PhishingTransfer,
|
|
37
|
+
customCourseEnrollment,
|
|
38
|
+
customExamAccommodation,
|
|
39
|
+
clrMixedRolePortfolio,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const expectResultReferences = (credential: JsonRecord): void => {
|
|
43
|
+
const descriptions = asRecords(achievementOf(credential).resultDescription);
|
|
44
|
+
const byId = new Map(descriptions.map(description => [description.id, description]));
|
|
45
|
+
expect(byId.size).toBe(descriptions.length);
|
|
46
|
+
for (const result of asRecords(subjectOf(credential).result)) {
|
|
47
|
+
const description = byId.get(result.resultDescription);
|
|
48
|
+
expect(description).toBeDefined();
|
|
49
|
+
if (!description) throw new Error('Missing result description');
|
|
50
|
+
if (result.achievedLevel || description.requiredLevel) {
|
|
51
|
+
const levels = asRecords(description.rubricCriterionLevel);
|
|
52
|
+
if (result.achievedLevel) {
|
|
53
|
+
expect(levels.some(level => level.id === result.achievedLevel)).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
if (description.requiredLevel) {
|
|
56
|
+
expect(levels.some(level => level.id === description.requiredLevel)).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('Curated example coverage', () => {
|
|
63
|
+
it.each(fixtures)('$id is registered once, validated, and unsigned', fixture => {
|
|
64
|
+
expect(ALL_FIXTURES.filter(entry => entry.id === fixture.id)).toEqual([fixture]);
|
|
65
|
+
expect(fixture.source).toBe('synthetic');
|
|
66
|
+
expect(fixture.signed).toBe(false);
|
|
67
|
+
expect(fixture.validity).toBe('valid');
|
|
68
|
+
expect(fixture.validator).toBeDefined();
|
|
69
|
+
expect(fixture.validator?.safeParse(fixture.credential).success).toBe(true);
|
|
70
|
+
expect(fixture.credential.proof).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('uses named issuer profiles while preserving shared and distinct issuer groups', () => {
|
|
74
|
+
const issuers = fixtures.map(fixture => asRecord(fixture.credential.issuer));
|
|
75
|
+
for (const issuer of issuers) {
|
|
76
|
+
expect(issuer.id).toMatch(/^did:/);
|
|
77
|
+
expect(issuer.type).toEqual(['Profile']);
|
|
78
|
+
expect(issuer.name).toBeTruthy();
|
|
79
|
+
expect(issuer.name).not.toMatch(/^https?:/);
|
|
80
|
+
}
|
|
81
|
+
expect(new Set(issuers.map(issuer => issuer.id)).size).toBe(5);
|
|
82
|
+
expect(obv3FoodAllergenPracticeFr.credential.issuer).toEqual(
|
|
83
|
+
obv3FoodAllergenPractice.credential.issuer
|
|
84
|
+
);
|
|
85
|
+
expect(obv3FoodAllergenFacilitator.credential.issuer).toEqual(
|
|
86
|
+
obv3FoodAllergenPractice.credential.issuer
|
|
87
|
+
);
|
|
88
|
+
for (const child of asRecords(
|
|
89
|
+
subjectOf(clrMixedRolePortfolio.credential).verifiableCredential
|
|
90
|
+
)) {
|
|
91
|
+
expect(child.issuer).toEqual(clrMixedRolePortfolio.credential.issuer);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it.each([obv3FoodAllergenPractice, obv3FoodAllergenPracticeFr])(
|
|
96
|
+
'$id expresses RubricScore within the declared range',
|
|
97
|
+
fixture => {
|
|
98
|
+
const subject = subjectOf(fixture.credential);
|
|
99
|
+
const [description] = asRecords(achievementOf(fixture.credential).resultDescription);
|
|
100
|
+
const [result] = asRecords(subject.result);
|
|
101
|
+
expect(description.resultType).toBe('RubricScore');
|
|
102
|
+
expect(description).toMatchObject({ valueMin: '0', valueMax: '4', requiredValue: '3' });
|
|
103
|
+
expect(result).toMatchObject({ value: '4', status: 'Completed' });
|
|
104
|
+
expect(Number(result.value)).toBeGreaterThanOrEqual(Number(description.requiredValue));
|
|
105
|
+
expect(Number(result.value)).toBeLessThanOrEqual(Number(description.valueMax));
|
|
106
|
+
expect(subject.role).toBe('learner');
|
|
107
|
+
expectResultReferences(fixture.credential);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
it('keeps French text, scoring, alignment, and counterpart identities consistent', () => {
|
|
112
|
+
const english = achievementOf(obv3FoodAllergenPractice.credential);
|
|
113
|
+
const french = achievementOf(obv3FoodAllergenPracticeFr.credential);
|
|
114
|
+
expect(french.inLanguage).toBe('fr');
|
|
115
|
+
expect(french.name).toContain('Prévention');
|
|
116
|
+
expect(asRecord(french.criteria).narrative).toContain('Repère');
|
|
117
|
+
expect(french.alignment).toEqual(english.alignment);
|
|
118
|
+
const [englishScore] = asRecords(english.resultDescription);
|
|
119
|
+
const [frenchScore] = asRecords(french.resultDescription);
|
|
120
|
+
expect(frenchScore).toMatchObject({
|
|
121
|
+
resultType: englishScore.resultType,
|
|
122
|
+
valueMin: englishScore.valueMin,
|
|
123
|
+
valueMax: englishScore.valueMax,
|
|
124
|
+
requiredValue: englishScore.requiredValue,
|
|
125
|
+
});
|
|
126
|
+
expect(french.id).not.toBe(english.id);
|
|
127
|
+
expect(frenchScore.id).not.toBe(englishScore.id);
|
|
128
|
+
expect(obv3FoodAllergenPracticeFr.credential.id).not.toBe(
|
|
129
|
+
obv3FoodAllergenPractice.credential.id
|
|
130
|
+
);
|
|
131
|
+
expect(subjectOf(obv3FoodAllergenPracticeFr.credential).id).toBe(
|
|
132
|
+
subjectOf(obv3FoodAllergenPractice.credential).id
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('assesses the matched facilitator as an instructor, separately from learner practice', () => {
|
|
137
|
+
const instructor = subjectOf(obv3FoodAllergenFacilitator.credential);
|
|
138
|
+
const learner = subjectOf(obv3FoodAllergenPractice.credential);
|
|
139
|
+
const achievement = asRecord(instructor.achievement);
|
|
140
|
+
expect(instructor.id).toBe(learner.id);
|
|
141
|
+
expect(instructor.role).toBe('instructor');
|
|
142
|
+
expect(achievement.description).toContain('Assesses teaching performance');
|
|
143
|
+
expect(asRecords(achievement.alignment).map(alignment => alignment.targetName)).toEqual([
|
|
144
|
+
'Instructing',
|
|
145
|
+
]);
|
|
146
|
+
expect(asRecords(achievement.resultDescription)[0].resultType).toBe('PerformanceLevel');
|
|
147
|
+
expectResultReferences(obv3FoodAllergenFacilitator.credential);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('preserves achieved-level-only and raw-score branches in the same project', () => {
|
|
151
|
+
const [level, score] = asRecords(subjectOf(obv3PhishingTransfer.credential).result);
|
|
152
|
+
expect(level.achievedLevel).toBeDefined();
|
|
153
|
+
expect(level).not.toHaveProperty('value');
|
|
154
|
+
expect(score.value).toBe('9');
|
|
155
|
+
expect(score).not.toHaveProperty('achievedLevel');
|
|
156
|
+
expect(
|
|
157
|
+
asRecords(achievementOf(obv3PhishingTransfer.credential).resultDescription).map(
|
|
158
|
+
description => description.resultType
|
|
159
|
+
)
|
|
160
|
+
).toEqual(['RubricCriterionLevel', 'RawScore']);
|
|
161
|
+
expectResultReferences(obv3PhishingTransfer.credential);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it.each([customCourseEnrollment, customExamAccommodation])(
|
|
165
|
+
'$id remains a custom administrative claim with no achievement',
|
|
166
|
+
fixture => {
|
|
167
|
+
const subject = subjectOf(fixture.credential);
|
|
168
|
+
expect(fixture.spec).toBe('custom');
|
|
169
|
+
expect(fixture.profile).toBe('generic');
|
|
170
|
+
expect(subject.designation).toBeDefined();
|
|
171
|
+
expect(subject.claimRole).toBeDefined();
|
|
172
|
+
expect(subject.claimScope).toBeDefined();
|
|
173
|
+
expect(subject).not.toHaveProperty('achievement');
|
|
174
|
+
expect(subject).not.toHaveProperty('result');
|
|
175
|
+
expect(subject).not.toHaveProperty('diagnosis');
|
|
176
|
+
const context = fixture.credential['@context'];
|
|
177
|
+
expect(Array.isArray(context)).toBe(true);
|
|
178
|
+
if (!Array.isArray(context)) throw new Error('Expected inline context');
|
|
179
|
+
const terms = asRecord(context[1]);
|
|
180
|
+
expect(terms[fixture.credential.type[1]]).toBeDefined();
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it('keeps mixed CLR roles, mirrored definitions, and canonical achievement references', () => {
|
|
185
|
+
expect(clrMixedRolePortfolio.credential['@context']).toEqual([
|
|
186
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
187
|
+
'https://purl.imsglobal.org/spec/clr/v2p0/context.json',
|
|
188
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
|
189
|
+
]);
|
|
190
|
+
const subject = subjectOf(clrMixedRolePortfolio.credential);
|
|
191
|
+
const children = asRecords(subject.verifiableCredential);
|
|
192
|
+
expect(children).toHaveLength(3);
|
|
193
|
+
expect(new Set(children.map(child => child.id)).size).toBe(3);
|
|
194
|
+
expect(children.map(child => subjectOf(child).role)).toEqual([
|
|
195
|
+
'learner',
|
|
196
|
+
'learner',
|
|
197
|
+
'instructor',
|
|
198
|
+
]);
|
|
199
|
+
expect(subject.achievement).toEqual(children.map(achievementOf));
|
|
200
|
+
const achievementIds = new Set(children.map(child => achievementOf(child).id));
|
|
201
|
+
const credentialIds = new Set(children.map(child => child.id));
|
|
202
|
+
for (const association of asRecords(subject.association)) {
|
|
203
|
+
expect(association.type).toBe('Association');
|
|
204
|
+
expect(achievementIds.has(association.sourceId)).toBe(true);
|
|
205
|
+
expect(achievementIds.has(association.targetId)).toBe(true);
|
|
206
|
+
expect(credentialIds.has(association.sourceId)).toBe(false);
|
|
207
|
+
expect(credentialIds.has(association.targetId)).toBe(false);
|
|
208
|
+
}
|
|
209
|
+
for (const child of children) {
|
|
210
|
+
expect(subjectOf(child).id).toBe(subject.id);
|
|
211
|
+
expect(child.proof).toBeUndefined();
|
|
212
|
+
expect(UnsignedAchievementCredentialValidator.safeParse(child).success).toBe(true);
|
|
213
|
+
expectResultReferences(child);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('prepares CLR children together without changing source data or child issuer claims', () => {
|
|
218
|
+
const before = JSON.stringify(clrMixedRolePortfolio.credential);
|
|
219
|
+
const originalSubject = subjectOf(clrMixedRolePortfolio.credential);
|
|
220
|
+
const originalChildren = asRecords(originalSubject.verifiableCredential);
|
|
221
|
+
const prepared = prepareFixture(clrMixedRolePortfolio, {
|
|
222
|
+
issuerDid: 'did:example:portfolio-test-issuer',
|
|
223
|
+
subjectDid: 'did:example:portfolio-test-subject',
|
|
224
|
+
validFrom: '2026-10-04T12:00:00Z',
|
|
225
|
+
freshIds: true,
|
|
226
|
+
});
|
|
227
|
+
expect(asRecord(prepared.issuer).id).toBe('did:example:portfolio-test-issuer');
|
|
228
|
+
expect(asRecord(prepared.issuer).name).toBe(
|
|
229
|
+
asRecord(clrMixedRolePortfolio.credential.issuer).name
|
|
230
|
+
);
|
|
231
|
+
expect(prepared.validFrom).toBe('2026-10-04T12:00:00Z');
|
|
232
|
+
const subject = subjectOf(prepared);
|
|
233
|
+
expect(subject.id).toBe('did:example:portfolio-test-subject');
|
|
234
|
+
expect(prepared.id).not.toBe(clrMixedRolePortfolio.credential.id);
|
|
235
|
+
const children = asRecords(subject.verifiableCredential);
|
|
236
|
+
for (const [index, child] of children.entries()) {
|
|
237
|
+
expect(subjectOf(child).id).toBe(subject.id);
|
|
238
|
+
expect(child.id).not.toBe(originalChildren[index].id);
|
|
239
|
+
expect(child.issuer).toEqual(originalChildren[index].issuer);
|
|
240
|
+
expect(child.validFrom).toBe(originalChildren[index].validFrom);
|
|
241
|
+
expect(child.proof).toBeUndefined();
|
|
242
|
+
expectResultReferences(child);
|
|
243
|
+
}
|
|
244
|
+
expect(subject.achievement).toEqual(children.map(achievementOf));
|
|
245
|
+
expect(subject.association).toEqual(originalSubject.association);
|
|
246
|
+
expect(JSON.stringify(clrMixedRolePortfolio.credential)).toBe(before);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -60,6 +60,41 @@ describe('Credential issuance', () => {
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
describe('curated examples issue without remote contexts', () => {
|
|
64
|
+
// A deterministic, in-memory test identity; no account or persistent key writes.
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
let offlineWallet: any;
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
offlineWallet = await initLearnCard({
|
|
70
|
+
seed: 'd'.repeat(64),
|
|
71
|
+
didkit,
|
|
72
|
+
allowRemoteContexts: false,
|
|
73
|
+
});
|
|
74
|
+
}, 30_000);
|
|
75
|
+
|
|
76
|
+
it.each([
|
|
77
|
+
'obv3/food-allergen-practice',
|
|
78
|
+
'obv3/food-allergen-facilitator',
|
|
79
|
+
'obv3/food-allergen-practice-fr',
|
|
80
|
+
'obv3/phishing-transfer',
|
|
81
|
+
'custom/course-enrollment',
|
|
82
|
+
'custom/exam-accommodation',
|
|
83
|
+
'clr/mixed-role-portfolio',
|
|
84
|
+
])(
|
|
85
|
+
'%s',
|
|
86
|
+
async id => {
|
|
87
|
+
const prepared = prepareFixture(getFixture(id), {
|
|
88
|
+
issuerDid: offlineWallet.id.did(),
|
|
89
|
+
subjectDid: 'did:example:curated-test-subject',
|
|
90
|
+
});
|
|
91
|
+
const signed = await offlineWallet.invoke.issueCredential(prepared);
|
|
92
|
+
expect(signed.proof).toBeDefined();
|
|
93
|
+
},
|
|
94
|
+
15_000
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
63
98
|
it('issues the full CLR fixture without remote contexts', async () => {
|
|
64
99
|
const offlineWallet = await initLearnCard({
|
|
65
100
|
seed: 'b'.repeat(64),
|
|
@@ -582,6 +582,37 @@ describe('prepareFixture', () => {
|
|
|
582
582
|
expect(subject.id).toBe(subjectDid);
|
|
583
583
|
});
|
|
584
584
|
|
|
585
|
+
it('preserves signed credentials nested in a CLR', () => {
|
|
586
|
+
const fixture = getFixture('vc-v2/basic');
|
|
587
|
+
const signedEmbeddedCredential = {
|
|
588
|
+
'@context': ['https://www.w3.org/ns/credentials/v2'],
|
|
589
|
+
id: fixture.credential.id,
|
|
590
|
+
type: ['VerifiableCredential'],
|
|
591
|
+
issuer: 'did:example:nested-issuer',
|
|
592
|
+
validFrom: '2025-01-01T00:00:00Z',
|
|
593
|
+
credentialSubject: { id: 'did:example:signed-subject' },
|
|
594
|
+
proof: { type: 'DataIntegrityProof', proofValue: 'signed-value' },
|
|
595
|
+
};
|
|
596
|
+
const prepared = prepareFixture(
|
|
597
|
+
{
|
|
598
|
+
...fixture,
|
|
599
|
+
credential: {
|
|
600
|
+
...fixture.credential,
|
|
601
|
+
credentialSubject: {
|
|
602
|
+
id: 'did:example:old-root',
|
|
603
|
+
verifiableCredential: [signedEmbeddedCredential],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
{ issuerDid, subjectDid }
|
|
608
|
+
);
|
|
609
|
+
const subject = prepared.credentialSubject as UnknownRecord;
|
|
610
|
+
const embedded = subject.verifiableCredential as UnknownRecord[];
|
|
611
|
+
|
|
612
|
+
expect(subject.id).toBe(subjectDid);
|
|
613
|
+
expect(embedded[0]).toEqual(signedEmbeddedCredential);
|
|
614
|
+
});
|
|
615
|
+
|
|
585
616
|
it('generates fresh UUIDs for id fields by default', () => {
|
|
586
617
|
const fixture = getFixture('obv3/minimal-badge');
|
|
587
618
|
const prepared = prepareFixture(fixture, { issuerDid });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CredentialBundle } from './types';
|
|
2
|
+
import { studentBundle } from './student';
|
|
3
|
+
|
|
4
|
+
const BUNDLES: Readonly<Record<string, CredentialBundle>> = {
|
|
5
|
+
[studentBundle.id]: studentBundle,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getBundle = (id: string): CredentialBundle => {
|
|
9
|
+
const bundle = BUNDLES[id];
|
|
10
|
+
|
|
11
|
+
if (!bundle) {
|
|
12
|
+
throw new Error(`Unknown credential bundle: ${id}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return bundle;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { studentBundle } from './student';
|
|
19
|
+
export type { CredentialBundle, CredentialBundleEntry, CredentialBundleIssuer } from './types';
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { CredentialBundle, CredentialBundleIssuer } from './types';
|
|
2
|
+
|
|
3
|
+
const ISSUERS = {
|
|
4
|
+
hillValleyHigh: {
|
|
5
|
+
profileId: 'sample-hill-valley-high',
|
|
6
|
+
displayName: 'Hill Valley High School',
|
|
7
|
+
image: 'https://cdn.filestackcontent.com/rko7HmtCQ5aI5gfS3r1o',
|
|
8
|
+
},
|
|
9
|
+
worldScouting: {
|
|
10
|
+
profileId: 'sample-world-scouting',
|
|
11
|
+
displayName: 'World Scouting',
|
|
12
|
+
image: 'https://cdn.filestackcontent.com/tY9KCX5T4mQK9GERTGhf',
|
|
13
|
+
},
|
|
14
|
+
royCharleston: {
|
|
15
|
+
profileId: 'sample-roy-charleston',
|
|
16
|
+
displayName: 'Roy Charleston',
|
|
17
|
+
},
|
|
18
|
+
toefl: {
|
|
19
|
+
profileId: 'sample-toefl',
|
|
20
|
+
displayName: 'TOEFL',
|
|
21
|
+
image: 'https://cdn.filestackcontent.com/s6P4u2TVSaeHDVS64UKw',
|
|
22
|
+
},
|
|
23
|
+
collegeBoard: {
|
|
24
|
+
profileId: 'sample-college-board',
|
|
25
|
+
displayName: 'College Board',
|
|
26
|
+
image: 'https://cdn.filestackcontent.com/TMTJ1xuKSKWuPOxMc2fQ',
|
|
27
|
+
},
|
|
28
|
+
arcadiaMediaLabs: {
|
|
29
|
+
profileId: 'sample-arcadia-media-labs',
|
|
30
|
+
displayName: 'Arcadia Media Labs',
|
|
31
|
+
image: 'https://cdn.filestackcontent.com/bfPLw9dQfWhpVYorZVfA',
|
|
32
|
+
},
|
|
33
|
+
codeRiseAcademy: {
|
|
34
|
+
profileId: 'sample-code-rise-academy',
|
|
35
|
+
displayName: 'CodeRise Academy',
|
|
36
|
+
image: 'https://cdn.filestackcontent.com/nHMhLsvFQmKU2TGKYzHK',
|
|
37
|
+
},
|
|
38
|
+
redwoodValleyUniversity: {
|
|
39
|
+
profileId: 'sample-redwood-valley-university',
|
|
40
|
+
displayName: 'Redwood Valley University',
|
|
41
|
+
image: 'https://cdn.filestackcontent.com/kok4oellRhKa2mWRllZS',
|
|
42
|
+
},
|
|
43
|
+
learningEconomy: {
|
|
44
|
+
profileId: 'sample-learning-economy',
|
|
45
|
+
displayName: 'Learning Economy',
|
|
46
|
+
image: 'https://cdn.filestackcontent.com/iLpqAJWYTDCxARqo6LN4',
|
|
47
|
+
},
|
|
48
|
+
oliviaTrujillo: {
|
|
49
|
+
profileId: 'sample-olivia-trujillo',
|
|
50
|
+
displayName: 'Olivia Trujillo',
|
|
51
|
+
image: 'https://cdn.filestackcontent.com/OKefWUSvTnuHELTSGH0z',
|
|
52
|
+
},
|
|
53
|
+
summitAccessibilityOffice: {
|
|
54
|
+
profileId: 'sample-summit-accessibility-office',
|
|
55
|
+
displayName: 'Summit Accessibility Office',
|
|
56
|
+
image: 'https://cdn.filestackcontent.com/JEAA9HfUSNa8SPp8nQiU',
|
|
57
|
+
},
|
|
58
|
+
kikuhanaInternationalUniversity: {
|
|
59
|
+
profileId: 'sample-kikuhana-international-university',
|
|
60
|
+
displayName: 'Kikuhana International University',
|
|
61
|
+
image: 'https://cdn.filestackcontent.com/HLQCIyRMQgSnxZtKmevv',
|
|
62
|
+
},
|
|
63
|
+
campTallPines: {
|
|
64
|
+
profileId: 'sample-camp-tall-pines',
|
|
65
|
+
displayName: 'Camp Tall Pines',
|
|
66
|
+
image: 'https://cdn.filestackcontent.com/frBHDn7SV2f8HKcxkGrj',
|
|
67
|
+
},
|
|
68
|
+
summitMarketing: {
|
|
69
|
+
profileId: 'sample-summit-marketing',
|
|
70
|
+
displayName: 'Summit Marketing',
|
|
71
|
+
image: 'https://cdn.filestackcontent.com/Q5BWlBiRJevkBlBlYaN2',
|
|
72
|
+
},
|
|
73
|
+
motlowStateCollege: {
|
|
74
|
+
profileId: 'sample-motlow-state-college',
|
|
75
|
+
displayName: 'Motlow State College',
|
|
76
|
+
image: 'https://cdn.filestackcontent.com/rjXzVVEjRVu0RSb8gYPZ',
|
|
77
|
+
},
|
|
78
|
+
} as const satisfies Record<string, CredentialBundleIssuer>;
|
|
79
|
+
|
|
80
|
+
export const studentBundle: CredentialBundle = {
|
|
81
|
+
id: 'student',
|
|
82
|
+
displayName: 'Student',
|
|
83
|
+
blurb: 'Explore academic, creative, leadership, service, and work credentials in a LearnCard.',
|
|
84
|
+
entries: [
|
|
85
|
+
{
|
|
86
|
+
fixtureId: 'obv3/student-afterschool-program-mentor',
|
|
87
|
+
validFromOffsetDays: -79,
|
|
88
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
fixtureId: 'obv3/student-environment-badge',
|
|
92
|
+
validFromOffsetDays: -79,
|
|
93
|
+
issuer: ISSUERS.worldScouting,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
fixtureId: 'obv3/student-rock-climbing-mentor',
|
|
97
|
+
validFromOffsetDays: -79,
|
|
98
|
+
issuer: ISSUERS.royCharleston,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
fixtureId: 'obv3/student-park-cleanup-helper',
|
|
102
|
+
validFromOffsetDays: -79,
|
|
103
|
+
issuer: ISSUERS.royCharleston,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
fixtureId: 'obv3/student-first-place-science-fair',
|
|
107
|
+
validFromOffsetDays: -80,
|
|
108
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
fixtureId: 'obv3/student-toefl-certification',
|
|
112
|
+
validFromOffsetDays: -80,
|
|
113
|
+
issuer: ISSUERS.toefl,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
fixtureId: 'obv3/student-senior-capstone-project',
|
|
117
|
+
validFromOffsetDays: -80,
|
|
118
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
fixtureId: 'obv3/student-sat-score',
|
|
122
|
+
validFromOffsetDays: -80,
|
|
123
|
+
issuer: ISSUERS.collegeBoard,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
fixtureId: 'obv3/student-hill-valley-high-school-diploma',
|
|
127
|
+
validFromOffsetDays: -80,
|
|
128
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
fixtureId: 'obv3/student-biology-101',
|
|
132
|
+
validFromOffsetDays: -80,
|
|
133
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
fixtureId: 'obv3/student-ai-fundamentals',
|
|
137
|
+
validFromOffsetDays: -80,
|
|
138
|
+
issuer: ISSUERS.arcadiaMediaLabs,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
fixtureId: 'obv3/student-technical-bootcamp',
|
|
142
|
+
validFromOffsetDays: -80,
|
|
143
|
+
issuer: ISSUERS.codeRiseAcademy,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
fixtureId: 'clr/student-official-academic-transcript',
|
|
147
|
+
validFromOffsetDays: -494,
|
|
148
|
+
issuer: ISSUERS.redwoodValleyUniversity,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
fixtureId: 'obv3/student-ba-interactive-media-design',
|
|
152
|
+
validFromOffsetDays: -80,
|
|
153
|
+
issuer: ISSUERS.redwoodValleyUniversity,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
fixtureId: 'obv3/student-ap-math-course',
|
|
157
|
+
validFromOffsetDays: -80,
|
|
158
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
fixtureId: 'obv3/student-animated-short',
|
|
162
|
+
validFromOffsetDays: -80,
|
|
163
|
+
issuer: ISSUERS.learningEconomy,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
fixtureId: 'obv3/student-capstone-project',
|
|
167
|
+
validFromOffsetDays: -80,
|
|
168
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
fixtureId: 'obv3/student-skyway-magazine',
|
|
172
|
+
validFromOffsetDays: -80,
|
|
173
|
+
issuer: ISSUERS.learningEconomy,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
fixtureId: 'obv3/student-photo-contest-award',
|
|
177
|
+
validFromOffsetDays: -80,
|
|
178
|
+
issuer: ISSUERS.oliviaTrujillo,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
fixtureId: 'obv3/student-design-portfolio',
|
|
182
|
+
validFromOffsetDays: -80,
|
|
183
|
+
issuer: ISSUERS.oliviaTrujillo,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
fixtureId: 'obv3/student-workplace-ergonomic-assessment',
|
|
187
|
+
validFromOffsetDays: -80,
|
|
188
|
+
issuer: ISSUERS.summitAccessibilityOffice,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
fixtureId: 'obv3/student-iep-documentation',
|
|
192
|
+
validFromOffsetDays: -80,
|
|
193
|
+
issuer: ISSUERS.summitAccessibilityOffice,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
fixtureId: 'obv3/student-accessibility-services-registration',
|
|
197
|
+
validFromOffsetDays: -80,
|
|
198
|
+
issuer: ISSUERS.summitAccessibilityOffice,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
fixtureId: 'obv3/student-extended-time-on-exams-approval',
|
|
202
|
+
validFromOffsetDays: -80,
|
|
203
|
+
issuer: ISSUERS.summitAccessibilityOffice,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
fixtureId: 'obv3/student-mental-health-accommodation-letter',
|
|
207
|
+
validFromOffsetDays: -80,
|
|
208
|
+
issuer: ISSUERS.summitAccessibilityOffice,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
fixtureId: 'obv3/student-study-abroad-program',
|
|
212
|
+
validFromOffsetDays: -80,
|
|
213
|
+
issuer: ISSUERS.kikuhanaInternationalUniversity,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
fixtureId: 'obv3/student-camp-counselor',
|
|
217
|
+
validFromOffsetDays: -80,
|
|
218
|
+
issuer: ISSUERS.campTallPines,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
fixtureId: 'obv3/student-president-of-class',
|
|
222
|
+
validFromOffsetDays: -80,
|
|
223
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
fixtureId: 'obv3/student-resume',
|
|
227
|
+
validFromOffsetDays: -80,
|
|
228
|
+
issuer: ISSUERS.oliviaTrujillo,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
fixtureId: 'obv3/student-internship',
|
|
232
|
+
validFromOffsetDays: -80,
|
|
233
|
+
issuer: ISSUERS.summitMarketing,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
fixtureId: 'obv3/student-world-scouting-troop-id',
|
|
237
|
+
validFromOffsetDays: -431,
|
|
238
|
+
issuer: ISSUERS.worldScouting,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
fixtureId: 'obv3/student-hill-valley-high-school-student-id',
|
|
242
|
+
validFromOffsetDays: -431,
|
|
243
|
+
issuer: ISSUERS.hillValleyHigh,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
fixtureId: 'obv3/student-motlow-college-id',
|
|
247
|
+
validFromOffsetDays: -431,
|
|
248
|
+
issuer: ISSUERS.motlowStateCollege,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface CredentialBundleIssuer {
|
|
2
|
+
/** Namespaced profile ID created and owned by the sample seeder. */
|
|
3
|
+
profileId: `sample-${string}`;
|
|
4
|
+
/** Issuer name shown on the rendered credential. */
|
|
5
|
+
displayName: string;
|
|
6
|
+
/** Optional issuer avatar copied to the safe sample profile. */
|
|
7
|
+
image?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CredentialBundleEntry {
|
|
11
|
+
fixtureId: string;
|
|
12
|
+
/** Shift the fixture's issue date relative to the seed run. */
|
|
13
|
+
validFromOffsetDays: number;
|
|
14
|
+
/** Safe sample issuer used when publishing this credential. */
|
|
15
|
+
issuer: CredentialBundleIssuer;
|
|
16
|
+
/** Override the credential's display name after fixture preparation. */
|
|
17
|
+
name?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CredentialBundle {
|
|
21
|
+
id: string;
|
|
22
|
+
displayName: string;
|
|
23
|
+
blurb: string;
|
|
24
|
+
entries: readonly CredentialBundleEntry[];
|
|
25
|
+
}
|