@xivdyetools/auth 1.0.2 → 1.0.3

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/jwt.test.ts CHANGED
@@ -1,337 +1,337 @@
1
- /**
2
- * Tests for JWT Verification Utilities
3
- */
4
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
- import {
6
- decodeJWT,
7
- verifyJWT,
8
- verifyJWTSignatureOnly,
9
- isJWTExpired,
10
- getJWTTimeToExpiry,
11
- type JWTPayload,
12
- } from './jwt.js';
13
- import { base64UrlEncode, base64UrlEncodeBytes } from '@xivdyetools/crypto';
14
- import { createHmacKey } from './hmac.js';
15
-
16
- // Helper to create a valid JWT for testing
17
- async function createTestJWT(
18
- payload: JWTPayload,
19
- secret: string,
20
- algorithm = 'HS256'
21
- ): Promise<string> {
22
- const header = { alg: algorithm, typ: 'JWT' };
23
- const headerB64 = base64UrlEncode(JSON.stringify(header));
24
- const payloadB64 = base64UrlEncode(JSON.stringify(payload));
25
-
26
- const signatureInput = `${headerB64}.${payloadB64}`;
27
- const key = await createHmacKey(secret, 'sign');
28
- const signature = await crypto.subtle.sign(
29
- 'HMAC',
30
- key,
31
- new TextEncoder().encode(signatureInput)
32
- );
33
- const signatureB64 = base64UrlEncodeBytes(new Uint8Array(signature));
34
-
35
- return `${headerB64}.${payloadB64}.${signatureB64}`;
36
- }
37
-
38
- describe('jwt.ts', () => {
39
- const secret = 'test-jwt-secret-key-123';
40
-
41
- beforeEach(() => {
42
- vi.useFakeTimers();
43
- vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
44
- });
45
-
46
- afterEach(() => {
47
- vi.useRealTimers();
48
- });
49
-
50
- describe('decodeJWT', () => {
51
- it('should decode a valid JWT payload', async () => {
52
- const payload: JWTPayload = {
53
- sub: '123456789',
54
- iat: Math.floor(Date.now() / 1000),
55
- exp: Math.floor(Date.now() / 1000) + 3600,
56
- type: 'access',
57
- username: 'testuser',
58
- };
59
- const token = await createTestJWT(payload, secret);
60
-
61
- const decoded = decodeJWT(token);
62
-
63
- expect(decoded).not.toBeNull();
64
- expect(decoded?.sub).toBe('123456789');
65
- expect(decoded?.type).toBe('access');
66
- expect(decoded?.username).toBe('testuser');
67
- });
68
-
69
- it('should return null for invalid token format', () => {
70
- const decoded = decodeJWT('not.a.valid.token.format');
71
- expect(decoded).toBeNull();
72
- });
73
-
74
- it('should return null for malformed base64', () => {
75
- const decoded = decodeJWT('not-base64.also-not.valid');
76
- expect(decoded).toBeNull();
77
- });
78
-
79
- it('should return null for invalid JSON payload', () => {
80
- const headerB64 = base64UrlEncode('{"alg":"HS256","typ":"JWT"}');
81
- const payloadB64 = base64UrlEncodeBytes(
82
- new TextEncoder().encode('not-json')
83
- );
84
- const decoded = decodeJWT(`${headerB64}.${payloadB64}.signature`);
85
- expect(decoded).toBeNull();
86
- });
87
- });
88
-
89
- describe('verifyJWT', () => {
90
- it('should verify a valid token', async () => {
91
- const payload: JWTPayload = {
92
- sub: '123456789',
93
- iat: Math.floor(Date.now() / 1000),
94
- exp: Math.floor(Date.now() / 1000) + 3600,
95
- type: 'access',
96
- };
97
- const token = await createTestJWT(payload, secret);
98
-
99
- const verified = await verifyJWT(token, secret);
100
-
101
- expect(verified).not.toBeNull();
102
- expect(verified?.sub).toBe('123456789');
103
- });
104
-
105
- it('should return null for expired token', async () => {
106
- const payload: JWTPayload = {
107
- sub: '123456789',
108
- iat: Math.floor(Date.now() / 1000) - 7200,
109
- exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
110
- type: 'access',
111
- };
112
- const token = await createTestJWT(payload, secret);
113
-
114
- const verified = await verifyJWT(token, secret);
115
-
116
- expect(verified).toBeNull();
117
- });
118
-
119
- it('should return null for wrong secret', async () => {
120
- const payload: JWTPayload = {
121
- sub: '123456789',
122
- iat: Math.floor(Date.now() / 1000),
123
- exp: Math.floor(Date.now() / 1000) + 3600,
124
- type: 'access',
125
- };
126
- const token = await createTestJWT(payload, secret);
127
-
128
- const verified = await verifyJWT(token, 'wrong-secret');
129
-
130
- expect(verified).toBeNull();
131
- });
132
-
133
- it('should return null for tampered payload', async () => {
134
- const payload: JWTPayload = {
135
- sub: '123456789',
136
- iat: Math.floor(Date.now() / 1000),
137
- exp: Math.floor(Date.now() / 1000) + 3600,
138
- type: 'access',
139
- };
140
- const token = await createTestJWT(payload, secret);
141
-
142
- // Tamper with the payload
143
- const parts = token.split('.');
144
- const tamperedPayload = { ...payload, sub: 'tampered-id' };
145
- parts[1] = base64UrlEncode(JSON.stringify(tamperedPayload));
146
- const tamperedToken = parts.join('.');
147
-
148
- const verified = await verifyJWT(tamperedToken, secret);
149
-
150
- expect(verified).toBeNull();
151
- });
152
-
153
- it('should reject non-HS256 algorithm (security)', async () => {
154
- const payload: JWTPayload = {
155
- sub: '123456789',
156
- iat: Math.floor(Date.now() / 1000),
157
- exp: Math.floor(Date.now() / 1000) + 3600,
158
- type: 'access',
159
- };
160
- // Create token with different algorithm in header
161
- const token = await createTestJWT(payload, secret, 'none');
162
-
163
- const verified = await verifyJWT(token, secret);
164
-
165
- expect(verified).toBeNull();
166
- });
167
-
168
- it('should return null for malformed token', async () => {
169
- const verified = await verifyJWT('not-a-jwt', secret);
170
- expect(verified).toBeNull();
171
- });
172
-
173
- it('should handle token without exp claim', async () => {
174
- // Create a token without exp - should still work if signature is valid
175
- const header = { alg: 'HS256', typ: 'JWT' };
176
- const payload = {
177
- sub: '123456789',
178
- iat: Math.floor(Date.now() / 1000),
179
- type: 'access',
180
- };
181
- const headerB64 = base64UrlEncode(JSON.stringify(header));
182
- const payloadB64 = base64UrlEncode(JSON.stringify(payload));
183
- const signatureInput = `${headerB64}.${payloadB64}`;
184
- const key = await createHmacKey(secret, 'sign');
185
- const signature = await crypto.subtle.sign(
186
- 'HMAC',
187
- key,
188
- new TextEncoder().encode(signatureInput)
189
- );
190
- const signatureB64 = base64UrlEncodeBytes(new Uint8Array(signature));
191
- const token = `${headerB64}.${payloadB64}.${signatureB64}`;
192
-
193
- const verified = await verifyJWT(token, secret);
194
-
195
- // Should pass since no exp means no expiration check
196
- expect(verified).not.toBeNull();
197
- });
198
- });
199
-
200
- describe('verifyJWTSignatureOnly', () => {
201
- it('should verify signature even for expired token', async () => {
202
- const payload: JWTPayload = {
203
- sub: '123456789',
204
- iat: Math.floor(Date.now() / 1000) - 7200,
205
- exp: Math.floor(Date.now() / 1000) - 3600, // Expired
206
- type: 'refresh',
207
- };
208
- const token = await createTestJWT(payload, secret);
209
-
210
- const verified = await verifyJWTSignatureOnly(token, secret);
211
-
212
- expect(verified).not.toBeNull();
213
- expect(verified?.sub).toBe('123456789');
214
- });
215
-
216
- it('should return null for invalid signature', async () => {
217
- const payload: JWTPayload = {
218
- sub: '123456789',
219
- iat: Math.floor(Date.now() / 1000),
220
- exp: Math.floor(Date.now() / 1000) + 3600,
221
- type: 'refresh',
222
- };
223
- const token = await createTestJWT(payload, secret);
224
-
225
- const verified = await verifyJWTSignatureOnly(token, 'wrong-secret');
226
-
227
- expect(verified).toBeNull();
228
- });
229
-
230
- it('should reject non-HS256 algorithm (security)', async () => {
231
- const payload: JWTPayload = {
232
- sub: '123456789',
233
- iat: Math.floor(Date.now() / 1000),
234
- exp: Math.floor(Date.now() / 1000) + 3600,
235
- type: 'refresh',
236
- };
237
- const token = await createTestJWT(payload, secret, 'HS384');
238
-
239
- const verified = await verifyJWTSignatureOnly(token, secret);
240
-
241
- expect(verified).toBeNull();
242
- });
243
-
244
- it('should respect maxAgeMs parameter', async () => {
245
- const payload: JWTPayload = {
246
- sub: '123456789',
247
- iat: Math.floor(Date.now() / 1000) - 7200, // 2 hours ago
248
- exp: Math.floor(Date.now() / 1000) - 3600,
249
- type: 'refresh',
250
- };
251
- const token = await createTestJWT(payload, secret);
252
-
253
- // Should fail with 1 hour max age
254
- const verified = await verifyJWTSignatureOnly(token, secret, 3600 * 1000);
255
-
256
- expect(verified).toBeNull();
257
- });
258
-
259
- it('should accept token within maxAgeMs', async () => {
260
- const payload: JWTPayload = {
261
- sub: '123456789',
262
- iat: Math.floor(Date.now() / 1000) - 1800, // 30 minutes ago
263
- exp: Math.floor(Date.now() / 1000) - 900, // Expired 15 minutes ago
264
- type: 'refresh',
265
- };
266
- const token = await createTestJWT(payload, secret);
267
-
268
- // Should pass with 1 hour max age
269
- const verified = await verifyJWTSignatureOnly(token, secret, 3600 * 1000);
270
-
271
- expect(verified).not.toBeNull();
272
- });
273
- });
274
-
275
- describe('isJWTExpired', () => {
276
- it('should return false for valid non-expired token', async () => {
277
- const payload: JWTPayload = {
278
- sub: '123456789',
279
- iat: Math.floor(Date.now() / 1000),
280
- exp: Math.floor(Date.now() / 1000) + 3600,
281
- type: 'access',
282
- };
283
- const token = await createTestJWT(payload, secret);
284
-
285
- expect(isJWTExpired(token)).toBe(false);
286
- });
287
-
288
- it('should return true for expired token', async () => {
289
- const payload: JWTPayload = {
290
- sub: '123456789',
291
- iat: Math.floor(Date.now() / 1000) - 7200,
292
- exp: Math.floor(Date.now() / 1000) - 3600,
293
- type: 'access',
294
- };
295
- const token = await createTestJWT(payload, secret);
296
-
297
- expect(isJWTExpired(token)).toBe(true);
298
- });
299
-
300
- it('should return true for malformed token', () => {
301
- expect(isJWTExpired('not-a-jwt')).toBe(true);
302
- });
303
- });
304
-
305
- describe('getJWTTimeToExpiry', () => {
306
- it('should return correct time to expiry', async () => {
307
- const expiresIn = 3600; // 1 hour
308
- const payload: JWTPayload = {
309
- sub: '123456789',
310
- iat: Math.floor(Date.now() / 1000),
311
- exp: Math.floor(Date.now() / 1000) + expiresIn,
312
- type: 'access',
313
- };
314
- const token = await createTestJWT(payload, secret);
315
-
316
- const ttl = getJWTTimeToExpiry(token);
317
-
318
- expect(ttl).toBe(expiresIn);
319
- });
320
-
321
- it('should return 0 for expired token', async () => {
322
- const payload: JWTPayload = {
323
- sub: '123456789',
324
- iat: Math.floor(Date.now() / 1000) - 7200,
325
- exp: Math.floor(Date.now() / 1000) - 3600,
326
- type: 'access',
327
- };
328
- const token = await createTestJWT(payload, secret);
329
-
330
- expect(getJWTTimeToExpiry(token)).toBe(0);
331
- });
332
-
333
- it('should return 0 for malformed token', () => {
334
- expect(getJWTTimeToExpiry('not-a-jwt')).toBe(0);
335
- });
336
- });
337
- });
1
+ /**
2
+ * Tests for JWT Verification Utilities
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import {
6
+ decodeJWT,
7
+ verifyJWT,
8
+ verifyJWTSignatureOnly,
9
+ isJWTExpired,
10
+ getJWTTimeToExpiry,
11
+ type JWTPayload,
12
+ } from './jwt.js';
13
+ import { base64UrlEncode, base64UrlEncodeBytes } from '@xivdyetools/crypto';
14
+ import { createHmacKey } from './hmac.js';
15
+
16
+ // Helper to create a valid JWT for testing
17
+ async function createTestJWT(
18
+ payload: JWTPayload,
19
+ secret: string,
20
+ algorithm = 'HS256'
21
+ ): Promise<string> {
22
+ const header = { alg: algorithm, typ: 'JWT' };
23
+ const headerB64 = base64UrlEncode(JSON.stringify(header));
24
+ const payloadB64 = base64UrlEncode(JSON.stringify(payload));
25
+
26
+ const signatureInput = `${headerB64}.${payloadB64}`;
27
+ const key = await createHmacKey(secret, 'sign');
28
+ const signature = await crypto.subtle.sign(
29
+ 'HMAC',
30
+ key,
31
+ new TextEncoder().encode(signatureInput)
32
+ );
33
+ const signatureB64 = base64UrlEncodeBytes(new Uint8Array(signature));
34
+
35
+ return `${headerB64}.${payloadB64}.${signatureB64}`;
36
+ }
37
+
38
+ describe('jwt.ts', () => {
39
+ const secret = 'test-jwt-secret-key-123';
40
+
41
+ beforeEach(() => {
42
+ vi.useFakeTimers();
43
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
44
+ });
45
+
46
+ afterEach(() => {
47
+ vi.useRealTimers();
48
+ });
49
+
50
+ describe('decodeJWT', () => {
51
+ it('should decode a valid JWT payload', async () => {
52
+ const payload: JWTPayload = {
53
+ sub: '123456789',
54
+ iat: Math.floor(Date.now() / 1000),
55
+ exp: Math.floor(Date.now() / 1000) + 3600,
56
+ type: 'access',
57
+ username: 'testuser',
58
+ };
59
+ const token = await createTestJWT(payload, secret);
60
+
61
+ const decoded = decodeJWT(token);
62
+
63
+ expect(decoded).not.toBeNull();
64
+ expect(decoded?.sub).toBe('123456789');
65
+ expect(decoded?.type).toBe('access');
66
+ expect(decoded?.username).toBe('testuser');
67
+ });
68
+
69
+ it('should return null for invalid token format', () => {
70
+ const decoded = decodeJWT('not.a.valid.token.format');
71
+ expect(decoded).toBeNull();
72
+ });
73
+
74
+ it('should return null for malformed base64', () => {
75
+ const decoded = decodeJWT('not-base64.also-not.valid');
76
+ expect(decoded).toBeNull();
77
+ });
78
+
79
+ it('should return null for invalid JSON payload', () => {
80
+ const headerB64 = base64UrlEncode('{"alg":"HS256","typ":"JWT"}');
81
+ const payloadB64 = base64UrlEncodeBytes(
82
+ new TextEncoder().encode('not-json')
83
+ );
84
+ const decoded = decodeJWT(`${headerB64}.${payloadB64}.signature`);
85
+ expect(decoded).toBeNull();
86
+ });
87
+ });
88
+
89
+ describe('verifyJWT', () => {
90
+ it('should verify a valid token', async () => {
91
+ const payload: JWTPayload = {
92
+ sub: '123456789',
93
+ iat: Math.floor(Date.now() / 1000),
94
+ exp: Math.floor(Date.now() / 1000) + 3600,
95
+ type: 'access',
96
+ };
97
+ const token = await createTestJWT(payload, secret);
98
+
99
+ const verified = await verifyJWT(token, secret);
100
+
101
+ expect(verified).not.toBeNull();
102
+ expect(verified?.sub).toBe('123456789');
103
+ });
104
+
105
+ it('should return null for expired token', async () => {
106
+ const payload: JWTPayload = {
107
+ sub: '123456789',
108
+ iat: Math.floor(Date.now() / 1000) - 7200,
109
+ exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
110
+ type: 'access',
111
+ };
112
+ const token = await createTestJWT(payload, secret);
113
+
114
+ const verified = await verifyJWT(token, secret);
115
+
116
+ expect(verified).toBeNull();
117
+ });
118
+
119
+ it('should return null for wrong secret', async () => {
120
+ const payload: JWTPayload = {
121
+ sub: '123456789',
122
+ iat: Math.floor(Date.now() / 1000),
123
+ exp: Math.floor(Date.now() / 1000) + 3600,
124
+ type: 'access',
125
+ };
126
+ const token = await createTestJWT(payload, secret);
127
+
128
+ const verified = await verifyJWT(token, 'wrong-secret');
129
+
130
+ expect(verified).toBeNull();
131
+ });
132
+
133
+ it('should return null for tampered payload', async () => {
134
+ const payload: JWTPayload = {
135
+ sub: '123456789',
136
+ iat: Math.floor(Date.now() / 1000),
137
+ exp: Math.floor(Date.now() / 1000) + 3600,
138
+ type: 'access',
139
+ };
140
+ const token = await createTestJWT(payload, secret);
141
+
142
+ // Tamper with the payload
143
+ const parts = token.split('.');
144
+ const tamperedPayload = { ...payload, sub: 'tampered-id' };
145
+ parts[1] = base64UrlEncode(JSON.stringify(tamperedPayload));
146
+ const tamperedToken = parts.join('.');
147
+
148
+ const verified = await verifyJWT(tamperedToken, secret);
149
+
150
+ expect(verified).toBeNull();
151
+ });
152
+
153
+ it('should reject non-HS256 algorithm (security)', async () => {
154
+ const payload: JWTPayload = {
155
+ sub: '123456789',
156
+ iat: Math.floor(Date.now() / 1000),
157
+ exp: Math.floor(Date.now() / 1000) + 3600,
158
+ type: 'access',
159
+ };
160
+ // Create token with different algorithm in header
161
+ const token = await createTestJWT(payload, secret, 'none');
162
+
163
+ const verified = await verifyJWT(token, secret);
164
+
165
+ expect(verified).toBeNull();
166
+ });
167
+
168
+ it('should return null for malformed token', async () => {
169
+ const verified = await verifyJWT('not-a-jwt', secret);
170
+ expect(verified).toBeNull();
171
+ });
172
+
173
+ it('should handle token without exp claim', async () => {
174
+ // Create a token without exp - should still work if signature is valid
175
+ const header = { alg: 'HS256', typ: 'JWT' };
176
+ const payload = {
177
+ sub: '123456789',
178
+ iat: Math.floor(Date.now() / 1000),
179
+ type: 'access',
180
+ };
181
+ const headerB64 = base64UrlEncode(JSON.stringify(header));
182
+ const payloadB64 = base64UrlEncode(JSON.stringify(payload));
183
+ const signatureInput = `${headerB64}.${payloadB64}`;
184
+ const key = await createHmacKey(secret, 'sign');
185
+ const signature = await crypto.subtle.sign(
186
+ 'HMAC',
187
+ key,
188
+ new TextEncoder().encode(signatureInput)
189
+ );
190
+ const signatureB64 = base64UrlEncodeBytes(new Uint8Array(signature));
191
+ const token = `${headerB64}.${payloadB64}.${signatureB64}`;
192
+
193
+ const verified = await verifyJWT(token, secret);
194
+
195
+ // Should pass since no exp means no expiration check
196
+ expect(verified).not.toBeNull();
197
+ });
198
+ });
199
+
200
+ describe('verifyJWTSignatureOnly', () => {
201
+ it('should verify signature even for expired token', async () => {
202
+ const payload: JWTPayload = {
203
+ sub: '123456789',
204
+ iat: Math.floor(Date.now() / 1000) - 7200,
205
+ exp: Math.floor(Date.now() / 1000) - 3600, // Expired
206
+ type: 'refresh',
207
+ };
208
+ const token = await createTestJWT(payload, secret);
209
+
210
+ const verified = await verifyJWTSignatureOnly(token, secret);
211
+
212
+ expect(verified).not.toBeNull();
213
+ expect(verified?.sub).toBe('123456789');
214
+ });
215
+
216
+ it('should return null for invalid signature', async () => {
217
+ const payload: JWTPayload = {
218
+ sub: '123456789',
219
+ iat: Math.floor(Date.now() / 1000),
220
+ exp: Math.floor(Date.now() / 1000) + 3600,
221
+ type: 'refresh',
222
+ };
223
+ const token = await createTestJWT(payload, secret);
224
+
225
+ const verified = await verifyJWTSignatureOnly(token, 'wrong-secret');
226
+
227
+ expect(verified).toBeNull();
228
+ });
229
+
230
+ it('should reject non-HS256 algorithm (security)', async () => {
231
+ const payload: JWTPayload = {
232
+ sub: '123456789',
233
+ iat: Math.floor(Date.now() / 1000),
234
+ exp: Math.floor(Date.now() / 1000) + 3600,
235
+ type: 'refresh',
236
+ };
237
+ const token = await createTestJWT(payload, secret, 'HS384');
238
+
239
+ const verified = await verifyJWTSignatureOnly(token, secret);
240
+
241
+ expect(verified).toBeNull();
242
+ });
243
+
244
+ it('should respect maxAgeMs parameter', async () => {
245
+ const payload: JWTPayload = {
246
+ sub: '123456789',
247
+ iat: Math.floor(Date.now() / 1000) - 7200, // 2 hours ago
248
+ exp: Math.floor(Date.now() / 1000) - 3600,
249
+ type: 'refresh',
250
+ };
251
+ const token = await createTestJWT(payload, secret);
252
+
253
+ // Should fail with 1 hour max age
254
+ const verified = await verifyJWTSignatureOnly(token, secret, 3600 * 1000);
255
+
256
+ expect(verified).toBeNull();
257
+ });
258
+
259
+ it('should accept token within maxAgeMs', async () => {
260
+ const payload: JWTPayload = {
261
+ sub: '123456789',
262
+ iat: Math.floor(Date.now() / 1000) - 1800, // 30 minutes ago
263
+ exp: Math.floor(Date.now() / 1000) - 900, // Expired 15 minutes ago
264
+ type: 'refresh',
265
+ };
266
+ const token = await createTestJWT(payload, secret);
267
+
268
+ // Should pass with 1 hour max age
269
+ const verified = await verifyJWTSignatureOnly(token, secret, 3600 * 1000);
270
+
271
+ expect(verified).not.toBeNull();
272
+ });
273
+ });
274
+
275
+ describe('isJWTExpired', () => {
276
+ it('should return false for valid non-expired token', async () => {
277
+ const payload: JWTPayload = {
278
+ sub: '123456789',
279
+ iat: Math.floor(Date.now() / 1000),
280
+ exp: Math.floor(Date.now() / 1000) + 3600,
281
+ type: 'access',
282
+ };
283
+ const token = await createTestJWT(payload, secret);
284
+
285
+ expect(isJWTExpired(token)).toBe(false);
286
+ });
287
+
288
+ it('should return true for expired token', async () => {
289
+ const payload: JWTPayload = {
290
+ sub: '123456789',
291
+ iat: Math.floor(Date.now() / 1000) - 7200,
292
+ exp: Math.floor(Date.now() / 1000) - 3600,
293
+ type: 'access',
294
+ };
295
+ const token = await createTestJWT(payload, secret);
296
+
297
+ expect(isJWTExpired(token)).toBe(true);
298
+ });
299
+
300
+ it('should return true for malformed token', () => {
301
+ expect(isJWTExpired('not-a-jwt')).toBe(true);
302
+ });
303
+ });
304
+
305
+ describe('getJWTTimeToExpiry', () => {
306
+ it('should return correct time to expiry', async () => {
307
+ const expiresIn = 3600; // 1 hour
308
+ const payload: JWTPayload = {
309
+ sub: '123456789',
310
+ iat: Math.floor(Date.now() / 1000),
311
+ exp: Math.floor(Date.now() / 1000) + expiresIn,
312
+ type: 'access',
313
+ };
314
+ const token = await createTestJWT(payload, secret);
315
+
316
+ const ttl = getJWTTimeToExpiry(token);
317
+
318
+ expect(ttl).toBe(expiresIn);
319
+ });
320
+
321
+ it('should return 0 for expired token', async () => {
322
+ const payload: JWTPayload = {
323
+ sub: '123456789',
324
+ iat: Math.floor(Date.now() / 1000) - 7200,
325
+ exp: Math.floor(Date.now() / 1000) - 3600,
326
+ type: 'access',
327
+ };
328
+ const token = await createTestJWT(payload, secret);
329
+
330
+ expect(getJWTTimeToExpiry(token)).toBe(0);
331
+ });
332
+
333
+ it('should return 0 for malformed token', () => {
334
+ expect(getJWTTimeToExpiry('not-a-jwt')).toBe(0);
335
+ });
336
+ });
337
+ });