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