dvlprsh-dcql-test 0.1.8
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/CHANGELOG.md +37 -0
- package/dist/index.d.mts +135 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +341 -0
- package/dist/index.mjs +314 -0
- package/package.json +59 -0
- package/src/credentials/credential.ts +12 -0
- package/src/credentials/sdjwtvc.credential.ts +208 -0
- package/src/dcql.ts +170 -0
- package/src/index.ts +4 -0
- package/src/match.ts +88 -0
- package/src/test/dcql.spec.ts +295 -0
- package/src/test/index.spec.ts +7 -0
- package/src/test/pathMatch.spec.ts +163 -0
- package/src/test/sdjwtvc-credentials.spec.ts +259 -0
- package/src/type.ts +65 -0
- package/tsconfig.json +9 -0
- package/vitest.config.mts +4 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
2
|
+
import { SdJwtVcCredential } from '../credentials/sdjwtvc.credential';
|
3
|
+
import { pathMatch } from '../match';
|
4
|
+
|
5
|
+
describe('pathMatch', () => {
|
6
|
+
let credential: SdJwtVcCredential;
|
7
|
+
|
8
|
+
beforeEach(() => {
|
9
|
+
credential = new SdJwtVcCredential('test-id', 'test-vct-value');
|
10
|
+
});
|
11
|
+
|
12
|
+
describe('processPathPointer', () => {
|
13
|
+
describe('String components - object key selection', () => {
|
14
|
+
it('should select a property by name', () => {
|
15
|
+
const data = { name: 'John', age: 30 };
|
16
|
+
const result = pathMatch(['name'], data);
|
17
|
+
expect(result).toEqual(['John']);
|
18
|
+
});
|
19
|
+
|
20
|
+
it('should navigate nested objects', () => {
|
21
|
+
const data = { person: { name: 'John', age: 30 } };
|
22
|
+
const result = pathMatch(['person', 'name'], data);
|
23
|
+
expect(result).toEqual(['John']);
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should throw error when accessing string key on non-object', () => {
|
27
|
+
const data = ['name', 'age'];
|
28
|
+
expect(() => pathMatch(['0'], data)).toThrow(
|
29
|
+
'Path component requires object but found non-object element',
|
30
|
+
);
|
31
|
+
});
|
32
|
+
|
33
|
+
it('should throw error when accessing string key on null', () => {
|
34
|
+
const data = { name: null };
|
35
|
+
expect(() => pathMatch(['name', 'prop'], data)).toThrow(
|
36
|
+
'Path component requires object but found non-object element',
|
37
|
+
);
|
38
|
+
});
|
39
|
+
|
40
|
+
it('should throw error when property does not exist and results in empty selection', () => {
|
41
|
+
const data = { name: 'John' };
|
42
|
+
expect(() => pathMatch(['age'], data)).toThrow(
|
43
|
+
'No elements selected after processing path component',
|
44
|
+
);
|
45
|
+
});
|
46
|
+
});
|
47
|
+
|
48
|
+
describe('Null components - all array elements selection', () => {
|
49
|
+
it('should select all elements from an array', () => {
|
50
|
+
const data = { items: [1, 2, 3] };
|
51
|
+
const result = pathMatch(['items', null], data);
|
52
|
+
expect(result).toEqual([1, 2, 3]);
|
53
|
+
});
|
54
|
+
|
55
|
+
it('should throw error when using null on non-array', () => {
|
56
|
+
const data = { items: 'not-an-array' };
|
57
|
+
expect(() => pathMatch(['items', null], data)).toThrow(
|
58
|
+
'Null path component requires array but found non-array element',
|
59
|
+
);
|
60
|
+
});
|
61
|
+
|
62
|
+
it('should process all array elements for the next segment', () => {
|
63
|
+
const data = {
|
64
|
+
people: [
|
65
|
+
{ name: 'John', roles: ['admin', 'user'] },
|
66
|
+
{ name: 'Jane', roles: ['editor'] },
|
67
|
+
],
|
68
|
+
};
|
69
|
+
// Select all people, then all roles for each person
|
70
|
+
const result = pathMatch(['people', null, 'roles', null], data);
|
71
|
+
expect(result).toEqual(['admin', 'user', 'editor']);
|
72
|
+
});
|
73
|
+
|
74
|
+
it('should throw error on empty array', () => {
|
75
|
+
const data = { items: [] };
|
76
|
+
expect(() => pathMatch(['items', null], data)).toThrow(
|
77
|
+
'No elements selected after processing path component',
|
78
|
+
);
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('Number components - array index selection', () => {
|
83
|
+
// Note: This isn't in the type definition but the implementation handles it
|
84
|
+
it('should select element by index from an array', () => {
|
85
|
+
const data = { items: [1, 2, 3] };
|
86
|
+
// Using any to bypass TypeScript's type checking
|
87
|
+
const path: any = ['items', 1];
|
88
|
+
const result = pathMatch(path, data);
|
89
|
+
expect(result).toEqual([2]); // 0-based index, so index 1 is the second element (2)
|
90
|
+
});
|
91
|
+
|
92
|
+
it('should throw error when using number on non-array', () => {
|
93
|
+
const data = { items: 'not-an-array' };
|
94
|
+
// Using any to bypass TypeScript's type checking
|
95
|
+
const path: any = ['items', 1];
|
96
|
+
expect(() => pathMatch(path, data)).toThrow(
|
97
|
+
'Numeric path component requires array but found non-array element',
|
98
|
+
);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('should throw error when index is out of bounds', () => {
|
102
|
+
const data = { items: [1, 2, 3] };
|
103
|
+
// Using any to bypass TypeScript's type checking
|
104
|
+
const path: any = ['items', 10]; // Index out of bounds
|
105
|
+
expect(() => pathMatch(path, data)).toThrow(
|
106
|
+
'No elements selected after processing path component',
|
107
|
+
);
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
describe('Complex paths and combinations', () => {
|
112
|
+
it('should handle complex nested structures', () => {
|
113
|
+
const data = {
|
114
|
+
organization: {
|
115
|
+
departments: [
|
116
|
+
{
|
117
|
+
name: 'Engineering',
|
118
|
+
teams: [
|
119
|
+
{ name: 'Frontend', members: ['Alice', 'Bob'] },
|
120
|
+
{ name: 'Backend', members: ['Charlie', 'Dave'] },
|
121
|
+
],
|
122
|
+
},
|
123
|
+
{
|
124
|
+
name: 'Marketing',
|
125
|
+
teams: [{ name: 'Growth', members: ['Eve', 'Frank'] }],
|
126
|
+
},
|
127
|
+
],
|
128
|
+
},
|
129
|
+
};
|
130
|
+
|
131
|
+
// Get all team names
|
132
|
+
const teamNames = pathMatch(
|
133
|
+
['organization', 'departments', null, 'teams', null, 'name'],
|
134
|
+
data,
|
135
|
+
);
|
136
|
+
expect(teamNames).toEqual(['Frontend', 'Backend', 'Growth']);
|
137
|
+
|
138
|
+
// Get all members from all teams
|
139
|
+
const allMembers = pathMatch(
|
140
|
+
['organization', 'departments', null, 'teams', null, 'members', null],
|
141
|
+
data,
|
142
|
+
);
|
143
|
+
expect(allMembers).toEqual([
|
144
|
+
'Alice',
|
145
|
+
'Bob',
|
146
|
+
'Charlie',
|
147
|
+
'Dave',
|
148
|
+
'Eve',
|
149
|
+
'Frank',
|
150
|
+
]);
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should throw error for invalid path component', () => {
|
154
|
+
const data = { name: 'John' };
|
155
|
+
// Using any to bypass TypeScript's type checking
|
156
|
+
const path: any = ['name', true];
|
157
|
+
expect(() => pathMatch(path, data)).toThrow(
|
158
|
+
'Invalid path component: true',
|
159
|
+
);
|
160
|
+
});
|
161
|
+
});
|
162
|
+
});
|
163
|
+
});
|
@@ -0,0 +1,259 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
import { SdJwtVcCredential } from '../credentials/sdjwtvc.credential';
|
3
|
+
import { Claims, ClaimSet, TrustedAuthority } from '../type';
|
4
|
+
|
5
|
+
describe('Credentials', () => {
|
6
|
+
describe('SdJwtVcCredential', () => {
|
7
|
+
it('should create an instance with required properties', () => {
|
8
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
9
|
+
|
10
|
+
expect(credential).toBeDefined();
|
11
|
+
expect(credential.id).toBe('test-id');
|
12
|
+
expect(credential.vct_values).toEqual(['test-vct-value']);
|
13
|
+
});
|
14
|
+
|
15
|
+
describe('setMultiple', () => {
|
16
|
+
it('should set multiple flag', () => {
|
17
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
18
|
+
|
19
|
+
const result = credential.setMultiple(true);
|
20
|
+
|
21
|
+
// Should return this for chaining
|
22
|
+
expect(result).toBe(credential);
|
23
|
+
|
24
|
+
// Verify it was set by checking serialized output
|
25
|
+
const serialized = credential.serialize();
|
26
|
+
expect(serialized.multiple).toBe(true);
|
27
|
+
});
|
28
|
+
});
|
29
|
+
|
30
|
+
describe('setTrustedAuthorities', () => {
|
31
|
+
it('should set trusted authorities', () => {
|
32
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
33
|
+
const authorities: TrustedAuthority[] = [
|
34
|
+
{ type: 'aki', value: ['test-value'] },
|
35
|
+
];
|
36
|
+
|
37
|
+
const result = credential.setTrustedAuthorities(authorities);
|
38
|
+
|
39
|
+
// Should return this for chaining
|
40
|
+
expect(result).toBe(credential);
|
41
|
+
|
42
|
+
// Verify it was set by checking serialized output
|
43
|
+
const serialized = credential.serialize();
|
44
|
+
expect(serialized.trusted_authorities).toEqual(authorities);
|
45
|
+
});
|
46
|
+
});
|
47
|
+
|
48
|
+
describe('setRequireCryptographicHolderBinding', () => {
|
49
|
+
it('should set cryptographic holder binding requirement', () => {
|
50
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
51
|
+
|
52
|
+
const result = credential.setRequireCryptographicHolderBinding(false);
|
53
|
+
|
54
|
+
// Should return this for chaining
|
55
|
+
expect(result).toBe(credential);
|
56
|
+
|
57
|
+
// Verify it was set by checking serialized output
|
58
|
+
const serialized = credential.serialize();
|
59
|
+
expect(serialized.require_cryptographic_holder_binding).toBe(false);
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('setClaims', () => {
|
64
|
+
it('should set claims', () => {
|
65
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
66
|
+
const claims: Claims[] = [
|
67
|
+
{ path: ['$.vc.credentialSubject.firstName'], value: ['John'] },
|
68
|
+
{ path: ['$.vc.credentialSubject.lastName'], value: ['Doe'] },
|
69
|
+
];
|
70
|
+
|
71
|
+
const result = credential.setClaims(claims);
|
72
|
+
|
73
|
+
// Should return this for chaining
|
74
|
+
expect(result).toBe(credential);
|
75
|
+
|
76
|
+
// Verify it was set by checking serialized output
|
77
|
+
const serialized = credential.serialize();
|
78
|
+
expect(serialized.claims).toEqual(claims);
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('setClaimSets', () => {
|
83
|
+
it('should set claim sets when claims are properly set', () => {
|
84
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
85
|
+
const claims: Claims[] = [
|
86
|
+
{
|
87
|
+
id: 'claim1',
|
88
|
+
path: ['$.vc.credentialSubject.firstName'],
|
89
|
+
value: ['John'],
|
90
|
+
},
|
91
|
+
{
|
92
|
+
id: 'claim2',
|
93
|
+
path: ['$.vc.credentialSubject.lastName'],
|
94
|
+
value: ['Doe'],
|
95
|
+
},
|
96
|
+
];
|
97
|
+
|
98
|
+
credential.setClaims(claims);
|
99
|
+
|
100
|
+
const claimSets: ClaimSet[] = [['claim1', 'claim2']];
|
101
|
+
|
102
|
+
const result = credential.setClaimSets(claimSets);
|
103
|
+
|
104
|
+
// Should return this for chaining
|
105
|
+
expect(result).toBe(credential);
|
106
|
+
|
107
|
+
// Verify it was set by checking serialized output
|
108
|
+
const serialized = credential.serialize();
|
109
|
+
expect(serialized.claim_sets).toEqual(claimSets);
|
110
|
+
});
|
111
|
+
|
112
|
+
it('should throw error if claims are not set', () => {
|
113
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
114
|
+
const claimSets: ClaimSet[] = [['claim1']];
|
115
|
+
|
116
|
+
expect(() => credential.setClaimSets(claimSets)).toThrow(
|
117
|
+
'Claims must be set before claim sets',
|
118
|
+
);
|
119
|
+
});
|
120
|
+
|
121
|
+
it('should throw error if claims do not have IDs', () => {
|
122
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
123
|
+
// Claims without IDs
|
124
|
+
const claims: Claims[] = [
|
125
|
+
{ path: ['$.vc.credentialSubject.firstName'], value: ['John'] },
|
126
|
+
];
|
127
|
+
|
128
|
+
credential.setClaims(claims);
|
129
|
+
|
130
|
+
const claimSets: ClaimSet[] = [['claim1']];
|
131
|
+
|
132
|
+
expect(() => credential.setClaimSets(claimSets)).toThrow(
|
133
|
+
'Claims must not have an id before claim sets',
|
134
|
+
);
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
describe('serialize', () => {
|
139
|
+
it('should serialize credential with all properties', () => {
|
140
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
141
|
+
const authorities: TrustedAuthority[] = [
|
142
|
+
{ type: 'aki', value: ['test-value'] },
|
143
|
+
];
|
144
|
+
const claims: Claims[] = [
|
145
|
+
{
|
146
|
+
id: 'claim1',
|
147
|
+
path: ['$.vc.credentialSubject.firstName'],
|
148
|
+
value: ['John'],
|
149
|
+
},
|
150
|
+
];
|
151
|
+
const claimSets: ClaimSet[] = [['claim1']];
|
152
|
+
|
153
|
+
credential
|
154
|
+
.setMultiple(true)
|
155
|
+
.setTrustedAuthorities(authorities)
|
156
|
+
.setRequireCryptographicHolderBinding(false)
|
157
|
+
.setClaims(claims)
|
158
|
+
.setClaimSets(claimSets);
|
159
|
+
|
160
|
+
const serialized = credential.serialize();
|
161
|
+
|
162
|
+
expect(serialized).toEqual({
|
163
|
+
id: 'test-id',
|
164
|
+
format: 'dc+sd-jwt',
|
165
|
+
meta: { vct_values: ['test-vct-value'] },
|
166
|
+
multiple: true,
|
167
|
+
trusted_authorities: authorities,
|
168
|
+
require_cryptographic_holder_binding: false,
|
169
|
+
claims: claims,
|
170
|
+
claim_sets: claimSets,
|
171
|
+
});
|
172
|
+
});
|
173
|
+
|
174
|
+
it('should serialize credential with minimal properties', () => {
|
175
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value']);
|
176
|
+
|
177
|
+
const serialized = credential.serialize();
|
178
|
+
|
179
|
+
expect(serialized).toEqual({
|
180
|
+
id: 'test-id',
|
181
|
+
format: 'dc+sd-jwt',
|
182
|
+
meta: { vct_values: ['test-vct-value'] },
|
183
|
+
multiple: undefined,
|
184
|
+
trusted_authorities: undefined,
|
185
|
+
require_cryptographic_holder_binding: undefined,
|
186
|
+
claims: undefined,
|
187
|
+
claim_sets: undefined,
|
188
|
+
});
|
189
|
+
});
|
190
|
+
});
|
191
|
+
|
192
|
+
describe('Match claim functionality', () => {
|
193
|
+
// Access the private matchClaim method for testing
|
194
|
+
|
195
|
+
it('should match a simple claim', () => {
|
196
|
+
const claim = { path: ['name'] };
|
197
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
198
|
+
claims: [claim],
|
199
|
+
});
|
200
|
+
const data = { vct_values: ['test-vct-value'], name: 'John' };
|
201
|
+
expect(credential.match(data)).toStrictEqual({
|
202
|
+
match: true,
|
203
|
+
matchedClaims: [{ path: ['name'] }],
|
204
|
+
});
|
205
|
+
});
|
206
|
+
|
207
|
+
it('should match a claim with value restriction', () => {
|
208
|
+
const claim = { path: ['age'], value: [30, 40] };
|
209
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
210
|
+
claims: [claim],
|
211
|
+
});
|
212
|
+
const data = { vct: 'test-vct-value', age: 30 };
|
213
|
+
expect(credential.match(data)).toStrictEqual({
|
214
|
+
match: true,
|
215
|
+
matchedClaims: [{ path: ['age'], value: [30, 40] }],
|
216
|
+
});
|
217
|
+
});
|
218
|
+
|
219
|
+
it("should not match when value doesn't match restriction", () => {
|
220
|
+
const claim = { path: ['age'], value: [40, 50] };
|
221
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
222
|
+
claims: [claim],
|
223
|
+
});
|
224
|
+
const data = { vct: 'test-vct-value', age: 30 };
|
225
|
+
expect(credential.match(data)).toStrictEqual({ match: false });
|
226
|
+
});
|
227
|
+
|
228
|
+
it('should handle errors in path processing gracefully', () => {
|
229
|
+
const claim = { path: ['items', null] };
|
230
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
231
|
+
claims: [claim],
|
232
|
+
});
|
233
|
+
const data = { vct: 'test-vct-value', items: 'not-an-array' };
|
234
|
+
expect(credential.match(data)).toStrictEqual({ match: false });
|
235
|
+
});
|
236
|
+
|
237
|
+
it('should match array elements with null in path', () => {
|
238
|
+
const claim = { path: ['items', null], value: [3] };
|
239
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
240
|
+
claims: [claim],
|
241
|
+
});
|
242
|
+
const data = { vct: 'test-vct-value', items: [1, 2, 3, 4] };
|
243
|
+
expect(credential.match(data)).toStrictEqual({
|
244
|
+
match: true,
|
245
|
+
matchedClaims: [{ path: ['items', null], value: [3] }],
|
246
|
+
});
|
247
|
+
});
|
248
|
+
|
249
|
+
it("should return false when claim path doesn't exist", () => {
|
250
|
+
const claim = { path: ['nonexistent'] };
|
251
|
+
const credential = new SdJwtVcCredential('test-id', ['test-vct-value'], {
|
252
|
+
claims: [claim],
|
253
|
+
});
|
254
|
+
const data = { vct: 'test-vct-value', name: 'John' };
|
255
|
+
expect(credential.match(data)).toStrictEqual({ match: false });
|
256
|
+
});
|
257
|
+
});
|
258
|
+
});
|
259
|
+
});
|
package/src/type.ts
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
export type Format = 'dc+sd-jwt' | 'ldp_vc' | 'jwt_vc_json';
|
2
|
+
|
3
|
+
export type TrustedAuthority = {
|
4
|
+
type: 'aki' | 'etsi_tl' | 'openid_federation';
|
5
|
+
value: string[];
|
6
|
+
};
|
7
|
+
|
8
|
+
export type Credential = {
|
9
|
+
id: string;
|
10
|
+
format: Format;
|
11
|
+
|
12
|
+
/** optional, default is false */
|
13
|
+
multiple?: boolean;
|
14
|
+
meta?: { vct_values: string[] } | { doctype_value: string };
|
15
|
+
trusted_authorities?: TrustedAuthority[];
|
16
|
+
|
17
|
+
/** optional, default is true */
|
18
|
+
require_cryptographic_holder_binding?: boolean;
|
19
|
+
|
20
|
+
claims?: Claims[];
|
21
|
+
claim_sets?: ClaimSet[];
|
22
|
+
};
|
23
|
+
|
24
|
+
export type CredentialIds = Array<string>;
|
25
|
+
|
26
|
+
export type CredentialSet = {
|
27
|
+
options: CredentialIds[];
|
28
|
+
purpose?: string;
|
29
|
+
|
30
|
+
/** optional, default is true */
|
31
|
+
required?: boolean;
|
32
|
+
};
|
33
|
+
|
34
|
+
export type rawDCQL = {
|
35
|
+
credentials: Credential[];
|
36
|
+
credential_sets?: CredentialSet[];
|
37
|
+
};
|
38
|
+
|
39
|
+
export type Claims = {
|
40
|
+
/** optional, but if claim_sets is present, it is required */
|
41
|
+
id?: string;
|
42
|
+
|
43
|
+
path: Array<string | number | null>;
|
44
|
+
|
45
|
+
value?: Array<number | string | boolean>;
|
46
|
+
};
|
47
|
+
|
48
|
+
export type ClaimSet = string[];
|
49
|
+
|
50
|
+
export type SdJwtVcCredentialQuery = Credential & {
|
51
|
+
format: 'dc+sd-jwt';
|
52
|
+
meta: {
|
53
|
+
vct_values: string[];
|
54
|
+
};
|
55
|
+
};
|
56
|
+
|
57
|
+
export type MatchResult =
|
58
|
+
| {
|
59
|
+
match: false;
|
60
|
+
matchedClaims?: undefined;
|
61
|
+
}
|
62
|
+
| {
|
63
|
+
match: true;
|
64
|
+
matchedClaims: Claims[];
|
65
|
+
};
|
package/tsconfig.json
ADDED