@kravc/schema 2.7.6 → 2.8.0-alpha.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 +19 -14
- package/dist/CredentialFactory.d.ts +345 -0
- package/dist/CredentialFactory.d.ts.map +1 -0
- package/dist/CredentialFactory.js +381 -0
- package/dist/CredentialFactory.js.map +1 -0
- package/dist/Schema.d.ts +448 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +506 -0
- package/dist/Schema.js.map +1 -0
- package/dist/ValidationError.d.ts +70 -0
- package/dist/ValidationError.d.ts.map +1 -0
- package/dist/ValidationError.js +78 -0
- package/dist/ValidationError.js.map +1 -0
- package/dist/Validator.d.ts +483 -0
- package/dist/Validator.d.ts.map +1 -0
- package/dist/Validator.js +570 -0
- package/dist/Validator.js.map +1 -0
- package/dist/helpers/JsonSchema.d.ts +99 -0
- package/dist/helpers/JsonSchema.d.ts.map +1 -0
- package/dist/helpers/JsonSchema.js +3 -0
- package/dist/helpers/JsonSchema.js.map +1 -0
- package/dist/helpers/cleanupAttributes.d.ts +34 -0
- package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
- package/dist/helpers/cleanupAttributes.js +113 -0
- package/dist/helpers/cleanupAttributes.js.map +1 -0
- package/dist/helpers/cleanupNulls.d.ts +27 -0
- package/dist/helpers/cleanupNulls.d.ts.map +1 -0
- package/dist/helpers/cleanupNulls.js +96 -0
- package/dist/helpers/cleanupNulls.js.map +1 -0
- package/dist/helpers/getReferenceIds.d.ts +169 -0
- package/dist/helpers/getReferenceIds.d.ts.map +1 -0
- package/dist/helpers/getReferenceIds.js +241 -0
- package/dist/helpers/getReferenceIds.js.map +1 -0
- package/dist/helpers/got.d.ts +60 -0
- package/dist/helpers/got.d.ts.map +1 -0
- package/dist/helpers/got.js +72 -0
- package/dist/helpers/got.js.map +1 -0
- package/dist/helpers/mapObjectProperties.d.ts +150 -0
- package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
- package/dist/helpers/mapObjectProperties.js +229 -0
- package/dist/helpers/mapObjectProperties.js.map +1 -0
- package/dist/helpers/normalizeAttributes.d.ts +213 -0
- package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
- package/dist/helpers/normalizeAttributes.js +243 -0
- package/dist/helpers/normalizeAttributes.js.map +1 -0
- package/dist/helpers/normalizeProperties.d.ts +168 -0
- package/dist/helpers/normalizeProperties.d.ts.map +1 -0
- package/dist/helpers/normalizeProperties.js +223 -0
- package/dist/helpers/normalizeProperties.js.map +1 -0
- package/dist/helpers/normalizeRequired.d.ts +159 -0
- package/dist/helpers/normalizeRequired.d.ts.map +1 -0
- package/dist/helpers/normalizeRequired.js +206 -0
- package/dist/helpers/normalizeRequired.js.map +1 -0
- package/dist/helpers/normalizeType.d.ts +81 -0
- package/dist/helpers/normalizeType.d.ts.map +1 -0
- package/dist/helpers/normalizeType.js +210 -0
- package/dist/helpers/normalizeType.js.map +1 -0
- package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
- package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
- package/dist/helpers/nullifyEmptyValues.js +191 -0
- package/dist/helpers/nullifyEmptyValues.js.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.js +138 -0
- package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
- package/dist/helpers/validateId.d.ts +39 -0
- package/dist/helpers/validateId.d.ts.map +1 -0
- package/dist/helpers/validateId.js +51 -0
- package/dist/helpers/validateId.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ld/documentLoader.d.ts +8 -0
- package/dist/ld/documentLoader.d.ts.map +1 -0
- package/dist/ld/documentLoader.js +24 -0
- package/dist/ld/documentLoader.js.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.js +32 -0
- package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
- package/dist/ld/getLinkedDataContext.d.ts +19 -0
- package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
- package/dist/ld/getLinkedDataContext.js +50 -0
- package/dist/ld/getLinkedDataContext.js.map +1 -0
- package/eslint.config.mjs +32 -52
- package/examples/credentials/createAccountCredential.ts +27 -0
- package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
- package/examples/index.ts +7 -0
- package/examples/schemas/FavoriteItemSchema.ts +27 -0
- package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
- package/examples/schemas/PreferencesSchema.ts +29 -0
- package/examples/schemas/ProfileSchema.ts +91 -0
- package/examples/schemas/Status.yaml +3 -0
- package/examples/schemas/StatusSchema.ts +12 -0
- package/jest.config.mjs +5 -0
- package/package.json +27 -20
- package/src/CredentialFactory.ts +392 -0
- package/src/Schema.ts +583 -0
- package/src/ValidationError.ts +90 -0
- package/src/Validator.ts +603 -0
- package/src/__tests__/CredentialFactory.test.ts +588 -0
- package/src/__tests__/Schema.test.ts +371 -0
- package/src/__tests__/ValidationError.test.ts +235 -0
- package/src/__tests__/Validator.test.ts +787 -0
- package/src/helpers/JsonSchema.ts +119 -0
- package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
- package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
- package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
- package/src/helpers/__tests__/got.test.ts +193 -0
- package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
- package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
- package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
- package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
- package/src/helpers/__tests__/normalizeType.test.ts +772 -0
- package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
- package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
- package/src/helpers/__tests__/validateId.test.ts +118 -0
- package/src/helpers/cleanupAttributes.ts +151 -0
- package/src/helpers/cleanupNulls.ts +106 -0
- package/src/helpers/getReferenceIds.ts +273 -0
- package/src/helpers/got.ts +73 -0
- package/src/helpers/mapObjectProperties.ts +272 -0
- package/src/helpers/normalizeAttributes.ts +247 -0
- package/src/helpers/normalizeProperties.ts +249 -0
- package/src/helpers/normalizeRequired.ts +233 -0
- package/src/helpers/normalizeType.ts +235 -0
- package/src/helpers/nullifyEmptyValues.ts +207 -0
- package/src/helpers/removeRequiredAndDefault.ts +151 -0
- package/src/helpers/validateId.ts +53 -0
- package/src/index.ts +13 -0
- package/src/ld/__tests__/documentLoader.test.ts +57 -0
- package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
- package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
- package/src/ld/documentLoader.ts +28 -0
- package/src/ld/getLinkedDataAttributeType.ts +46 -0
- package/src/ld/getLinkedDataContext.ts +80 -0
- package/tsconfig.json +27 -0
- package/types/credentials-context.d.ts +14 -0
- package/types/security-context.d.ts +6 -0
- package/examples/Status.yaml +0 -3
- package/examples/createAccountCredential.js +0 -27
- package/examples/createMineSweeperScoreCredential.js +0 -63
- package/examples/index.js +0 -9
- package/src/CredentialFactory.js +0 -67
- package/src/CredentialFactory.spec.js +0 -131
- package/src/Schema.js +0 -104
- package/src/Schema.spec.js +0 -172
- package/src/ValidationError.js +0 -31
- package/src/Validator.js +0 -128
- package/src/Validator.spec.js +0 -355
- package/src/helpers/cleanupAttributes.js +0 -71
- package/src/helpers/cleanupNulls.js +0 -42
- package/src/helpers/getReferenceIds.js +0 -71
- package/src/helpers/mapObject.js +0 -65
- package/src/helpers/normalizeAttributes.js +0 -28
- package/src/helpers/normalizeProperties.js +0 -61
- package/src/helpers/normalizeRequired.js +0 -37
- package/src/helpers/normalizeType.js +0 -41
- package/src/helpers/nullifyEmptyValues.js +0 -57
- package/src/helpers/removeRequiredAndDefault.js +0 -30
- package/src/helpers/validateId.js +0 -19
- package/src/index.d.ts +0 -25
- package/src/index.js +0 -8
- package/src/ld/documentLoader.js +0 -25
- package/src/ld/documentLoader.spec.js +0 -12
- package/src/ld/getLinkedDataContext.js +0 -63
- package/src/ld/getLinkedDataType.js +0 -38
- /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
- /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { canonize } from 'jsonld';
|
|
2
|
+
import { Schema, CredentialFactory, documentLoader } from '../../src';
|
|
3
|
+
import ValidationError from '../ValidationError';
|
|
4
|
+
import {
|
|
5
|
+
createAccountCredential,
|
|
6
|
+
createMineSweeperScoreCredential
|
|
7
|
+
} from '../../examples';
|
|
8
|
+
|
|
9
|
+
// Wrap documentLoader to return a Promise for jsonld compatibility
|
|
10
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
11
|
+
const asyncDocumentLoader = async (url: string) => {
|
|
12
|
+
const result = documentLoader(url);
|
|
13
|
+
return {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
document: result.document as any,
|
|
16
|
+
contextUrl: result.contextUrl ?? undefined,
|
|
17
|
+
documentUrl: result.documentUrl
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
} as any;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('CredentialFactory', () => {
|
|
23
|
+
describe('CredentialFactory.constructor(uri, schemas)', () => {
|
|
24
|
+
it('throws error if "uri" parameter is missing', () => {
|
|
25
|
+
expect(
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
() => new CredentialFactory(undefined as any, [])
|
|
28
|
+
).toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws error if "uri" parameter is not a URL', () => {
|
|
32
|
+
expect(
|
|
33
|
+
() => new CredentialFactory('BAD_URL', [])
|
|
34
|
+
).toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('creates factory with minimal schema', () => {
|
|
38
|
+
const minimalSchema = new Schema({}, 'MinimalSchema');
|
|
39
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [minimalSchema]);
|
|
40
|
+
expect(factory).toBeDefined();
|
|
41
|
+
expect(factory.credentialType).toBe('TestV1');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('creates factory with single schema', () => {
|
|
45
|
+
const schema = new Schema({
|
|
46
|
+
name: { type: 'string', required: true }
|
|
47
|
+
}, 'TestSchema');
|
|
48
|
+
|
|
49
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema]);
|
|
50
|
+
expect(factory).toBeDefined();
|
|
51
|
+
expect(factory.credentialType).toBe('TestV1');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('creates factory with multiple schemas', () => {
|
|
55
|
+
const schema1 = new Schema({ name: { type: 'string' } }, 'Schema1');
|
|
56
|
+
const schema2 = new Schema({ value: { type: 'number' } }, 'Schema2');
|
|
57
|
+
|
|
58
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema1, schema2]);
|
|
59
|
+
expect(factory).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('processes schemas without URL by creating new Schema with factory URI', () => {
|
|
63
|
+
const schemaWithoutUrl = new Schema({
|
|
64
|
+
name: { type: 'string', required: true }
|
|
65
|
+
}, 'TestSchema');
|
|
66
|
+
|
|
67
|
+
expect(schemaWithoutUrl.url).toBeUndefined();
|
|
68
|
+
|
|
69
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithoutUrl]);
|
|
70
|
+
const context = factory.context;
|
|
71
|
+
|
|
72
|
+
// Schema should now have linkedDataType with factory URI
|
|
73
|
+
expect(context.TestSchema).toBeDefined();
|
|
74
|
+
expect(context.TestSchema['@id']).toContain('https://example.com/schema/TestV1');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('preserves schemas with existing URL', () => {
|
|
78
|
+
const schemaWithUrl = new Schema({
|
|
79
|
+
name: { type: 'string', required: true }
|
|
80
|
+
}, 'TestSchema', 'https://schema.org/');
|
|
81
|
+
|
|
82
|
+
expect(schemaWithUrl.url).toBe('https://schema.org/');
|
|
83
|
+
|
|
84
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithUrl]);
|
|
85
|
+
const context = factory.context;
|
|
86
|
+
|
|
87
|
+
// Schema should keep its original URL
|
|
88
|
+
expect(context.TestSchema).toBeDefined();
|
|
89
|
+
expect(context.TestSchema['@id']).toContain('https://schema.org/');
|
|
90
|
+
expect(context.TestSchema['@id']).not.toContain('https://example.com/schema/TestV1');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('throws error if schema validation fails', () => {
|
|
94
|
+
// Create schemas with invalid reference that will fail validation
|
|
95
|
+
const schemaWithInvalidRef = new Schema({
|
|
96
|
+
invalid: {
|
|
97
|
+
$ref: 'NonExistentSchema',
|
|
98
|
+
required: true
|
|
99
|
+
}
|
|
100
|
+
}, 'InvalidSchema');
|
|
101
|
+
|
|
102
|
+
// This will fail during Validator construction because referenced schema doesn't exist
|
|
103
|
+
expect(
|
|
104
|
+
() => new CredentialFactory('https://example.com/schema/TestV1', [schemaWithInvalidRef])
|
|
105
|
+
).toThrow('Schemas validation failed');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('.credentialType', () => {
|
|
110
|
+
let minimalSchema: Schema;
|
|
111
|
+
|
|
112
|
+
beforeAll(() => {
|
|
113
|
+
minimalSchema = new Schema({}, 'MinimalSchema');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns credential type from URI', () => {
|
|
117
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1', [minimalSchema]);
|
|
118
|
+
expect(factory.credentialType).toBe('AccountV1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns last segment of URI path', () => {
|
|
122
|
+
const factory = new CredentialFactory('https://example.com/path/to/CredentialV2', [minimalSchema]);
|
|
123
|
+
expect(factory.credentialType).toBe('CredentialV2');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles URI with trailing slash', () => {
|
|
127
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1/', [minimalSchema]);
|
|
128
|
+
expect(factory.credentialType).toBe('');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('handles URI with query parameters', () => {
|
|
132
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1?version=1', [minimalSchema]);
|
|
133
|
+
expect(factory.credentialType).toBe('AccountV1?version=1');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles URI with hash fragment', () => {
|
|
137
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1#section', [minimalSchema]);
|
|
138
|
+
expect(factory.credentialType).toBe('AccountV1#section');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('.context', () => {
|
|
143
|
+
let minimalSchema: Schema;
|
|
144
|
+
|
|
145
|
+
beforeAll(() => {
|
|
146
|
+
minimalSchema = new Schema({}, 'MinimalSchema');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns context with credential type mapping', () => {
|
|
150
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1', [minimalSchema]);
|
|
151
|
+
const context = factory.context;
|
|
152
|
+
|
|
153
|
+
expect(context.AccountV1).toEqual({ '@id': 'https://example.com/schema/AccountV1' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('includes schema linked data types in context', () => {
|
|
157
|
+
const schema = new Schema({
|
|
158
|
+
name: { type: 'string', required: true }
|
|
159
|
+
}, 'Account', 'https://example.com/schema/');
|
|
160
|
+
|
|
161
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1', [schema]);
|
|
162
|
+
const context = factory.context;
|
|
163
|
+
|
|
164
|
+
expect(context.AccountV1).toBeDefined();
|
|
165
|
+
expect(context.Account).toBeDefined();
|
|
166
|
+
expect(context.Account['@id']).toBeDefined();
|
|
167
|
+
expect(context.Account['@context']).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('includes multiple schema types in context', () => {
|
|
171
|
+
const schema1 = new Schema({ name: { type: 'string' } }, 'Schema1', 'https://example.com/');
|
|
172
|
+
const schema2 = new Schema({ value: { type: 'number' } }, 'Schema2', 'https://example.com/');
|
|
173
|
+
|
|
174
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema1, schema2]);
|
|
175
|
+
const context = factory.context;
|
|
176
|
+
|
|
177
|
+
expect(context.TestV1).toBeDefined();
|
|
178
|
+
expect(context.Schema1).toBeDefined();
|
|
179
|
+
expect(context.Schema2).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not include schemas without linkedDataType in context', () => {
|
|
183
|
+
// Schema without URL won't have linkedDataType
|
|
184
|
+
const schemaWithoutUrl = new Schema({
|
|
185
|
+
name: { type: 'string' }
|
|
186
|
+
}, 'TestSchema');
|
|
187
|
+
|
|
188
|
+
const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithoutUrl]);
|
|
189
|
+
const context = factory.context;
|
|
190
|
+
|
|
191
|
+
// Only credential type should be present, schema without URL won't have linkedDataType
|
|
192
|
+
expect(context.TestV1).toBeDefined();
|
|
193
|
+
// Note: Schema without URL will get linkedDataType created during factory construction
|
|
194
|
+
// So TestSchema should actually be present
|
|
195
|
+
expect(context.TestSchema).toBeDefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('merges credential type with schema contexts', () => {
|
|
199
|
+
const schema = new Schema({
|
|
200
|
+
name: { type: 'string' }
|
|
201
|
+
}, 'Account', 'https://example.com/schema/');
|
|
202
|
+
|
|
203
|
+
const factory = new CredentialFactory('https://example.com/schema/AccountV1', [schema]);
|
|
204
|
+
const context = factory.context;
|
|
205
|
+
|
|
206
|
+
// Credential type should be first, then schema contexts
|
|
207
|
+
expect(Object.keys(context)[0]).toBe('AccountV1');
|
|
208
|
+
expect(context.Account).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('.createCredential(id, holder, subject = {})', () => {
|
|
213
|
+
let factory: CredentialFactory;
|
|
214
|
+
let simpleFactory: CredentialFactory;
|
|
215
|
+
|
|
216
|
+
beforeAll(() => {
|
|
217
|
+
const videoGameSchema = new Schema({
|
|
218
|
+
id: {},
|
|
219
|
+
name: { type: 'string', required: true },
|
|
220
|
+
version: { type: 'string', required: true }
|
|
221
|
+
}, 'VideoGame', 'https://schema.org/');
|
|
222
|
+
|
|
223
|
+
factory = new CredentialFactory('https://example.com/StarCraft', [videoGameSchema]);
|
|
224
|
+
|
|
225
|
+
const simpleSchema = new Schema({
|
|
226
|
+
id: {},
|
|
227
|
+
name: { type: 'string', required: true },
|
|
228
|
+
email: { type: 'string', format: 'email' }
|
|
229
|
+
}, 'User');
|
|
230
|
+
|
|
231
|
+
simpleFactory = new CredentialFactory('https://example.com/schema/UserV1', [simpleSchema]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('returns single schema based credential', async () => {
|
|
235
|
+
const credential = await createAccountCredential('did:PLAYER_ID', 'CAHTEP');
|
|
236
|
+
|
|
237
|
+
expect(credential).toBeDefined();
|
|
238
|
+
await canonize(credential, { documentLoader: asyncDocumentLoader });
|
|
239
|
+
|
|
240
|
+
const { credentialSubject: { createdAt } } = credential;
|
|
241
|
+
expect(credential).toEqual({
|
|
242
|
+
'@context': [
|
|
243
|
+
'https://www.w3.org/2018/credentials/v1',
|
|
244
|
+
{
|
|
245
|
+
AccountV1: { '@id': 'https://example.com/schema/AccountV1' },
|
|
246
|
+
Account: {
|
|
247
|
+
'@id': 'https://example.com/schema/AccountV1#Account',
|
|
248
|
+
'@context': {
|
|
249
|
+
'@vocab': 'https://example.com/schema/AccountV1#',
|
|
250
|
+
'@version': 1.1,
|
|
251
|
+
'@protected': true,
|
|
252
|
+
schema: 'https://schema.org/',
|
|
253
|
+
username: { '@id': 'username' },
|
|
254
|
+
createdAt: { '@id': 'createdAt', '@type': 'schema:DateTime' },
|
|
255
|
+
dateOfBirth: { '@id': 'dateOfBirth', '@type': 'schema:Date' }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
id: 'https://example.com/account/CAHTEP',
|
|
261
|
+
type: [ 'VerifiableCredential', 'AccountV1' ],
|
|
262
|
+
holder: 'did:PLAYER_ID',
|
|
263
|
+
credentialSubject: {
|
|
264
|
+
id: 'did:PLAYER_ID',
|
|
265
|
+
username: 'CAHTEP',
|
|
266
|
+
createdAt,
|
|
267
|
+
type: 'Account'
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('returns multiple schemas based credential', async () => {
|
|
273
|
+
const playerScore = {
|
|
274
|
+
wins: 5,
|
|
275
|
+
losses: 5,
|
|
276
|
+
winRate: 50,
|
|
277
|
+
UNDEFINED: 'VALUE',
|
|
278
|
+
bestScore: 23450,
|
|
279
|
+
endurance: 'P5M22S',
|
|
280
|
+
dateCreated: '2020-10-10T00:00:00Z',
|
|
281
|
+
bestRoundTime: 10000
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const credential = await createMineSweeperScoreCredential(
|
|
285
|
+
'did:GAME_ID',
|
|
286
|
+
'did:PLAYER_ID',
|
|
287
|
+
playerScore
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(credential).toBeDefined();
|
|
291
|
+
await canonize(credential, { documentLoader: asyncDocumentLoader });
|
|
292
|
+
|
|
293
|
+
const { credentialSubject } = credential;
|
|
294
|
+
expect(credentialSubject.type).toBe('Player');
|
|
295
|
+
expect(credentialSubject.hasVideoGameScore.type).toBe('VideoGameScore');
|
|
296
|
+
expect(credentialSubject.hasVideoGameScore.game.type).toBe('VideoGame');
|
|
297
|
+
expect(credentialSubject.hasVideoGameScore.UNDEFINED).toBeUndefined();
|
|
298
|
+
|
|
299
|
+
const customContext = credential['@context'][1];
|
|
300
|
+
expect(customContext.VideoGame).toEqual({
|
|
301
|
+
'@id': 'https://schema.org/VideoGame',
|
|
302
|
+
'@context': {
|
|
303
|
+
'@protected': true,
|
|
304
|
+
'@version': 1.1,
|
|
305
|
+
'@vocab': 'https://schema.org/',
|
|
306
|
+
name: { '@id': 'name' },
|
|
307
|
+
version: { '@id': 'version' }
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('throws error if "id" parameter is missing', () => {
|
|
313
|
+
expect(
|
|
314
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
315
|
+
() => factory.createCredential(undefined as any, 'did:HOLDER_ID')
|
|
316
|
+
).toThrow('Parameter "id" is required');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('throws error if "holder" parameter is missing', () => {
|
|
320
|
+
expect(
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
322
|
+
() => factory.createCredential('did:CREDENTIAL_ID', undefined as any)
|
|
323
|
+
).toThrow('Parameter "holder" is required');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('throws error if "id" parameter is not a valid URL', () => {
|
|
327
|
+
expect(
|
|
328
|
+
() => factory.createCredential('INVALID_ID', 'did:HOLDER_ID')
|
|
329
|
+
).toThrow('Parameter "id" must be a URL');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('throws error if "holder" parameter is not a valid URL', () => {
|
|
333
|
+
expect(
|
|
334
|
+
() => factory.createCredential('did:CREDENTIAL_ID', 'INVALID_HOLDER')
|
|
335
|
+
).toThrow('Parameter "holder" must be a URL');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('creates credential with empty subject when schema allows it', () => {
|
|
339
|
+
const optionalSchema = new Schema({
|
|
340
|
+
id: {},
|
|
341
|
+
name: { type: 'string' } // Not required
|
|
342
|
+
}, 'OptionalUser');
|
|
343
|
+
|
|
344
|
+
const optionalFactory = new CredentialFactory(
|
|
345
|
+
'https://example.com/schema/OptionalUserV1',
|
|
346
|
+
[optionalSchema]
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const credential = optionalFactory.createCredential(
|
|
350
|
+
'https://example.com/credentials/123',
|
|
351
|
+
'did:holder:456',
|
|
352
|
+
{ id: 'did:holder:456' }
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(credential).toBeDefined();
|
|
356
|
+
expect(credential.id).toBe('https://example.com/credentials/123');
|
|
357
|
+
expect(credential.holder).toBe('did:holder:456');
|
|
358
|
+
expect(credential.type).toEqual(['VerifiableCredential', 'OptionalUserV1']);
|
|
359
|
+
expect(credential['@context']).toBeDefined();
|
|
360
|
+
expect(credential.credentialSubject).toBeDefined();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('creates credential with minimal required fields', () => {
|
|
364
|
+
const credential = simpleFactory.createCredential(
|
|
365
|
+
'https://example.com/credentials/123',
|
|
366
|
+
'did:holder:456',
|
|
367
|
+
{
|
|
368
|
+
id: 'did:holder:456',
|
|
369
|
+
name: 'John Doe'
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(credential.credentialSubject).toEqual({
|
|
374
|
+
id: 'did:holder:456',
|
|
375
|
+
name: 'John Doe',
|
|
376
|
+
type: 'User'
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('includes VerifiableCredential in type array', () => {
|
|
381
|
+
const credential = simpleFactory.createCredential(
|
|
382
|
+
'https://example.com/credentials/123',
|
|
383
|
+
'did:holder:456',
|
|
384
|
+
{ id: 'did:holder:456', name: 'John' }
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(credential.type).toContain('VerifiableCredential');
|
|
388
|
+
expect(credential.type).toContain('UserV1');
|
|
389
|
+
expect(credential.type).toHaveLength(2);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('includes credentials context URL in @context', () => {
|
|
393
|
+
const credential = simpleFactory.createCredential(
|
|
394
|
+
'https://example.com/credentials/123',
|
|
395
|
+
'did:holder:456',
|
|
396
|
+
{ id: 'did:holder:456', name: 'John' }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(credential['@context']).toBeInstanceOf(Array);
|
|
400
|
+
expect(credential['@context'][0]).toBe('https://www.w3.org/2018/credentials/v1');
|
|
401
|
+
expect(credential['@context'][1]).toBeDefined();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('includes factory context in @context', () => {
|
|
405
|
+
const credential = simpleFactory.createCredential(
|
|
406
|
+
'https://example.com/credentials/123',
|
|
407
|
+
'did:holder:456',
|
|
408
|
+
{ id: 'did:holder:456', name: 'John' }
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const customContext = credential['@context'][1];
|
|
412
|
+
expect(customContext.UserV1).toBeDefined();
|
|
413
|
+
expect(customContext.UserV1['@id']).toBe('https://example.com/schema/UserV1');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('validates subject against root schema', () => {
|
|
417
|
+
expect(
|
|
418
|
+
() => simpleFactory.createCredential(
|
|
419
|
+
'https://example.com/credentials/123',
|
|
420
|
+
'did:holder:456',
|
|
421
|
+
{
|
|
422
|
+
id: 'did:holder:456'
|
|
423
|
+
// Missing required 'name' field
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
).toThrow(ValidationError);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('throws ValidationError with correct schema ID', () => {
|
|
430
|
+
try {
|
|
431
|
+
simpleFactory.createCredential(
|
|
432
|
+
'https://example.com/credentials/123',
|
|
433
|
+
'did:holder:456',
|
|
434
|
+
{
|
|
435
|
+
id: 'did:holder:456'
|
|
436
|
+
// Missing required 'name' field
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
fail('Should have thrown ValidationError');
|
|
440
|
+
} catch (error) {
|
|
441
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
442
|
+
const validationError = error as ValidationError;
|
|
443
|
+
const errorJson = validationError.toJSON();
|
|
444
|
+
expect(errorJson.schemaId).toBe('User');
|
|
445
|
+
expect(errorJson.message).toBe('"User" validation failed');
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('applies schema defaults to subject', () => {
|
|
450
|
+
const schemaWithDefault = new Schema({
|
|
451
|
+
id: {},
|
|
452
|
+
name: { type: 'string', required: true },
|
|
453
|
+
status: { type: 'string', default: 'active' }
|
|
454
|
+
}, 'UserWithDefault');
|
|
455
|
+
|
|
456
|
+
const factoryWithDefault = new CredentialFactory(
|
|
457
|
+
'https://example.com/schema/UserV1',
|
|
458
|
+
[schemaWithDefault]
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const credential = factoryWithDefault.createCredential(
|
|
462
|
+
'https://example.com/credentials/123',
|
|
463
|
+
'did:holder:456',
|
|
464
|
+
{
|
|
465
|
+
id: 'did:holder:456',
|
|
466
|
+
name: 'John'
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(credential.credentialSubject.status).toBe('active');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('normalizes subject data according to schema', () => {
|
|
474
|
+
const credential = simpleFactory.createCredential(
|
|
475
|
+
'https://example.com/credentials/123',
|
|
476
|
+
'did:holder:456',
|
|
477
|
+
{
|
|
478
|
+
id: 'did:holder:456',
|
|
479
|
+
name: 'John Doe',
|
|
480
|
+
email: 'john@example.com'
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
expect(credential.credentialSubject.email).toBe('john@example.com');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('sets credentialSubject type to root schema ID', () => {
|
|
488
|
+
const credential = simpleFactory.createCredential(
|
|
489
|
+
'https://example.com/credentials/123',
|
|
490
|
+
'did:holder:456',
|
|
491
|
+
{
|
|
492
|
+
id: 'did:holder:456',
|
|
493
|
+
name: 'John'
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
expect(credential.credentialSubject.type).toBe('User');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('handles subject with nested objects when schemas support it', () => {
|
|
501
|
+
const nestedSchema = new Schema({
|
|
502
|
+
id: {},
|
|
503
|
+
profile: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
properties: {
|
|
506
|
+
name: { type: 'string', required: true },
|
|
507
|
+
age: { type: 'integer' }
|
|
508
|
+
},
|
|
509
|
+
required: true
|
|
510
|
+
}
|
|
511
|
+
}, 'UserWithProfile');
|
|
512
|
+
|
|
513
|
+
const nestedFactory = new CredentialFactory(
|
|
514
|
+
'https://example.com/schema/UserV1',
|
|
515
|
+
[nestedSchema]
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const credential = nestedFactory.createCredential(
|
|
519
|
+
'https://example.com/credentials/123',
|
|
520
|
+
'did:holder:456',
|
|
521
|
+
{
|
|
522
|
+
id: 'did:holder:456',
|
|
523
|
+
profile: {
|
|
524
|
+
name: 'John',
|
|
525
|
+
age: 30
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
expect(credential.credentialSubject.profile).toBeDefined();
|
|
531
|
+
expect(credential.credentialSubject.profile.name).toBe('John');
|
|
532
|
+
expect(credential.credentialSubject.profile.age).toBe(30);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('removes undefined properties from subject', () => {
|
|
536
|
+
const credential = simpleFactory.createCredential(
|
|
537
|
+
'https://example.com/credentials/123',
|
|
538
|
+
'did:holder:456',
|
|
539
|
+
{
|
|
540
|
+
id: 'did:holder:456',
|
|
541
|
+
name: 'John',
|
|
542
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
543
|
+
undefinedField: undefined as any
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
548
|
+
expect((credential.credentialSubject as any).undefinedField).toBeUndefined();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('handles credential ID with different URL formats', () => {
|
|
552
|
+
const testCases = [
|
|
553
|
+
'https://example.com/credentials/123',
|
|
554
|
+
'did:example:123',
|
|
555
|
+
'http://example.com/credentials/456'
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
testCases.forEach(credentialId => {
|
|
559
|
+
const credential = simpleFactory.createCredential(
|
|
560
|
+
credentialId,
|
|
561
|
+
'did:holder:456',
|
|
562
|
+
{ id: 'did:holder:456', name: 'John' }
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
expect(credential.id).toBe(credentialId);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('handles holder with different identifier formats', () => {
|
|
570
|
+
const testCases = [
|
|
571
|
+
'did:example:123',
|
|
572
|
+
'https://example.com/users/123',
|
|
573
|
+
'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
testCases.forEach(holder => {
|
|
577
|
+
const credential = simpleFactory.createCredential(
|
|
578
|
+
'https://example.com/credentials/123',
|
|
579
|
+
holder,
|
|
580
|
+
{ id: holder, name: 'John' }
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
expect(credential.holder).toBe(holder);
|
|
584
|
+
expect(credential.credentialSubject.id).toBe(holder);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|