@sd-jwt/jwt-status-list 0.6.2-next.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import {
2
+ createHeaderAndPayload,
3
+ getListFromStatusListJWT,
4
+ getStatusListFromJWT,
5
+ } from '../status-list-jwt';
6
+ import type {
7
+ StatusListJWTHeaderParameters,
8
+ JWTwithStatusListPayload,
9
+ } from '../types';
10
+ import { StatusList } from '../status-list';
11
+ import { jwtVerify, type KeyLike, SignJWT } from 'jose';
12
+ import { beforeAll, describe, expect, it } from 'vitest';
13
+ import { generateKeyPairSync } from 'node:crypto';
14
+ import type { JwtPayload } from '@sd-jwt/types';
15
+
16
+ describe('JWTStatusList', () => {
17
+ let publicKey: KeyLike;
18
+ let privateKey: KeyLike;
19
+
20
+ const header: StatusListJWTHeaderParameters = {
21
+ alg: 'ES256',
22
+ typ: 'statuslist+jwt',
23
+ };
24
+
25
+ beforeAll(() => {
26
+ // Generate a key pair for testing
27
+ const keyPair = generateKeyPairSync('ec', {
28
+ namedCurve: 'P-256',
29
+ });
30
+ publicKey = keyPair.publicKey;
31
+ privateKey = keyPair.privateKey;
32
+ });
33
+
34
+ it('should create a JWT with a status list', async () => {
35
+ const statusList = new StatusList([1, 0, 1, 1, 1], 1);
36
+ const iss = 'https://example.com';
37
+ const payload: JwtPayload = {
38
+ iss,
39
+ sub: `${iss}/statuslist/1`,
40
+ iat: new Date().getTime() / 1000,
41
+ };
42
+
43
+ const values = createHeaderAndPayload(statusList, payload, header);
44
+
45
+ const jwt = await new SignJWT(values.payload)
46
+ .setProtectedHeader(values.header)
47
+ .sign(privateKey);
48
+ // Verify the signed JWT with the public key
49
+ const verified = await jwtVerify(jwt, publicKey);
50
+ expect(verified.payload.status_list).toEqual({
51
+ bits: statusList.getBitsPerStatus(),
52
+ lst: statusList.compressStatusList(),
53
+ });
54
+ expect(verified.protectedHeader.typ).toBe('statuslist+jwt');
55
+ });
56
+
57
+ it('should get the status list from a JWT without verifying the signature', async () => {
58
+ const list = [1, 0, 1, 0, 1];
59
+ const statusList = new StatusList(list, 1);
60
+ const iss = 'https://example.com';
61
+ const payload: JwtPayload = {
62
+ iss,
63
+ sub: `${iss}/statuslist/1`,
64
+ iat: new Date().getTime() / 1000,
65
+ };
66
+
67
+ const values = createHeaderAndPayload(statusList, payload, header);
68
+
69
+ const jwt = await new SignJWT(values.payload)
70
+ .setProtectedHeader(values.header)
71
+ .sign(privateKey);
72
+
73
+ const extractedList = getListFromStatusListJWT(jwt);
74
+ for (let i = 0; i < list.length; i++) {
75
+ expect(extractedList.getStatus(i)).toBe(list[i]);
76
+ }
77
+ });
78
+
79
+ it('should throw an error if the JWT is invalid', async () => {
80
+ const list = [1, 0, 1, 0, 1];
81
+ const statusList = new StatusList(list, 2);
82
+ const iss = 'https://example.com';
83
+ let payload: JwtPayload = {
84
+ sub: `${iss}/statuslist/1`,
85
+ iat: new Date().getTime() / 1000,
86
+ };
87
+ expect(() => {
88
+ createHeaderAndPayload(statusList, payload as JwtPayload, header);
89
+ }).toThrow('iss field is required');
90
+
91
+ payload = {
92
+ iss,
93
+ iat: new Date().getTime() / 1000,
94
+ };
95
+ expect(() => createHeaderAndPayload(statusList, payload, header)).toThrow(
96
+ 'sub field is required',
97
+ );
98
+
99
+ payload = {
100
+ iss,
101
+ sub: `${iss}/statuslist/1`,
102
+ };
103
+ expect(() => createHeaderAndPayload(statusList, payload, header)).toThrow(
104
+ 'iat field is required',
105
+ );
106
+ });
107
+
108
+ it('should get the status entry from a JWT', async () => {
109
+ const payload: JWTwithStatusListPayload = {
110
+ iss: 'https://example.com',
111
+ sub: 'https://example.com/status/1',
112
+ iat: new Date().getTime() / 1000,
113
+ status: {
114
+ status_list: {
115
+ idx: 0,
116
+ uri: 'https://example.com/status/1',
117
+ },
118
+ },
119
+ };
120
+ const jwt = await new SignJWT(payload)
121
+ .setProtectedHeader({ alg: 'ES256' })
122
+ .sign(privateKey);
123
+ const reference = getStatusListFromJWT(jwt);
124
+ expect(reference).toEqual(payload.status.status_list);
125
+ });
126
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it, test } from 'vitest';
2
+ import { StatusList } from '../index';
3
+ import type { BitsPerStatus } from '../types';
4
+
5
+ describe('StatusList', () => {
6
+ const listLength = 10000;
7
+
8
+ it('test from the example with 1 bit status', () => {
9
+ const status: number[] = [];
10
+ status[0] = 1;
11
+ status[1] = 0;
12
+ status[2] = 0;
13
+ status[3] = 1;
14
+ status[4] = 1;
15
+ status[5] = 1;
16
+ status[6] = 0;
17
+ status[7] = 1;
18
+ status[8] = 1;
19
+ status[9] = 1;
20
+ status[10] = 0;
21
+ status[11] = 0;
22
+ status[12] = 0;
23
+ status[13] = 1;
24
+ status[14] = 0;
25
+ status[15] = 1;
26
+ const manager = new StatusList(status, 1);
27
+ const encoded = manager.compressStatusList();
28
+ expect(encoded).toBe('eNrbuRgAAhcBXQ');
29
+ const l = StatusList.decompressStatusList(encoded, 1);
30
+ for (let i = 0; i < status.length; i++) {
31
+ expect(l.getStatus(i)).toBe(status[i]);
32
+ }
33
+ });
34
+
35
+ it('test from the example with 2 bit status', () => {
36
+ const status: number[] = [];
37
+ status[0] = 1;
38
+ status[1] = 2;
39
+ status[2] = 0;
40
+ status[3] = 3;
41
+ status[4] = 0;
42
+ status[5] = 1;
43
+ status[6] = 0;
44
+ status[7] = 1;
45
+ status[8] = 1;
46
+ status[9] = 2;
47
+ status[10] = 3;
48
+ status[11] = 3;
49
+ const manager = new StatusList(status, 2);
50
+ const encoded = manager.compressStatusList();
51
+ expect(encoded).toBe('eNo76fITAAPfAgc');
52
+ const l = StatusList.decompressStatusList(encoded, 2);
53
+ for (let i = 0; i < status.length; i++) {
54
+ expect(l.getStatus(i)).toBe(status[i]);
55
+ }
56
+ });
57
+
58
+ // Test with different bitsPerStatus values
59
+ describe.each([
60
+ [1 as BitsPerStatus],
61
+ [2 as BitsPerStatus],
62
+ [4 as BitsPerStatus],
63
+ [8 as BitsPerStatus],
64
+ ])('with %i bitsPerStatus', (bitsPerStatus) => {
65
+ let manager: StatusList;
66
+
67
+ function createListe(
68
+ length: number,
69
+ bitsPerStatus: BitsPerStatus,
70
+ ): number[] {
71
+ const list: number[] = [];
72
+ for (let i = 0; i < length; i++) {
73
+ list.push(Math.floor(Math.random() * 2 ** bitsPerStatus));
74
+ }
75
+ return list;
76
+ }
77
+
78
+ it('should pass an incorrect list with wrong entries', () => {
79
+ expect(() => {
80
+ new StatusList([2 ** bitsPerStatus + 1], bitsPerStatus);
81
+ }).toThrowError();
82
+ });
83
+
84
+ it('should compress and decompress status list correctly', () => {
85
+ const statusList = createListe(listLength, bitsPerStatus);
86
+ manager = new StatusList(statusList, bitsPerStatus);
87
+ const compressedStatusList = manager.compressStatusList();
88
+ const decodedStatuslist = StatusList.decompressStatusList(
89
+ compressedStatusList,
90
+ bitsPerStatus,
91
+ );
92
+ checkIfEqual(decodedStatuslist, statusList);
93
+ });
94
+
95
+ it('should return the bitsPerStatus value', () => {
96
+ const statusList = createListe(
97
+ listLength,
98
+ bitsPerStatus as BitsPerStatus,
99
+ );
100
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
101
+ expect(manager.getBitsPerStatus()).toBe(bitsPerStatus);
102
+ });
103
+
104
+ it('getStatus returns the correct status', () => {
105
+ const statusList = createListe(
106
+ listLength,
107
+ bitsPerStatus as BitsPerStatus,
108
+ );
109
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
110
+
111
+ for (let i = 0; i < statusList.length; i++) {
112
+ expect(manager.getStatus(i)).toBe(statusList[i]);
113
+ }
114
+ });
115
+
116
+ it('setStatus sets the correct status', () => {
117
+ const statusList = createListe(
118
+ listLength,
119
+ bitsPerStatus as BitsPerStatus,
120
+ );
121
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
122
+
123
+ const newValue = Math.floor(Math.random() * 2 ** bitsPerStatus);
124
+ manager.setStatus(0, newValue);
125
+ expect(manager.getStatus(0)).toBe(newValue);
126
+ });
127
+
128
+ it('getStatus throws an error for out of bounds index', () => {
129
+ const statusList = createListe(
130
+ listLength,
131
+ bitsPerStatus as BitsPerStatus,
132
+ );
133
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
134
+
135
+ expect(() => manager.getStatus(-1)).toThrow('Index out of bounds');
136
+ expect(() => manager.getStatus(listLength)).toThrow(
137
+ 'Index out of bounds',
138
+ );
139
+ });
140
+
141
+ it('setStatus throws an error for out of bounds index', () => {
142
+ const statusList = createListe(
143
+ listLength,
144
+ bitsPerStatus as BitsPerStatus,
145
+ );
146
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
147
+
148
+ expect(() => manager.setStatus(-1, 5)).toThrow('Index out of bounds');
149
+ expect(() => manager.setStatus(listLength, 6)).toThrow(
150
+ 'Index out of bounds',
151
+ );
152
+ });
153
+
154
+ it('decompressStatusList throws an error when decompression fails', () => {
155
+ const statusList = createListe(
156
+ listLength,
157
+ bitsPerStatus as BitsPerStatus,
158
+ );
159
+ manager = new StatusList(statusList, bitsPerStatus as BitsPerStatus);
160
+
161
+ const invalidCompressedData = 'invalid data';
162
+
163
+ expect(() =>
164
+ StatusList.decompressStatusList(invalidCompressedData, bitsPerStatus),
165
+ ).toThrowError();
166
+ });
167
+
168
+ test('encodeStatusList covers remaining bits in last byte', () => {
169
+ const bitsPerStatus = 1;
170
+ const totalStatuses = 10; // Not a multiple of 8
171
+ const statusList = Array(totalStatuses).fill(0);
172
+ const manager = new StatusList(statusList, bitsPerStatus);
173
+ const encoded = manager.compressStatusList();
174
+ const decoded = StatusList.decompressStatusList(encoded, bitsPerStatus);
175
+ //technially we need to validate all the status but we are just checking the length
176
+ checkIfEqual(decoded, statusList);
177
+ });
178
+
179
+ /**
180
+ * Check if the status list is equal to the given list.
181
+ * @param statuslist1
182
+ * @param rawStatusList
183
+ */
184
+ function checkIfEqual(statuslist1: StatusList, rawStatusList: number[]) {
185
+ for (let i = 0; i < rawStatusList.length; i++) {
186
+ expect(statuslist1.getStatus(i)).toBe(rawStatusList[i]);
187
+ }
188
+ }
189
+
190
+ describe('constructor', () => {
191
+ test.each<[number]>([
192
+ [3], // Invalid bitsPerStatus value
193
+ [5], // Invalid bitsPerStatus value
194
+ [6], // Invalid bitsPerStatus value
195
+ [7], // Invalid bitsPerStatus value
196
+ [9], // Invalid bitsPerStatus value
197
+ [10], // Invalid bitsPerStatus value
198
+ ])(
199
+ 'throws an error for invalid bitsPerStatus value (%i)',
200
+ (bitsPerStatus) => {
201
+ expect(() => {
202
+ new StatusList([], bitsPerStatus as BitsPerStatus);
203
+ }).toThrowError('bitsPerStatus must be 1, 2, 4, or 8');
204
+ },
205
+ );
206
+
207
+ test.each<[BitsPerStatus]>([
208
+ [1], // Valid bitsPerStatus value
209
+ [2], // Valid bitsPerStatus value
210
+ [4], // Valid bitsPerStatus value
211
+ [8], // Valid bitsPerStatus value
212
+ ])(
213
+ 'does not throw an error for valid bitsPerStatus value (%i)',
214
+ (bitsPerStatus) => {
215
+ expect(() => {
216
+ new StatusList([], bitsPerStatus);
217
+ }).not.toThrowError();
218
+ },
219
+ );
220
+ });
221
+ });
222
+ });
package/src/types.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { JwtPayload } from '@sd-jwt/types';
2
+
3
+ /**
4
+ * Reference to a status list entry.
5
+ */
6
+ export interface StatusListEntry {
7
+ idx: number;
8
+ uri: string;
9
+ }
10
+
11
+ /**
12
+ * Payload for a JWT
13
+ */
14
+ export interface JWTwithStatusListPayload extends JwtPayload {
15
+ status: {
16
+ status_list: StatusListEntry;
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Payload for a JWT with a status list.
22
+ */
23
+ export interface StatusListJWTPayload extends JwtPayload {
24
+ ttl?: number;
25
+ status_list: {
26
+ bits: BitsPerStatus;
27
+ lst: string;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * BitsPerStatus type.
33
+ */
34
+ export type BitsPerStatus = 1 | 2 | 4 | 8;
35
+
36
+ /**
37
+ * Header parameters for a JWT.
38
+ */
39
+ export type StatusListJWTHeaderParameters = {
40
+ alg: string;
41
+ typ: 'statuslist+jwt';
42
+ [key: string]: unknown;
43
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ // vite.config.ts
2
+ import { allEnvs } from '../../vitest.shared';
3
+
4
+ export default allEnvs;