@learncard/credential-library 2.0.1 → 2.0.2

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.
@@ -0,0 +1,38 @@
1
+ import { type UnsignedVC } from '@learncard/types';
2
+ import type { CredentialFixture } from '../../types';
3
+ /**
4
+ * Provisional CLR 2.0 transcript with a `LearnCardCredentialRefresh2026` service, built for
5
+ * the managed credential refresh flow (LC-2117 / LC-2135 / LC-2136).
6
+ *
7
+ * The credential starts as a provisional transcript; `buildFinalTranscriptVariant`
8
+ * produces the final variant WITHOUT changing the credential ID, issuer, or subject,
9
+ * mirroring a registrar publishing the finalized record to the same refresh service.
10
+ *
11
+ * JSON-LD note: neither the VCDM 1.1/2.0 nor the live OBv3/CLR contexts define the
12
+ * term `LearnCardCredentialRefresh2026` (or the LearnCard `authorization` extension), so
13
+ * the fixture carries an inline context fragment — DIDKit's data-loss detection
14
+ * otherwise refuses to sign.
15
+ */
16
+ /** Inline JSON-LD context fragment defining the refresh-service terms inline. */
17
+ export declare const REFRESH_SERVICE_INLINE_CONTEXT: {
18
+ readonly LearnCardCredentialRefresh2026: "https://learncard.com/refresh#LearnCardCredentialRefresh2026";
19
+ readonly authorization: {
20
+ readonly '@id': "https://purl.imsglobal.org/spec/ob/v3p0#authorization";
21
+ readonly '@context': {
22
+ readonly LearnCardDIDAuth: "https://docs.learncard.com/definitions#LearnCardDIDAuth";
23
+ };
24
+ };
25
+ };
26
+ export declare const clrProvisionalTranscript: CredentialFixture;
27
+ /**
28
+ * Builds the final transcript variant from a prepared provisional credential.
29
+ *
30
+ * Preserves the credential ID, issuer, subject, and refresh service (identity
31
+ * invariants the managed refresh pipeline enforces) while flipping user-visible
32
+ * content to its final state: name/description become final, the nested course
33
+ * credential shows the completed result, and `validFrom` moves forward.
34
+ */
35
+ export declare const buildFinalTranscriptVariant: (provisional: UnsignedVC, options?: {
36
+ validFrom?: string;
37
+ }) => UnsignedVC;
38
+ //# sourceMappingURL=provisional-transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provisional-transcript.d.ts","sourceRoot":"","sources":["../../../src/fixtures/clr/provisional-transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;;;;;;GAYG;AAEH,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;;;;;;CASjC,CAAC;AAaX,eAAO,MAAM,wBAAwB,EAAE,iBAoGtC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GACpC,aAAa,UAAU,EACvB,UAAS;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,KACrC,UAsCF,CAAC"}
@@ -38,8 +38,9 @@ import { vcV2LicenseCredential } from './vc-v2/license-credential';
38
38
  import { clrGreatPlainsFull } from './clr/great-plains-full';
39
39
  import { clrWestbridgeFull } from './clr/westbridge-full';
40
40
  import { clrCompetencyAligned } from './clr/competency-aligned';
41
+ import { clrProvisionalTranscript, buildFinalTranscriptVariant, REFRESH_SERVICE_INLINE_CONTEXT } from './clr/provisional-transcript';
41
42
  import { clrDemoIsdDiplomaAssessments } from './clr/demo-isd-diploma-assessments';
42
43
  import { sdJwtVcCourseCompletion } from './sd-jwt-vc/course-completion';
43
44
  export declare const ALL_FIXTURES: LibraryFixture[];
44
- export { vcV1Basic, vcV1WithStatus, vcV2Basic, vcV2WithEvidence, vcV2MultipleSubjects, vcV2RenderMethodExample, obv3MinimalBadge, obv3FullBadge, obv3WithAlignment, obv3WithEndorsement, obv3PlugfestJff2, clrMinimal, clrMultiAchievement, boostBasic, boostId, boostWithSkills, invalidMissingContext, invalidEmptyType, invalidMissingIssuer, clrNdStudentTranscript, obv3OneedtechFull, obv3ProfessionalCert, obv3MicroCredential, obv3CourseCompletion, obv3StandaloneFullCourse, obv3K12Diploma, obv3EndorsementCredential, clrUniversityTranscript, boostCommunityAward, boostDelegate, vcV1AlumniCredential, vcV2EmploymentCredential, vcV2EducationDegree, vcV2DigitalId, vcV2MembershipCredential, vcV2LicenseCredential, clrGreatPlainsFull, clrWestbridgeFull, clrCompetencyAligned, clrDemoIsdDiplomaAssessments, sdJwtVcCourseCompletion, };
45
+ export { vcV1Basic, vcV1WithStatus, vcV2Basic, vcV2WithEvidence, vcV2MultipleSubjects, vcV2RenderMethodExample, obv3MinimalBadge, obv3FullBadge, obv3WithAlignment, obv3WithEndorsement, obv3PlugfestJff2, clrMinimal, clrMultiAchievement, boostBasic, boostId, boostWithSkills, invalidMissingContext, invalidEmptyType, invalidMissingIssuer, clrNdStudentTranscript, obv3OneedtechFull, obv3ProfessionalCert, obv3MicroCredential, obv3CourseCompletion, obv3StandaloneFullCourse, obv3K12Diploma, obv3EndorsementCredential, clrUniversityTranscript, boostCommunityAward, boostDelegate, vcV1AlumniCredential, vcV2EmploymentCredential, vcV2EducationDegree, vcV2DigitalId, vcV2MembershipCredential, vcV2LicenseCredential, clrGreatPlainsFull, clrWestbridgeFull, clrCompetencyAligned, clrProvisionalTranscript, buildFinalTranscriptVariant, REFRESH_SERVICE_INLINE_CONTEXT, clrDemoIsdDiplomaAssessments, sdJwtVcCourseCompletion, };
45
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fixtures/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/credential-library",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Comprehensive library of Verifiable Credential fixtures for testing, development, and regression prevention across the VC ecosystem",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/credential-library.esm.js",
@@ -26,8 +26,8 @@
26
26
  "url": "https://github.com/learningeconomy/LearnCard/issues"
27
27
  },
28
28
  "devDependencies": {
29
- "@learncard/didkit-plugin": "1.9.13",
30
- "@learncard/init": "2.4.14",
29
+ "@learncard/didkit-plugin": "1.9.14",
30
+ "@learncard/init": "2.4.15",
31
31
  "@types/node": "^17.0.38",
32
32
  "esbuild": "^0.27.1",
33
33
  "jose": "^5.9.6",
@@ -36,8 +36,8 @@
36
36
  "vitest": "4.1.10"
37
37
  },
38
38
  "dependencies": {
39
- "@learncard/sd-jwt-vc-plugin": "0.2.11",
40
- "@learncard/types": "5.18.3",
39
+ "@learncard/sd-jwt-vc-plugin": "0.2.12",
40
+ "@learncard/types": "5.19.0",
41
41
  "@sd-jwt/sd-jwt-vc": "^0.19.0",
42
42
  "zod": "4.1.13"
43
43
  },
@@ -0,0 +1,148 @@
1
+ import { readFileSync } from 'fs';
2
+ import { readFile } from 'fs/promises';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ import { beforeAll, describe, expect, it } from 'vitest';
6
+ import { initLearnCard, type LearnCardFromSeed } from '@learncard/init';
7
+ import { AlignmentValidator, LCNBoostStatus, UnsignedVCValidator } from '@learncard/types';
8
+
9
+ const didkit = readFile(
10
+ require.resolve('@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm')
11
+ );
12
+
13
+ // `initLearnCard` is overloaded; `ReturnType` would pick the no-arg EmptyLearnCard overload.
14
+ let wallet: LearnCardFromSeed['returnValue'];
15
+ let subjectDid: string;
16
+
17
+ beforeAll(async () => {
18
+ wallet = await initLearnCard({ seed: 'a'.repeat(64), didkit, allowRemoteContexts: true });
19
+ const recipient = await initLearnCard({
20
+ seed: 'b'.repeat(64),
21
+ didkit,
22
+ allowRemoteContexts: true,
23
+ });
24
+ subjectDid = recipient.id.did();
25
+ }, 30_000);
26
+
27
+ const docsRoot = new URL('../../../../docs/', import.meta.url);
28
+ const summary = readFileSync(new URL('SUMMARY.md', docsRoot), 'utf8');
29
+ const section = summary.match(/^## 🧠 Understand\s*$([\s\S]*?)^## 📖 Reference\s*$/m)?.[1];
30
+ if (!section) throw new Error('docs/SUMMARY.md: missing Understand / Reference boundaries');
31
+ const pages = [...new Set([...section.matchAll(/\(([^)]+\.md)\)/g)].map(match => match[1]!))];
32
+ if (!pages.length) throw new Error('docs/SUMMARY.md: Understand has no Markdown links');
33
+
34
+ const isObject = (value: unknown): value is Record<string, unknown> =>
35
+ typeof value === 'object' && value !== null && !Array.isArray(value);
36
+
37
+ const entries = (value: unknown): unknown[] => (Array.isArray(value) ? value : [value]);
38
+
39
+ // Ignore ellipses within ordinary strings (including URL paths), but not an exact "..." value.
40
+ const isIllustrative = (source: string): boolean =>
41
+ source.includes('…') ||
42
+ [...source.matchAll(/"(?:\\.|[^"\\])*"|(\.\.\.)/g)].some(
43
+ match => match[0] === '"..."' || Boolean(match[1])
44
+ );
45
+
46
+ /** Check fragment fields wherever they occur, including inside achievement subjects. */
47
+ const validateFragments = (value: unknown): void => {
48
+ if (Array.isArray(value)) {
49
+ value.forEach(validateFragments);
50
+ return;
51
+ }
52
+ if (!isObject(value)) return;
53
+
54
+ if ('credentialStatus' in value) {
55
+ for (const entry of entries(value.credentialStatus)) {
56
+ expect(entry).toEqual(
57
+ expect.objectContaining({
58
+ type: 'BitstringStatusListEntry',
59
+ statusPurpose: expect.stringMatching(/^(revocation|suspension)$/),
60
+ statusListIndex: expect.any(String),
61
+ statusListCredential: expect.any(String),
62
+ })
63
+ );
64
+ }
65
+ }
66
+ if ('alignment' in value) {
67
+ expect(Array.isArray(value.alignment)).toBe(true);
68
+ for (const entry of entries(value.alignment)) {
69
+ AlignmentValidator.parse(entry);
70
+ expect(entry).toEqual(
71
+ expect.objectContaining({
72
+ type: expect.arrayContaining(['Alignment']),
73
+ targetName: expect.any(String),
74
+ targetUrl: expect.any(String),
75
+ })
76
+ );
77
+ }
78
+ }
79
+ if (
80
+ 'status' in value &&
81
+ !('credentialSubject' in value) &&
82
+ !isObject(value.status) &&
83
+ !Array.isArray(value.status)
84
+ ) {
85
+ LCNBoostStatus.parse(value.status);
86
+ }
87
+ Object.values(value).forEach(validateFragments);
88
+ };
89
+
90
+ describe('Understand documentation JSON', () => {
91
+ for (const page of pages) {
92
+ const markdown = readFileSync(fileURLToPath(new URL(page, docsRoot)), 'utf8');
93
+ // Match complete fences first so json-looking text inside another fence is not tested.
94
+ const fences = /^[ \t]{0,3}(`{3,}|~{3,})([^\r\n]*)\r?\n([\s\S]*?)^[ \t]{0,3}\1[ \t]*\r?$/gm;
95
+ for (const match of markdown.matchAll(fences)) {
96
+ if (match[2]!.trim() !== 'json') continue;
97
+ const source = match[3]!;
98
+ const line = markdown.slice(0, match.index).split('\n').length;
99
+ const location = `${page}:${line}`;
100
+
101
+ it(`${location} parses`, () => {
102
+ expect(() => JSON.parse(source)).not.toThrow();
103
+ });
104
+
105
+ // Parsing failures belong to the named parse test, not suite collection.
106
+ let parsed: unknown;
107
+ try {
108
+ parsed = JSON.parse(source);
109
+ } catch {
110
+ continue;
111
+ }
112
+ if (isIllustrative(source) || !isObject(parsed)) continue;
113
+ // Subject-only examples are fragments; credential-level markers must not
114
+ // silently become fragments when their context or type is malformed.
115
+ const credential =
116
+ '@context' in parsed ||
117
+ ('credentialSubject' in parsed && ('type' in parsed || 'issuer' in parsed));
118
+
119
+ if (credential) {
120
+ it(`${location} validates and issues`, async () => {
121
+ const prepared = JSON.parse(
122
+ source.includes('{{')
123
+ ? source.replace(/\{\{[^{}]+\}\}/g, 'Example')
124
+ : source
125
+ );
126
+ expect(prepared['@context']).toEqual(expect.any(Array));
127
+ expect(prepared.type).toEqual(expect.arrayContaining(['VerifiableCredential']));
128
+ prepared.issuer = wallet.id.did();
129
+ for (const subject of entries(prepared.credentialSubject)) {
130
+ if (isObject(subject)) subject.id ??= subjectDid;
131
+ }
132
+ const isVcV2 = (prepared['@context'] as unknown[]).some(
133
+ ctx => ctx === 'https://www.w3.org/ns/credentials/v2'
134
+ );
135
+ const dateField = isVcV2 ? 'validFrom' : 'issuanceDate';
136
+ prepared[dateField] ??= new Date().toISOString();
137
+ // Docs omit issuer/date intentionally: these are supplied during issuance.
138
+ const unsigned = UnsignedVCValidator.parse(prepared);
139
+ validateFragments(prepared);
140
+ const signed = await wallet.invoke.issueCredential(unsigned);
141
+ expect(signed.proof).toBeDefined();
142
+ }, 15_000);
143
+ } else {
144
+ it(`${location} validates fragment fields`, () => validateFragments(parsed));
145
+ }
146
+ }
147
+ }
148
+ });
@@ -4,7 +4,13 @@ import { describe, it, expect, beforeAll } from 'vitest';
4
4
 
5
5
  import { initLearnCard } from '@learncard/init';
6
6
 
7
- import { getAllFixtures, getFixture, isCredentialFixture, prepareFixture } from '../index';
7
+ import {
8
+ getAllFixtures,
9
+ getFixture,
10
+ isCredentialFixture,
11
+ prepareFixture,
12
+ buildFinalTranscriptVariant,
13
+ } from '../index';
8
14
 
9
15
  import type { CredentialFixture } from '../types';
10
16
 
@@ -69,4 +75,28 @@ describe('Credential issuance', () => {
69
75
 
70
76
  expect(signed.proof).toBeDefined();
71
77
  }, 30_000);
78
+
79
+ it('issues the provisional transcript fixture and its final variant without remote contexts', async () => {
80
+ const offlineWallet = await initLearnCard({
81
+ seed: 'c'.repeat(64),
82
+ didkit,
83
+ allowRemoteContexts: false,
84
+ });
85
+
86
+ const provisional = prepareFixture(getFixture('clr/provisional-transcript'), {
87
+ issuerDid: offlineWallet.id.did(),
88
+ subjectDid: 'did:example:test-subject-123',
89
+ });
90
+ const signedProvisional = await offlineWallet.invoke.issueCredential(provisional);
91
+
92
+ expect(signedProvisional.proof).toBeDefined();
93
+
94
+ const final = buildFinalTranscriptVariant(provisional, {
95
+ validFrom: new Date().toISOString(),
96
+ });
97
+ const signedFinal = await offlineWallet.invoke.issueCredential(final);
98
+
99
+ expect(signedFinal.proof).toBeDefined();
100
+ expect(signedFinal.id).toBe(signedProvisional.id);
101
+ }, 30_000);
72
102
  });
@@ -17,6 +17,7 @@ import {
17
17
  registerFixtures,
18
18
  prepareFixture,
19
19
  prepareFixtureById,
20
+ buildFinalTranscriptVariant,
20
21
  } from '../index';
21
22
 
22
23
  import { ALL_FIXTURES } from '../fixtures';
@@ -171,6 +172,65 @@ describe('Query API', () => {
171
172
  expect(() => getFixture('nonexistent/fixture')).toThrow('not found');
172
173
  });
173
174
 
175
+ // -----------------------------------------------------------------------
176
+ // Provisional transcript fixture (LC-2117 / LC-2135 / LC-2136)
177
+ // -----------------------------------------------------------------------
178
+
179
+ describe('clr/provisional-transcript', () => {
180
+ it('is discoverable as a valid CLR 2.0 fixture with a LearnCard managed refresh service', () => {
181
+ const fixture = getFixture('clr/provisional-transcript');
182
+
183
+ expect(isCredentialFixture(fixture)).toBe(true);
184
+ expect(fixture.spec).toBe('clr-v2');
185
+ expect(fixture.profile).toBe('learner-record');
186
+ expect(fixture.validity).toBe('valid');
187
+ expect(fixture.features).toContain('refresh-service');
188
+
189
+ if (!isCredentialFixture(fixture)) throw new Error('Expected W3C VC fixture');
190
+
191
+ const refreshService = (
192
+ fixture.credential as { refreshService?: { type?: unknown; id?: unknown } }
193
+ ).refreshService;
194
+
195
+ expect(refreshService?.type).toBe('LearnCardCredentialRefresh2026');
196
+ expect(typeof refreshService?.id).toBe('string');
197
+ });
198
+
199
+ it('prepares provisional and final variants sharing one credential ID, issuer, and subject', () => {
200
+ const provisional = prepareFixtureById('clr/provisional-transcript', {
201
+ issuerDid: 'did:example:test-issuer',
202
+ subjectDid: 'did:example:test-holder',
203
+ });
204
+
205
+ expect(typeof provisional.id).toBe('string');
206
+ expect(provisional.name).toContain('Provisional');
207
+
208
+ const final = buildFinalTranscriptVariant(provisional, {
209
+ validFrom: '2026-06-01T00:00:00Z',
210
+ });
211
+ const provisionalIssuer =
212
+ typeof provisional.issuer === 'string' ? provisional.issuer : provisional.issuer.id;
213
+ const finalIssuer = typeof final.issuer === 'string' ? final.issuer : final.issuer.id;
214
+ const provisionalSubject = Array.isArray(provisional.credentialSubject)
215
+ ? provisional.credentialSubject[0]
216
+ : provisional.credentialSubject;
217
+ const finalSubject = Array.isArray(final.credentialSubject)
218
+ ? final.credentialSubject[0]
219
+ : final.credentialSubject;
220
+
221
+ // Identity stability: the final version shares the credential ID, issuer,
222
+ // and subject with the provisional version.
223
+ expect(final.id).toBe(provisional.id);
224
+ expect(finalIssuer).toBe(provisionalIssuer);
225
+ expect(finalSubject?.id).toBe(provisionalSubject?.id);
226
+
227
+ // The final variant is materially different and marked final.
228
+ expect(final.name).toContain('Final');
229
+ expect(final.validFrom).toBe('2026-06-01T00:00:00Z');
230
+ expect(final.refreshService).toEqual(provisional.refreshService);
231
+ });
232
+ });
233
+
174
234
  it('findFixture returns undefined for unknown ID', () => {
175
235
  expect(findFixture('nonexistent/fixture')).toBeUndefined();
176
236
  });
@@ -528,8 +588,7 @@ describe('prepareFixture', () => {
528
588
  ? (programAchievement?.resultDescription as UnknownArrayRecord[])
529
589
  : [];
530
590
  const programResultDescriptionId = programResults[0]?.resultDescription as
531
- | string
532
- | undefined;
591
+ string | undefined;
533
592
  const programResultDescription = programResultDescriptions[0]?.id as string | undefined;
534
593
 
535
594
  expect(programResultDescriptionId).toBe(programResultDescription);
@@ -0,0 +1,193 @@
1
+ import { UnsignedClrCredentialValidator, type UnsignedVC } from '@learncard/types';
2
+
3
+ import type { CredentialFixture } from '../../types';
4
+
5
+ /**
6
+ * Provisional CLR 2.0 transcript with a `LearnCardCredentialRefresh2026` service, built for
7
+ * the managed credential refresh flow (LC-2117 / LC-2135 / LC-2136).
8
+ *
9
+ * The credential starts as a provisional transcript; `buildFinalTranscriptVariant`
10
+ * produces the final variant WITHOUT changing the credential ID, issuer, or subject,
11
+ * mirroring a registrar publishing the finalized record to the same refresh service.
12
+ *
13
+ * JSON-LD note: neither the VCDM 1.1/2.0 nor the live OBv3/CLR contexts define the
14
+ * term `LearnCardCredentialRefresh2026` (or the LearnCard `authorization` extension), so
15
+ * the fixture carries an inline context fragment — DIDKit's data-loss detection
16
+ * otherwise refuses to sign.
17
+ */
18
+
19
+ /** Inline JSON-LD context fragment defining the refresh-service terms inline. */
20
+ export const REFRESH_SERVICE_INLINE_CONTEXT = {
21
+ 'LearnCardCredentialRefresh2026':
22
+ 'https://learncard.com/refresh#LearnCardCredentialRefresh2026',
23
+ authorization: {
24
+ '@id': 'https://purl.imsglobal.org/spec/ob/v3p0#authorization',
25
+ '@context': {
26
+ LearnCardDIDAuth: 'https://docs.learncard.com/definitions#LearnCardDIDAuth',
27
+ },
28
+ },
29
+ } as const;
30
+
31
+ /**
32
+ * Placeholder refresh service on the unsigned fixture. Callers preparing the fixture
33
+ * for managed issuance replace `refreshService` with the allocated service descriptor
34
+ * (the allocated ID is only known after `allocateCredentialRefresh`).
35
+ */
36
+ const PLACEHOLDER_REFRESH_SERVICE = {
37
+ id: 'https://refresh.example.com/refresh/placeholder',
38
+ type: 'LearnCardCredentialRefresh2026',
39
+ authorization: { type: 'LearnCardDIDAuth' },
40
+ } as const;
41
+
42
+ export const clrProvisionalTranscript: CredentialFixture = {
43
+ id: 'clr/provisional-transcript',
44
+ name: 'Provisional Transcript (Refreshable)',
45
+ description:
46
+ 'A CLR v2 provisional transcript carrying a LearnCardCredentialRefresh2026 service. Paired with buildFinalTranscriptVariant, it demonstrates the managed provisional-to-final credential refresh flow.',
47
+ spec: 'clr-v2',
48
+ profile: 'learner-record',
49
+ features: ['refresh-service', 'results'],
50
+ source: 'synthetic',
51
+ signed: false,
52
+ validity: 'valid',
53
+ validator: UnsignedClrCredentialValidator,
54
+ tags: ['transcript', 'provisional', 'refresh', 'clr', 'managed-refresh'],
55
+
56
+ credential: {
57
+ '@context': [
58
+ 'https://www.w3.org/ns/credentials/v2',
59
+ 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
60
+ 'https://purl.imsglobal.org/spec/clr/v2p0/context.json',
61
+ REFRESH_SERVICE_INLINE_CONTEXT,
62
+ ],
63
+ id: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a47',
64
+ type: ['VerifiableCredential', 'ClrCredential'],
65
+ name: 'Provisional Transcript — Ridgeview Community College',
66
+ description:
67
+ 'Provisional record of courses completed to date. This transcript becomes final once the registrar certifies the completed term.',
68
+ issuer: {
69
+ id: 'did:web:registrar.ridgeview-example.edu',
70
+ type: ['Profile'],
71
+ name: 'Ridgeview Community College — Office of the Registrar',
72
+ },
73
+ validFrom: '2026-01-15T00:00:00Z',
74
+ refreshService: PLACEHOLDER_REFRESH_SERVICE,
75
+ credentialSubject: {
76
+ id: 'did:example:student-ridgeview-042',
77
+ type: ['ClrSubject'],
78
+ achievement: [
79
+ {
80
+ id: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a48',
81
+ type: ['Achievement'],
82
+ achievementType: 'Course',
83
+ name: 'BIO 150 — Principles of Biology',
84
+ description:
85
+ 'Introductory biology covering cell structure, genetics, and evolution.',
86
+ criteria: { narrative: 'Complete all coursework and the final exam.' },
87
+ humanCode: 'BIO-150',
88
+ fieldOfStudy: 'Biology',
89
+ creditsAvailable: 4,
90
+ resultDescription: [
91
+ {
92
+ id: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a49',
93
+ type: ['ResultDescription'],
94
+ name: 'Course Status',
95
+ resultType: 'RawScore',
96
+ },
97
+ ],
98
+ },
99
+ ],
100
+ verifiableCredential: [
101
+ {
102
+ '@context': [
103
+ 'https://www.w3.org/ns/credentials/v2',
104
+ 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
105
+ ],
106
+ id: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a50',
107
+ type: ['VerifiableCredential', 'AchievementCredential'],
108
+ name: 'BIO 150 — Principles of Biology (In Progress)',
109
+ issuer: {
110
+ id: 'did:web:registrar.ridgeview-example.edu',
111
+ type: ['Profile'],
112
+ name: 'Ridgeview Community College',
113
+ },
114
+ validFrom: '2026-01-15T00:00:00Z',
115
+ credentialSubject: {
116
+ id: 'did:example:student-ridgeview-042',
117
+ type: ['AchievementSubject'],
118
+ achievement: {
119
+ id: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a48',
120
+ type: ['Achievement'],
121
+ achievementType: 'Course',
122
+ name: 'BIO 150 — Principles of Biology',
123
+ criteria: {
124
+ narrative: 'Complete all coursework and the final exam.',
125
+ },
126
+ humanCode: 'BIO-150',
127
+ fieldOfStudy: 'Biology',
128
+ creditsAvailable: 4,
129
+ },
130
+ result: [
131
+ {
132
+ type: ['Result'],
133
+ resultDescription: 'urn:uuid:7d2e9f41-3c58-4e21-9b6a-1f0c5d8e2a49',
134
+ status: 'InProgress',
135
+ },
136
+ ],
137
+ },
138
+ },
139
+ ],
140
+ },
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Builds the final transcript variant from a prepared provisional credential.
146
+ *
147
+ * Preserves the credential ID, issuer, subject, and refresh service (identity
148
+ * invariants the managed refresh pipeline enforces) while flipping user-visible
149
+ * content to its final state: name/description become final, the nested course
150
+ * credential shows the completed result, and `validFrom` moves forward.
151
+ */
152
+ export const buildFinalTranscriptVariant = (
153
+ provisional: UnsignedVC,
154
+ options: { validFrom?: string } = {}
155
+ ): UnsignedVC => {
156
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
157
+ typeof value === 'object' && value !== null;
158
+
159
+ const final = JSON.parse(JSON.stringify(provisional)) as Record<string, unknown>;
160
+
161
+ final.name = 'Final Official Transcript — Ridgeview Community College';
162
+ final.description =
163
+ 'Final official transcript certified by the registrar. Supersedes the provisional record.';
164
+
165
+ if (options.validFrom) final.validFrom = options.validFrom;
166
+
167
+ const subject = final.credentialSubject;
168
+
169
+ if (isRecord(subject) && Array.isArray(subject.verifiableCredential)) {
170
+ subject.verifiableCredential = subject.verifiableCredential.map((nested: unknown) => {
171
+ if (!isRecord(nested)) return nested;
172
+
173
+ const next = { ...nested };
174
+
175
+ if (typeof next.name === 'string') {
176
+ next.name = next.name.replace('(In Progress)', '(Completed)');
177
+ }
178
+
179
+ if (isRecord(next.credentialSubject) && Array.isArray(next.credentialSubject.result)) {
180
+ next.credentialSubject = {
181
+ ...next.credentialSubject,
182
+ result: next.credentialSubject.result.map((result: unknown) =>
183
+ isRecord(result) ? { ...result, status: 'Completed', value: 'A' } : result
184
+ ),
185
+ };
186
+ }
187
+
188
+ return next;
189
+ });
190
+ }
191
+
192
+ return final as unknown as UnsignedVC;
193
+ };
@@ -56,6 +56,11 @@ 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 {
60
+ clrProvisionalTranscript,
61
+ buildFinalTranscriptVariant,
62
+ REFRESH_SERVICE_INLINE_CONTEXT,
63
+ } from './clr/provisional-transcript';
59
64
  import { clrDemoIsdDiplomaAssessments } from './clr/demo-isd-diploma-assessments';
60
65
 
61
66
  // SD-JWT VC
@@ -120,6 +125,7 @@ export const ALL_FIXTURES: LibraryFixture[] = [
120
125
  clrGreatPlainsFull,
121
126
  clrWestbridgeFull,
122
127
  clrCompetencyAligned,
128
+ clrProvisionalTranscript,
123
129
  clrDemoIsdDiplomaAssessments,
124
130
 
125
131
  // SD-JWT VC
@@ -167,6 +173,9 @@ export {
167
173
  clrGreatPlainsFull,
168
174
  clrWestbridgeFull,
169
175
  clrCompetencyAligned,
176
+ clrProvisionalTranscript,
177
+ buildFinalTranscriptVariant,
178
+ REFRESH_SERVICE_INLINE_CONTEXT,
170
179
  clrDemoIsdDiplomaAssessments,
171
180
  sdJwtVcCourseCompletion,
172
181
  };