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/src/dcql.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { CredentialBase } from './credentials/credential';
2
+ import { SdJwtVcCredential } from './credentials/sdjwtvc.credential';
3
+ import { Claims, CredentialSet, rawDCQL } from './type';
4
+
5
+ /**
6
+ * This class represent DCQL query data structure
7
+ */
8
+ export class DCQL {
9
+ private _credentials: CredentialBase[] = [];
10
+ private _credential_sets?: CredentialSet[];
11
+
12
+ constructor({
13
+ credentials,
14
+ credential_sets,
15
+ }: {
16
+ credentials?: CredentialBase[];
17
+ credential_sets?: CredentialSet[];
18
+ }) {
19
+ this._credentials = credentials ?? [];
20
+ this._credential_sets = credential_sets;
21
+ }
22
+
23
+ addCredential(credential: CredentialBase) {
24
+ this._credentials.push(credential);
25
+ return this;
26
+ }
27
+
28
+ addCredentialSet(credential_set: CredentialSet) {
29
+ if (!this._credential_sets) {
30
+ this._credential_sets = [];
31
+ }
32
+ this._credential_sets.push(credential_set);
33
+ return this;
34
+ }
35
+
36
+ serialize(): rawDCQL {
37
+ return {
38
+ credentials: this._credentials.map((c) => c.serialize()),
39
+ credential_sets: this._credential_sets,
40
+ };
41
+ }
42
+
43
+ static parse(raw: rawDCQL): DCQL {
44
+ const credentials = raw.credentials.map((c) => {
45
+ if (c.format === 'dc+sd-jwt') {
46
+ return SdJwtVcCredential.parseSdJwtCredential(c);
47
+ }
48
+ throw new Error('Invalid credential format');
49
+ });
50
+
51
+ return new DCQL({
52
+ credentials,
53
+ credential_sets: raw.credential_sets,
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Match credentials against an array of data records according to section 6.4.2 rules.
59
+ *
60
+ * If credential_sets is not provided, all credentials in credentials are requested.
61
+ * Otherwise, the Verifier requests credentials satisfying:
62
+ * - All required credential sets (where required is true or omitted)
63
+ * - Optionally, any other credential sets
64
+ *
65
+ * @param dataRecords Array of data records to match against
66
+ * @returns Object containing match result and matched credentials with their claims
67
+ */
68
+ match(dataRecords: Record<string, unknown>[]): {
69
+ match: boolean;
70
+ matchedCredentials?: Array<{
71
+ credential: Record<string, unknown>;
72
+ matchedClaims: Claims[];
73
+ dataIndex: number;
74
+ }>;
75
+ } {
76
+ // No credentials to match
77
+ if (this._credentials.length === 0) {
78
+ return { match: false };
79
+ }
80
+
81
+ // Results array to collect all matches
82
+ const allMatches: Array<{
83
+ credential: Record<string, unknown>;
84
+ dcqlCredential: CredentialBase;
85
+ matchedClaims: Claims[];
86
+ dataIndex: number;
87
+ }> = [];
88
+
89
+ // Check each credential against each data record
90
+ this._credentials.forEach((credential) => {
91
+ // Try to match the credential against each data record
92
+ dataRecords.forEach((data, index) => {
93
+ const result = credential.match(data);
94
+
95
+ // If there's a match, add it to our results
96
+ if (result.match) {
97
+ allMatches.push({
98
+ credential: data,
99
+ dcqlCredential: credential,
100
+ matchedClaims: result.matchedClaims,
101
+ dataIndex: index,
102
+ });
103
+ }
104
+ });
105
+ });
106
+
107
+ // If no credentials matched any data, return no match
108
+ if (allMatches.length === 0) {
109
+ return { match: false };
110
+ }
111
+
112
+ // If credential_sets is not defined, return all matched credentials
113
+ if (!this._credential_sets || this._credential_sets.length === 0) {
114
+ return {
115
+ match: true,
116
+ matchedCredentials: allMatches,
117
+ };
118
+ }
119
+
120
+ // Handle credential sets if they exist
121
+ const matchedIds = new Set(
122
+ allMatches.map((match) => {
123
+ const serialized = match.dcqlCredential.serialize();
124
+ return serialized.id;
125
+ }),
126
+ );
127
+
128
+ // First, separate required credential sets
129
+ const requiredSets = this._credential_sets.filter(
130
+ (set) => set.required === undefined || set.required === true,
131
+ );
132
+
133
+ // Check if all required sets are satisfied
134
+ const satisfiedRequiredSets = requiredSets.every((set) =>
135
+ this.isCredentialSetSatisfied(set, matchedIds),
136
+ );
137
+
138
+ // If any required set is not satisfied, we can't match
139
+ if (!satisfiedRequiredSets) {
140
+ return { match: false };
141
+ }
142
+
143
+ // We've satisfied all required sets, return all matched credentials
144
+ return {
145
+ match: true,
146
+ matchedCredentials: allMatches.map((matches) => {
147
+ return {
148
+ credential: matches.credential,
149
+ matchedClaims: matches.matchedClaims,
150
+ dataIndex: matches.dataIndex,
151
+ };
152
+ }),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Check if a credential set is satisfied by the matched credential IDs
158
+ * A credential set is satisfied if at least one of its options is satisfied
159
+ */
160
+ private isCredentialSetSatisfied(
161
+ set: CredentialSet,
162
+ matchedIds: Set<string>,
163
+ ): boolean {
164
+ // A set is satisfied if at least one of its options is satisfied
165
+ return set.options.some((option) => {
166
+ // An option is satisfied if all credential IDs in the option are matched
167
+ return option.every((credentialId) => matchedIds.has(credentialId));
168
+ });
169
+ }
170
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './dcql';
2
+ export * from './credentials/credential';
3
+ export * from './credentials/sdjwtvc.credential';
4
+ export * from './type';
package/src/match.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Process a claims path pointer
3
+ * @param path The claims path pointer array
4
+ * @param data The credential data
5
+ * @returns Array of selected JSON elements
6
+ * @throws Error if processing should be aborted according to the specification
7
+ */
8
+ export const pathMatch = (
9
+ path: Array<string | number | null>,
10
+ data: any,
11
+ ): any[] => {
12
+ // Start with the root element
13
+ let selectedElements: any[] = [data];
14
+
15
+ // Process the path from left to right
16
+ for (const component of path) {
17
+ const nextSelectedElements: any[] = [];
18
+
19
+ // Process each currently selected element
20
+ for (const element of selectedElements) {
21
+ // String: select the element with the key in the currently selected elements
22
+ if (typeof component === 'string') {
23
+ if (
24
+ element === null ||
25
+ typeof element !== 'object' ||
26
+ Array.isArray(element)
27
+ ) {
28
+ // According to spec: "If any of the currently selected element(s) is not an object,
29
+ // abort processing and return an error."
30
+ throw new Error(
31
+ 'Path component requires object but found non-object element',
32
+ );
33
+ }
34
+
35
+ if (component in element) {
36
+ nextSelectedElements.push(element[component]);
37
+ }
38
+ }
39
+ // null: select all elements of the currently selected arrays
40
+ else if (component === null) {
41
+ if (element === null || !Array.isArray(element)) {
42
+ // According to spec: "If any of the currently selected element(s) is not an array,
43
+ // abort processing and return an error."
44
+ throw new Error(
45
+ 'Null path component requires array but found non-array element',
46
+ );
47
+ }
48
+
49
+ nextSelectedElements.push(...element);
50
+ }
51
+ // number: select the element at the index in the currently selected arrays
52
+ else if (
53
+ typeof component === 'number' &&
54
+ component >= 0 &&
55
+ Number.isInteger(component)
56
+ ) {
57
+ if (element === null || !Array.isArray(element)) {
58
+ // According to spec: "If any of the currently selected element(s) is not an array,
59
+ // abort processing and return an error."
60
+ throw new Error(
61
+ 'Numeric path component requires array but found non-array element',
62
+ );
63
+ }
64
+ // If the index does not exist in a selected array, remove that array from the selection
65
+ if (component < element.length) {
66
+ nextSelectedElements.push(element[component]);
67
+ }
68
+ }
69
+ // Invalid component type
70
+ else {
71
+ // According to spec: "If the component is anything else, abort processing and return an error."
72
+ throw new Error(`Invalid path component: ${component}`);
73
+ }
74
+ }
75
+
76
+ // If no elements were selected, abort processing
77
+ if (nextSelectedElements.length === 0) {
78
+ // According to spec: "If the set of elements currently selected is empty,
79
+ // abort processing and return an error."
80
+ throw new Error('No elements selected after processing path component');
81
+ }
82
+
83
+ // Update the selected elements for the next iteration
84
+ selectedElements = nextSelectedElements;
85
+ }
86
+
87
+ return selectedElements;
88
+ };
@@ -0,0 +1,295 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DCQL } from '../dcql';
3
+ import { SdJwtVcCredential } from '../credentials/sdjwtvc.credential';
4
+ import { Claims, CredentialSet, rawDCQL } from '../type';
5
+
6
+ describe('DCQL', () => {
7
+ it('should create an instance', () => {
8
+ const dcql = new DCQL({});
9
+ expect(dcql).toBeDefined();
10
+ });
11
+
12
+ describe('addCredential', () => {
13
+ it('should add a credential', () => {
14
+ const dcql = new DCQL({});
15
+ const credential = new SdJwtVcCredential('test-id', 'test-vct');
16
+
17
+ const result = dcql.addCredential(credential);
18
+
19
+ // Should return this for chaining
20
+ expect(result).toBe(dcql);
21
+
22
+ // Verify it was added by checking serialized output
23
+ const serialized = dcql.serialize();
24
+ expect(serialized.credentials).toHaveLength(1);
25
+ expect(serialized.credentials[0].id).toBe('test-id');
26
+ });
27
+
28
+ it('should add multiple credentials', () => {
29
+ const dcql = new DCQL({});
30
+ const credential1 = new SdJwtVcCredential('test-id-1', 'test-vct-1');
31
+ const credential2 = new SdJwtVcCredential('test-id-2', 'test-vct-2');
32
+
33
+ dcql.addCredential(credential1).addCredential(credential2);
34
+
35
+ const serialized = dcql.serialize();
36
+ expect(serialized.credentials).toHaveLength(2);
37
+ expect(serialized.credentials[0].id).toBe('test-id-1');
38
+ expect(serialized.credentials[1].id).toBe('test-id-2');
39
+ });
40
+ });
41
+
42
+ describe('addCredentialSet', () => {
43
+ it('should add a credential set', () => {
44
+ const dcql = new DCQL({});
45
+ const credentialSet: CredentialSet = {
46
+ options: [['test-id-1'], ['test-id-2']],
47
+ required: true,
48
+ };
49
+
50
+ const result = dcql.addCredentialSet(credentialSet);
51
+
52
+ // Should return this for chaining
53
+ expect(result).toBe(dcql);
54
+
55
+ // Verify it was added by checking serialized output
56
+ const serialized = dcql.serialize();
57
+ expect(serialized.credential_sets).toHaveLength(1);
58
+ expect(serialized.credential_sets?.[0].options).toHaveLength(2);
59
+ expect(serialized.credential_sets?.[0].required).toBe(true);
60
+ });
61
+
62
+ it('should initialize credential_sets array if undefined', () => {
63
+ const dcql = new DCQL({});
64
+ const credentialSet: CredentialSet = {
65
+ options: [['test-id-1']],
66
+ };
67
+
68
+ dcql.addCredentialSet(credentialSet);
69
+
70
+ const serialized = dcql.serialize();
71
+ expect(serialized.credential_sets).toBeDefined();
72
+ expect(serialized.credential_sets).toHaveLength(1);
73
+ });
74
+ });
75
+
76
+ describe('parse', () => {
77
+ it('should parse valid raw DCQL with sd-jwt-vc credential', () => {
78
+ const rawDcql: rawDCQL = {
79
+ credentials: [
80
+ {
81
+ id: 'test-id',
82
+ format: 'dc+sd-jwt',
83
+ meta: { vct_values: ['test-vct'] },
84
+ multiple: true,
85
+ trusted_authorities: [{ type: 'aki', value: ['test-authority'] }],
86
+ require_cryptographic_holder_binding: true,
87
+ claims: [{ path: ['$.vc.credentialSubject.firstName'] }],
88
+ },
89
+ ],
90
+ };
91
+
92
+ const dcql = DCQL.parse(rawDcql);
93
+ const serialized = dcql.serialize();
94
+
95
+ expect(serialized.credentials).toHaveLength(1);
96
+ expect(serialized.credentials[0]).toEqual(rawDcql.credentials[0]);
97
+ });
98
+
99
+ it('should parse DCQL with credential sets', () => {
100
+ const rawDcql: rawDCQL = {
101
+ credentials: [
102
+ {
103
+ id: '0',
104
+ format: 'dc+sd-jwt',
105
+ meta: {
106
+ vct_values: [
107
+ 'eu.europa.ec.eudi.pid.1',
108
+ 'urn:eu.europa.ec.eudi:pid:1',
109
+ ],
110
+ },
111
+ claims: [
112
+ {
113
+ path: ['family_name'],
114
+ id: 'family_name',
115
+ },
116
+ {
117
+ path: ['given_name'],
118
+ id: 'given_name',
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ credential_sets: [
124
+ {
125
+ options: [['0']],
126
+ purpose: 'PID (sd-jwt-vc) - first_name and given_name',
127
+ },
128
+ ],
129
+ };
130
+
131
+ const dcql = DCQL.parse(rawDcql);
132
+ const serialized = dcql.serialize();
133
+
134
+ expect(serialized.credentials).toHaveLength(1);
135
+ expect(serialized.credential_sets).toHaveLength(1);
136
+ expect(serialized.credential_sets![0].options).toEqual([['0']]);
137
+ });
138
+ });
139
+
140
+ describe('serialize', () => {
141
+ it('should serialize empty DCQL', () => {
142
+ const dcql = new DCQL({});
143
+ const serialized = dcql.serialize();
144
+
145
+ expect(serialized).toEqual({
146
+ credentials: [],
147
+ credential_sets: undefined,
148
+ });
149
+ });
150
+
151
+ it('should serialize DCQL with credentials and credential sets', () => {
152
+ const dcql = new DCQL({});
153
+ const credential = new SdJwtVcCredential('test-id', ['test-vct']);
154
+ const credentialSet: CredentialSet = {
155
+ options: [['test-id']],
156
+ required: false,
157
+ };
158
+
159
+ dcql.addCredential(credential).addCredentialSet(credentialSet);
160
+
161
+ const serialized = dcql.serialize();
162
+ expect(serialized).toEqual({
163
+ credentials: [credential.serialize()],
164
+ credential_sets: [credentialSet],
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('match', () => {
170
+ it('empty data', () => {
171
+ const rawDcql: rawDCQL = {
172
+ credentials: [
173
+ {
174
+ id: 'cred-1',
175
+ format: 'dc+sd-jwt',
176
+ meta: { vct_values: ['vct-1'] },
177
+ },
178
+ {
179
+ id: 'cred-2',
180
+ format: 'dc+sd-jwt',
181
+ meta: { vct_values: ['vct-2'] },
182
+ },
183
+ ],
184
+ credential_sets: [
185
+ {
186
+ options: [['cred-1'], ['cred-2']],
187
+ required: true,
188
+ },
189
+ ],
190
+ };
191
+ const dcql = DCQL.parse(rawDcql);
192
+ const result = dcql.match([]);
193
+ expect(result).toEqual({ match: false });
194
+ });
195
+
196
+ it('test 1', () => {
197
+ const rawDcql: rawDCQL = {
198
+ credentials: [
199
+ {
200
+ id: 'cred-1',
201
+ format: 'dc+sd-jwt',
202
+ meta: { vct_values: ['vct-1'] },
203
+ },
204
+ {
205
+ id: 'cred-2',
206
+ format: 'dc+sd-jwt',
207
+ meta: { vct_values: ['vct-2'] },
208
+ },
209
+ ],
210
+ credential_sets: [
211
+ {
212
+ options: [['cred-1'], ['cred-2']],
213
+ required: true,
214
+ },
215
+ ],
216
+ };
217
+ const dcql = DCQL.parse(rawDcql);
218
+ const result = dcql.match([{ vct: 'vct-1', name: 'name-1' }]);
219
+ expect(result).toEqual({
220
+ match: true,
221
+ matchedCredentials: [
222
+ {
223
+ credential: { vct: 'vct-1', name: 'name-1' },
224
+ matchedClaims: [],
225
+ dataIndex: 0,
226
+ },
227
+ ],
228
+ });
229
+ });
230
+
231
+ it('Should match with animo credential', () => {
232
+ const dataRecords = {
233
+ vct: 'eu.europa.ec.eudi.pid.1',
234
+ given_name: 'Erika',
235
+ family_name: 'Mustermann',
236
+ };
237
+
238
+ const rawDcql: rawDCQL = {
239
+ credentials: [
240
+ {
241
+ id: '0',
242
+ format: 'dc+sd-jwt',
243
+ meta: {
244
+ vct_values: [
245
+ 'eu.europa.ec.eudi.pid.1',
246
+ 'urn:eu.europa.ec.eudi:pid:1',
247
+ ],
248
+ },
249
+ claims: [
250
+ {
251
+ path: ['family_name'],
252
+ id: 'family_name',
253
+ },
254
+ {
255
+ path: ['given_name'],
256
+ id: 'given_name',
257
+ },
258
+ ],
259
+ },
260
+ ],
261
+ credential_sets: [
262
+ {
263
+ options: [['0']],
264
+ purpose: 'PID (sd-jwt-vc) - first_name and given_name',
265
+ },
266
+ ],
267
+ };
268
+ const dcql = DCQL.parse(rawDcql);
269
+ const result = dcql.match([dataRecords]);
270
+ expect(result).toEqual({
271
+ match: true,
272
+ matchedCredentials: [
273
+ {
274
+ credential: {
275
+ vct: 'eu.europa.ec.eudi.pid.1',
276
+ family_name: 'Mustermann',
277
+ given_name: 'Erika',
278
+ },
279
+ matchedClaims: [
280
+ {
281
+ id: 'family_name',
282
+ path: ['family_name'],
283
+ },
284
+ {
285
+ id: 'given_name',
286
+ path: ['given_name'],
287
+ },
288
+ ],
289
+ dataIndex: 0,
290
+ },
291
+ ],
292
+ });
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,7 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ describe('Test#1', () => {
4
+ test('Test#1', () => {
5
+ expect(1).toBe(1);
6
+ });
7
+ });