tlc-claude-code 1.3.0 → 1.4.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.
Files changed (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. package/server/lib/zero-retention.test.js +258 -0
@@ -0,0 +1,670 @@
1
+ /**
2
+ * SSO Session Manager Tests
3
+ * Enhanced session management for SSO with IdP integration
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
+ import {
8
+ createSsoSessionManager,
9
+ SESSION_DEFAULTS,
10
+ } from './sso-session.js';
11
+
12
+ describe('sso-session', () => {
13
+ let sessionManager;
14
+ let mockIdpManager;
15
+ let mockMfaStore;
16
+
17
+ beforeEach(() => {
18
+ vi.useFakeTimers();
19
+ vi.setSystemTime(new Date('2024-01-15T10:00:00.000Z'));
20
+
21
+ // Mock IdP manager
22
+ mockIdpManager = {
23
+ getProvider: vi.fn(),
24
+ handleCallback: vi.fn(),
25
+ oauthRegistry: {
26
+ getProvider: vi.fn(),
27
+ },
28
+ };
29
+
30
+ // Mock MFA store
31
+ mockMfaStore = {
32
+ getMfaStatus: vi.fn().mockResolvedValue({ enabled: false }),
33
+ verifyMfa: vi.fn().mockResolvedValue({ valid: true }),
34
+ };
35
+
36
+ sessionManager = createSsoSessionManager({
37
+ idpManager: mockIdpManager,
38
+ mfaStore: mockMfaStore,
39
+ });
40
+ });
41
+
42
+ afterEach(() => {
43
+ vi.useRealTimers();
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ describe('SESSION_DEFAULTS', () => {
48
+ it('defines default configuration values', () => {
49
+ expect(SESSION_DEFAULTS.sessionDuration).toBe(86400000); // 24 hours
50
+ expect(SESSION_DEFAULTS.maxConcurrentSessions).toBe(5);
51
+ expect(SESSION_DEFAULTS.tokenRefreshThreshold).toBe(300000); // 5 minutes
52
+ });
53
+ });
54
+
55
+ describe('createSession', () => {
56
+ it('stores user and IdP info', async () => {
57
+ const authResult = {
58
+ profile: {
59
+ id: 'user-123',
60
+ email: 'user@example.com',
61
+ name: 'Test User',
62
+ },
63
+ tokens: {
64
+ accessToken: 'access-token-123',
65
+ refreshToken: 'refresh-token-456',
66
+ expiresIn: 3600,
67
+ },
68
+ };
69
+
70
+ const session = await sessionManager.createSession('github', authResult, {
71
+ userAgent: 'Mozilla/5.0',
72
+ ipAddress: '192.168.1.1',
73
+ });
74
+
75
+ expect(session.id).toBeDefined();
76
+ expect(session.userId).toBe('user-123');
77
+ expect(session.provider).toBe('github');
78
+ expect(session.accessToken).toBe('access-token-123');
79
+ expect(session.refreshToken).toBe('refresh-token-456');
80
+ expect(session.userAgent).toBe('Mozilla/5.0');
81
+ expect(session.ipAddress).toBe('192.168.1.1');
82
+ });
83
+
84
+ it('sets expiry from config', async () => {
85
+ const customManager = createSsoSessionManager({
86
+ idpManager: mockIdpManager,
87
+ mfaStore: mockMfaStore,
88
+ sessionDuration: 3600000, // 1 hour
89
+ });
90
+
91
+ const authResult = {
92
+ profile: { id: 'user-123', email: 'user@example.com' },
93
+ tokens: { accessToken: 'token', expiresIn: 3600 },
94
+ };
95
+
96
+ const session = await customManager.createSession('github', authResult, {});
97
+
98
+ const expectedExpiry = Date.now() + 3600000;
99
+ expect(session.expiresAt).toBe(expectedExpiry);
100
+ });
101
+
102
+ it('calculates token expiry from expiresIn', async () => {
103
+ const authResult = {
104
+ profile: { id: 'user-123', email: 'user@example.com' },
105
+ tokens: { accessToken: 'token', expiresIn: 7200 }, // 2 hours in seconds
106
+ };
107
+
108
+ const session = await sessionManager.createSession('github', authResult, {});
109
+
110
+ const expectedTokenExpiry = Date.now() + (7200 * 1000);
111
+ expect(session.tokenExpiry).toBe(expectedTokenExpiry);
112
+ });
113
+
114
+ it('sets createdAt and lastActivityAt', async () => {
115
+ const authResult = {
116
+ profile: { id: 'user-123', email: 'user@example.com' },
117
+ tokens: { accessToken: 'token' },
118
+ };
119
+
120
+ const session = await sessionManager.createSession('github', authResult, {});
121
+
122
+ expect(session.createdAt).toBe(Date.now());
123
+ expect(session.lastActivityAt).toBe(Date.now());
124
+ });
125
+
126
+ it('sets mfaVerified from MFA status', async () => {
127
+ mockMfaStore.getMfaStatus.mockResolvedValue({ enabled: true });
128
+ mockMfaStore.verifyMfa.mockResolvedValue({ valid: true });
129
+
130
+ const authResult = {
131
+ profile: { id: 'user-123', email: 'user@example.com' },
132
+ tokens: { accessToken: 'token' },
133
+ };
134
+
135
+ const session = await sessionManager.createSession('github', authResult, {
136
+ mfaCode: '123456',
137
+ });
138
+
139
+ expect(session.mfaVerified).toBe(true);
140
+ });
141
+
142
+ it('sets mfaVerified false when no MFA enabled', async () => {
143
+ mockMfaStore.getMfaStatus.mockResolvedValue({ enabled: false });
144
+
145
+ const authResult = {
146
+ profile: { id: 'user-123', email: 'user@example.com' },
147
+ tokens: { accessToken: 'token' },
148
+ };
149
+
150
+ const session = await sessionManager.createSession('github', authResult, {});
151
+
152
+ expect(session.mfaVerified).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe('getSession', () => {
157
+ it('returns valid session', async () => {
158
+ const authResult = {
159
+ profile: { id: 'user-123', email: 'user@example.com' },
160
+ tokens: { accessToken: 'token', expiresIn: 3600 },
161
+ };
162
+
163
+ const created = await sessionManager.createSession('github', authResult, {});
164
+ const session = await sessionManager.getSession(created.id);
165
+
166
+ expect(session).not.toBeNull();
167
+ expect(session.id).toBe(created.id);
168
+ expect(session.userId).toBe('user-123');
169
+ });
170
+
171
+ it('returns null for expired session', async () => {
172
+ const authResult = {
173
+ profile: { id: 'user-123', email: 'user@example.com' },
174
+ tokens: { accessToken: 'token' },
175
+ };
176
+
177
+ const created = await sessionManager.createSession('github', authResult, {});
178
+
179
+ // Advance time past session expiry
180
+ vi.advanceTimersByTime(SESSION_DEFAULTS.sessionDuration + 1000);
181
+
182
+ const session = await sessionManager.getSession(created.id);
183
+
184
+ expect(session).toBeNull();
185
+ });
186
+
187
+ it('returns null for non-existent session', async () => {
188
+ const session = await sessionManager.getSession('non-existent-id');
189
+
190
+ expect(session).toBeNull();
191
+ });
192
+
193
+ it('updates lastActivityAt on access', async () => {
194
+ const authResult = {
195
+ profile: { id: 'user-123', email: 'user@example.com' },
196
+ tokens: { accessToken: 'token' },
197
+ };
198
+
199
+ const created = await sessionManager.createSession('github', authResult, {});
200
+ const originalLastActivity = created.lastActivityAt;
201
+
202
+ // Advance time
203
+ vi.advanceTimersByTime(60000); // 1 minute
204
+
205
+ const session = await sessionManager.getSession(created.id);
206
+
207
+ expect(session.lastActivityAt).toBeGreaterThan(originalLastActivity);
208
+ });
209
+ });
210
+
211
+ describe('refreshSession', () => {
212
+ it('extends session lifetime', async () => {
213
+ const authResult = {
214
+ profile: { id: 'user-123', email: 'user@example.com' },
215
+ tokens: { accessToken: 'token', expiresIn: 3600 },
216
+ };
217
+
218
+ const created = await sessionManager.createSession('github', authResult, {});
219
+ const originalExpiry = created.expiresAt;
220
+
221
+ // Advance time
222
+ vi.advanceTimersByTime(60000); // 1 minute
223
+
224
+ const refreshed = await sessionManager.refreshSession(created.id);
225
+
226
+ expect(refreshed.expiresAt).toBeGreaterThan(originalExpiry);
227
+ });
228
+
229
+ it('refreshes IdP tokens when near expiry', async () => {
230
+ // Set up provider with refresh capability
231
+ mockIdpManager.oauthRegistry.getProvider.mockReturnValue({
232
+ tokenUrl: 'https://github.com/login/oauth/access_token',
233
+ clientId: 'client-123',
234
+ clientSecret: 'secret-456',
235
+ });
236
+
237
+ // Mock fetch for token refresh
238
+ const mockFetch = vi.fn().mockResolvedValue({
239
+ ok: true,
240
+ json: () => Promise.resolve({
241
+ access_token: 'new-access-token',
242
+ refresh_token: 'new-refresh-token',
243
+ expires_in: 3600,
244
+ }),
245
+ });
246
+ global.fetch = mockFetch;
247
+
248
+ const authResult = {
249
+ profile: { id: 'user-123', email: 'user@example.com' },
250
+ tokens: {
251
+ accessToken: 'old-token',
252
+ refreshToken: 'refresh-token',
253
+ expiresIn: 600, // 10 minutes - will be near expiry after time advance
254
+ },
255
+ };
256
+
257
+ const created = await sessionManager.createSession('github', authResult, {});
258
+
259
+ // Advance time close to token expiry (past threshold)
260
+ vi.advanceTimersByTime(400000); // 6.67 minutes - within 5 minute threshold
261
+
262
+ const refreshed = await sessionManager.refreshSession(created.id);
263
+
264
+ expect(refreshed.accessToken).toBe('new-access-token');
265
+ expect(refreshed.refreshToken).toBe('new-refresh-token');
266
+ });
267
+
268
+ it('returns null for non-existent session', async () => {
269
+ const result = await sessionManager.refreshSession('non-existent-id');
270
+
271
+ expect(result).toBeNull();
272
+ });
273
+
274
+ it('updates lastActivityAt', async () => {
275
+ const authResult = {
276
+ profile: { id: 'user-123', email: 'user@example.com' },
277
+ tokens: { accessToken: 'token' },
278
+ };
279
+
280
+ const created = await sessionManager.createSession('github', authResult, {});
281
+ const originalLastActivity = created.lastActivityAt;
282
+
283
+ vi.advanceTimersByTime(60000);
284
+
285
+ const refreshed = await sessionManager.refreshSession(created.id);
286
+
287
+ expect(refreshed.lastActivityAt).toBeGreaterThan(originalLastActivity);
288
+ });
289
+ });
290
+
291
+ describe('destroySession', () => {
292
+ it('removes session', async () => {
293
+ const authResult = {
294
+ profile: { id: 'user-123', email: 'user@example.com' },
295
+ tokens: { accessToken: 'token' },
296
+ };
297
+
298
+ const created = await sessionManager.createSession('github', authResult, {});
299
+ await sessionManager.destroySession(created.id);
300
+
301
+ const session = await sessionManager.getSession(created.id);
302
+ expect(session).toBeNull();
303
+ });
304
+
305
+ it('triggers IdP logout for SAML provider', async () => {
306
+ const mockSamlLogout = vi.fn().mockReturnValue({
307
+ url: 'https://idp.example.com/logout',
308
+ });
309
+
310
+ mockIdpManager.samlProvider = {
311
+ getIdP: vi.fn().mockReturnValue({ sloUrl: 'https://idp.example.com/logout' }),
312
+ createLogoutRequest: mockSamlLogout,
313
+ };
314
+
315
+ const authResult = {
316
+ profile: { id: 'user-123', email: 'user@example.com', nameId: 'name-id-123' },
317
+ tokens: {},
318
+ providerType: 'saml',
319
+ };
320
+
321
+ const created = await sessionManager.createSession('okta', authResult, {});
322
+ const result = await sessionManager.destroySession(created.id, { triggerIdpLogout: true });
323
+
324
+ expect(result.logoutUrl).toBeDefined();
325
+ });
326
+
327
+ it('triggers token revocation for OAuth provider', async () => {
328
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true });
329
+ global.fetch = mockFetch;
330
+
331
+ mockIdpManager.oauthRegistry.getProvider.mockReturnValue({
332
+ revokeUrl: 'https://github.com/login/oauth/revoke',
333
+ clientId: 'client-123',
334
+ clientSecret: 'secret-456',
335
+ });
336
+
337
+ const authResult = {
338
+ profile: { id: 'user-123', email: 'user@example.com' },
339
+ tokens: { accessToken: 'token-to-revoke' },
340
+ };
341
+
342
+ const created = await sessionManager.createSession('github', authResult, {});
343
+ await sessionManager.destroySession(created.id, { triggerIdpLogout: true });
344
+
345
+ expect(mockFetch).toHaveBeenCalled();
346
+ });
347
+
348
+ it('returns success for non-existent session', async () => {
349
+ const result = await sessionManager.destroySession('non-existent-id');
350
+
351
+ expect(result.success).toBe(true);
352
+ });
353
+ });
354
+
355
+ describe('enforceSessionLimit', () => {
356
+ it('limits concurrent sessions', async () => {
357
+ const customManager = createSsoSessionManager({
358
+ idpManager: mockIdpManager,
359
+ mfaStore: mockMfaStore,
360
+ maxConcurrentSessions: 2,
361
+ });
362
+
363
+ const authResult = {
364
+ profile: { id: 'user-123', email: 'user@example.com' },
365
+ tokens: { accessToken: 'token' },
366
+ };
367
+
368
+ // Create 3 sessions for the same user
369
+ const session1 = await customManager.createSession('github', authResult, {});
370
+ vi.advanceTimersByTime(1000);
371
+ const session2 = await customManager.createSession('github', authResult, {});
372
+ vi.advanceTimersByTime(1000);
373
+ const session3 = await customManager.createSession('github', authResult, {});
374
+
375
+ // Session 1 should be removed (oldest)
376
+ const found1 = await customManager.getSession(session1.id);
377
+ const found2 = await customManager.getSession(session2.id);
378
+ const found3 = await customManager.getSession(session3.id);
379
+
380
+ expect(found1).toBeNull();
381
+ expect(found2).not.toBeNull();
382
+ expect(found3).not.toBeNull();
383
+ });
384
+
385
+ it('removes oldest session when limit exceeded', async () => {
386
+ const customManager = createSsoSessionManager({
387
+ idpManager: mockIdpManager,
388
+ mfaStore: mockMfaStore,
389
+ maxConcurrentSessions: 2,
390
+ });
391
+
392
+ const authResult = {
393
+ profile: { id: 'user-123', email: 'user@example.com' },
394
+ tokens: { accessToken: 'token' },
395
+ };
396
+
397
+ const session1 = await customManager.createSession('github', authResult, {});
398
+ vi.advanceTimersByTime(1000);
399
+ await customManager.createSession('github', authResult, {});
400
+ vi.advanceTimersByTime(1000);
401
+ await customManager.createSession('github', authResult, {});
402
+
403
+ // Verify oldest was removed
404
+ const remaining = await customManager.getActiveSessions('user-123');
405
+ const sessionIds = remaining.map(s => s.id);
406
+
407
+ expect(sessionIds).not.toContain(session1.id);
408
+ expect(remaining.length).toBe(2);
409
+ });
410
+
411
+ it('does not affect sessions from different users', async () => {
412
+ const customManager = createSsoSessionManager({
413
+ idpManager: mockIdpManager,
414
+ mfaStore: mockMfaStore,
415
+ maxConcurrentSessions: 2,
416
+ });
417
+
418
+ const authResult1 = {
419
+ profile: { id: 'user-1', email: 'user1@example.com' },
420
+ tokens: { accessToken: 'token1' },
421
+ };
422
+
423
+ const authResult2 = {
424
+ profile: { id: 'user-2', email: 'user2@example.com' },
425
+ tokens: { accessToken: 'token2' },
426
+ };
427
+
428
+ // Create 2 sessions for user 1
429
+ await customManager.createSession('github', authResult1, {});
430
+ await customManager.createSession('github', authResult1, {});
431
+
432
+ // Create 2 sessions for user 2
433
+ await customManager.createSession('github', authResult2, {});
434
+ await customManager.createSession('github', authResult2, {});
435
+
436
+ // Each user should have their full allowed sessions
437
+ const user1Sessions = await customManager.getActiveSessions('user-1');
438
+ const user2Sessions = await customManager.getActiveSessions('user-2');
439
+
440
+ expect(user1Sessions.length).toBe(2);
441
+ expect(user2Sessions.length).toBe(2);
442
+ });
443
+ });
444
+
445
+ describe('getActiveSessions', () => {
446
+ it('returns user\'s sessions', async () => {
447
+ const authResult = {
448
+ profile: { id: 'user-123', email: 'user@example.com' },
449
+ tokens: { accessToken: 'token' },
450
+ };
451
+
452
+ await sessionManager.createSession('github', authResult, {});
453
+ await sessionManager.createSession('google', authResult, {});
454
+
455
+ const sessions = await sessionManager.getActiveSessions('user-123');
456
+
457
+ expect(sessions.length).toBe(2);
458
+ expect(sessions.every(s => s.userId === 'user-123')).toBe(true);
459
+ });
460
+
461
+ it('returns empty array for user with no sessions', async () => {
462
+ const sessions = await sessionManager.getActiveSessions('no-sessions-user');
463
+
464
+ expect(sessions).toEqual([]);
465
+ });
466
+
467
+ it('excludes expired sessions', async () => {
468
+ const shortDurationManager = createSsoSessionManager({
469
+ idpManager: mockIdpManager,
470
+ mfaStore: mockMfaStore,
471
+ sessionDuration: 60000, // 1 minute
472
+ });
473
+
474
+ const authResult = {
475
+ profile: { id: 'user-123', email: 'user@example.com' },
476
+ tokens: { accessToken: 'token' },
477
+ };
478
+
479
+ await shortDurationManager.createSession('github', authResult, {});
480
+ vi.advanceTimersByTime(30000); // 30 seconds
481
+ await shortDurationManager.createSession('google', authResult, {});
482
+
483
+ // Advance past first session expiry
484
+ vi.advanceTimersByTime(40000); // 40 more seconds
485
+
486
+ const sessions = await shortDurationManager.getActiveSessions('user-123');
487
+
488
+ expect(sessions.length).toBe(1);
489
+ expect(sessions[0].provider).toBe('google');
490
+ });
491
+
492
+ it('returns sessions with sanitized data', async () => {
493
+ const authResult = {
494
+ profile: { id: 'user-123', email: 'user@example.com' },
495
+ tokens: { accessToken: 'secret-token', refreshToken: 'secret-refresh' },
496
+ };
497
+
498
+ await sessionManager.createSession('github', authResult, {});
499
+
500
+ const sessions = await sessionManager.getActiveSessions('user-123');
501
+
502
+ // Should include metadata but not expose tokens in list view
503
+ expect(sessions[0].id).toBeDefined();
504
+ expect(sessions[0].provider).toBeDefined();
505
+ expect(sessions[0].createdAt).toBeDefined();
506
+ });
507
+ });
508
+
509
+ describe('cleanupExpiredSessions', () => {
510
+ it('removes old sessions', async () => {
511
+ const shortDurationManager = createSsoSessionManager({
512
+ idpManager: mockIdpManager,
513
+ mfaStore: mockMfaStore,
514
+ sessionDuration: 60000, // 1 minute
515
+ });
516
+
517
+ const authResult = {
518
+ profile: { id: 'user-123', email: 'user@example.com' },
519
+ tokens: { accessToken: 'token' },
520
+ };
521
+
522
+ await shortDurationManager.createSession('github', authResult, {});
523
+ vi.advanceTimersByTime(30000);
524
+ await shortDurationManager.createSession('google', authResult, {});
525
+
526
+ // Advance past first session expiry
527
+ vi.advanceTimersByTime(40000);
528
+
529
+ const removed = await shortDurationManager.cleanupExpiredSessions();
530
+
531
+ expect(removed).toBe(1);
532
+ });
533
+
534
+ it('returns count of removed sessions', async () => {
535
+ const shortDurationManager = createSsoSessionManager({
536
+ idpManager: mockIdpManager,
537
+ mfaStore: mockMfaStore,
538
+ sessionDuration: 60000,
539
+ });
540
+
541
+ const authResult1 = {
542
+ profile: { id: 'user-1', email: 'user1@example.com' },
543
+ tokens: { accessToken: 'token1' },
544
+ };
545
+
546
+ const authResult2 = {
547
+ profile: { id: 'user-2', email: 'user2@example.com' },
548
+ tokens: { accessToken: 'token2' },
549
+ };
550
+
551
+ await shortDurationManager.createSession('github', authResult1, {});
552
+ await shortDurationManager.createSession('github', authResult2, {});
553
+
554
+ // Expire all sessions
555
+ vi.advanceTimersByTime(70000);
556
+
557
+ const removed = await shortDurationManager.cleanupExpiredSessions();
558
+
559
+ expect(removed).toBe(2);
560
+ });
561
+
562
+ it('does not remove active sessions', async () => {
563
+ const authResult = {
564
+ profile: { id: 'user-123', email: 'user@example.com' },
565
+ tokens: { accessToken: 'token' },
566
+ };
567
+
568
+ await sessionManager.createSession('github', authResult, {});
569
+
570
+ // Don't advance time past expiry
571
+ vi.advanceTimersByTime(1000);
572
+
573
+ const removed = await sessionManager.cleanupExpiredSessions();
574
+
575
+ expect(removed).toBe(0);
576
+
577
+ const sessions = await sessionManager.getActiveSessions('user-123');
578
+ expect(sessions.length).toBe(1);
579
+ });
580
+ });
581
+
582
+ describe('getSessionByToken', () => {
583
+ it('returns session by access token', async () => {
584
+ const authResult = {
585
+ profile: { id: 'user-123', email: 'user@example.com' },
586
+ tokens: { accessToken: 'unique-token-123' },
587
+ };
588
+
589
+ const created = await sessionManager.createSession('github', authResult, {});
590
+ const session = await sessionManager.getSessionByToken('unique-token-123');
591
+
592
+ expect(session).not.toBeNull();
593
+ expect(session.id).toBe(created.id);
594
+ });
595
+
596
+ it('returns null for unknown token', async () => {
597
+ const session = await sessionManager.getSessionByToken('unknown-token');
598
+
599
+ expect(session).toBeNull();
600
+ });
601
+ });
602
+
603
+ describe('destroyAllUserSessions', () => {
604
+ it('removes all sessions for a user', async () => {
605
+ const authResult = {
606
+ profile: { id: 'user-123', email: 'user@example.com' },
607
+ tokens: { accessToken: 'token' },
608
+ };
609
+
610
+ await sessionManager.createSession('github', authResult, {});
611
+ await sessionManager.createSession('google', authResult, {});
612
+ await sessionManager.createSession('azuread', authResult, {});
613
+
614
+ const removed = await sessionManager.destroyAllUserSessions('user-123');
615
+
616
+ expect(removed).toBe(3);
617
+
618
+ const sessions = await sessionManager.getActiveSessions('user-123');
619
+ expect(sessions.length).toBe(0);
620
+ });
621
+
622
+ it('does not affect other users sessions', async () => {
623
+ const authResult1 = {
624
+ profile: { id: 'user-1', email: 'user1@example.com' },
625
+ tokens: { accessToken: 'token1' },
626
+ };
627
+
628
+ const authResult2 = {
629
+ profile: { id: 'user-2', email: 'user2@example.com' },
630
+ tokens: { accessToken: 'token2' },
631
+ };
632
+
633
+ await sessionManager.createSession('github', authResult1, {});
634
+ await sessionManager.createSession('github', authResult2, {});
635
+
636
+ await sessionManager.destroyAllUserSessions('user-1');
637
+
638
+ const user1Sessions = await sessionManager.getActiveSessions('user-1');
639
+ const user2Sessions = await sessionManager.getActiveSessions('user-2');
640
+
641
+ expect(user1Sessions.length).toBe(0);
642
+ expect(user2Sessions.length).toBe(1);
643
+ });
644
+ });
645
+
646
+ describe('getSessionStats', () => {
647
+ it('returns session statistics', async () => {
648
+ const authResult1 = {
649
+ profile: { id: 'user-1', email: 'user1@example.com' },
650
+ tokens: { accessToken: 'token1' },
651
+ };
652
+
653
+ const authResult2 = {
654
+ profile: { id: 'user-2', email: 'user2@example.com' },
655
+ tokens: { accessToken: 'token2' },
656
+ };
657
+
658
+ await sessionManager.createSession('github', authResult1, {});
659
+ await sessionManager.createSession('google', authResult1, {});
660
+ await sessionManager.createSession('github', authResult2, {});
661
+
662
+ const stats = await sessionManager.getSessionStats();
663
+
664
+ expect(stats.totalSessions).toBe(3);
665
+ expect(stats.uniqueUsers).toBe(2);
666
+ expect(stats.byProvider.github).toBe(2);
667
+ expect(stats.byProvider.google).toBe(1);
668
+ });
669
+ });
670
+ });