tlc-claude-code 1.2.29 → 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.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
- package/server/package-lock.json +14 -0
- package/server/package.json +1 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Enhanced session management for SSO with IdP integration.
|
|
5
|
+
* Handles session creation from IdP authentication, token storage,
|
|
6
|
+
* session timeout, single logout, refresh, and concurrent session limits.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default session configuration
|
|
13
|
+
*/
|
|
14
|
+
const SESSION_DEFAULTS = {
|
|
15
|
+
sessionDuration: 86400000, // 24 hours in milliseconds
|
|
16
|
+
maxConcurrentSessions: 5,
|
|
17
|
+
tokenRefreshThreshold: 300000, // 5 minutes before token expiry
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a UUID for session IDs
|
|
22
|
+
* @returns {string} UUID
|
|
23
|
+
*/
|
|
24
|
+
function generateSessionId() {
|
|
25
|
+
return crypto.randomUUID();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates an SSO Session Manager instance.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} options - Manager configuration
|
|
32
|
+
* @param {Object} options.idpManager - IdP Manager instance
|
|
33
|
+
* @param {Object} [options.mfaStore] - MFA Store instance
|
|
34
|
+
* @param {number} [options.sessionDuration] - Session duration in milliseconds
|
|
35
|
+
* @param {number} [options.maxConcurrentSessions] - Max concurrent sessions per user
|
|
36
|
+
* @param {number} [options.tokenRefreshThreshold] - Time before token expiry to trigger refresh
|
|
37
|
+
* @returns {Object} SSO Session Manager instance
|
|
38
|
+
*/
|
|
39
|
+
function createSsoSessionManager(options = {}) {
|
|
40
|
+
const {
|
|
41
|
+
idpManager,
|
|
42
|
+
mfaStore,
|
|
43
|
+
sessionDuration = SESSION_DEFAULTS.sessionDuration,
|
|
44
|
+
maxConcurrentSessions = SESSION_DEFAULTS.maxConcurrentSessions,
|
|
45
|
+
tokenRefreshThreshold = SESSION_DEFAULTS.tokenRefreshThreshold,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
// In-memory session store
|
|
49
|
+
// Key: session ID, Value: session object
|
|
50
|
+
const sessions = new Map();
|
|
51
|
+
|
|
52
|
+
// Index by user ID for efficient lookup
|
|
53
|
+
// Key: user ID, Value: Set of session IDs
|
|
54
|
+
const userSessions = new Map();
|
|
55
|
+
|
|
56
|
+
// Index by access token for efficient lookup
|
|
57
|
+
// Key: access token, Value: session ID
|
|
58
|
+
const tokenIndex = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a session from IdP authentication result
|
|
62
|
+
*
|
|
63
|
+
* @param {string} provider - Provider name (e.g., 'github', 'google')
|
|
64
|
+
* @param {Object} authResult - Authentication result from IdP
|
|
65
|
+
* @param {Object} metadata - Session metadata
|
|
66
|
+
* @returns {Promise<Object>} Created session
|
|
67
|
+
*/
|
|
68
|
+
async function createSession(provider, authResult, metadata = {}) {
|
|
69
|
+
const { profile, tokens = {}, providerType } = authResult;
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
|
|
72
|
+
// Determine MFA verification status
|
|
73
|
+
// mfaVerified is true only if user has MFA enabled AND verified with a code
|
|
74
|
+
let mfaVerified = false;
|
|
75
|
+
if (mfaStore && metadata.mfaCode) {
|
|
76
|
+
const mfaStatus = await mfaStore.getMfaStatus(profile.id);
|
|
77
|
+
if (mfaStatus.enabled) {
|
|
78
|
+
const mfaResult = await mfaStore.verifyMfa(profile.id, metadata.mfaCode);
|
|
79
|
+
mfaVerified = mfaResult.valid;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// If no mfaCode provided or MFA not enabled, mfaVerified stays false
|
|
83
|
+
|
|
84
|
+
// Calculate token expiry
|
|
85
|
+
let tokenExpiry = null;
|
|
86
|
+
if (tokens.expiresIn) {
|
|
87
|
+
tokenExpiry = now + (tokens.expiresIn * 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const session = {
|
|
91
|
+
id: generateSessionId(),
|
|
92
|
+
userId: profile.id,
|
|
93
|
+
provider,
|
|
94
|
+
providerType: providerType || 'oauth',
|
|
95
|
+
accessToken: tokens.accessToken || null,
|
|
96
|
+
refreshToken: tokens.refreshToken || null,
|
|
97
|
+
tokenExpiry,
|
|
98
|
+
createdAt: now,
|
|
99
|
+
expiresAt: now + sessionDuration,
|
|
100
|
+
lastActivityAt: now,
|
|
101
|
+
userAgent: metadata.userAgent || null,
|
|
102
|
+
ipAddress: metadata.ipAddress || null,
|
|
103
|
+
mfaVerified: mfaVerified === true ? true : false,
|
|
104
|
+
// Store additional profile data for SAML logout
|
|
105
|
+
nameId: profile.nameId || null,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Store session
|
|
109
|
+
sessions.set(session.id, session);
|
|
110
|
+
|
|
111
|
+
// Update user sessions index
|
|
112
|
+
if (!userSessions.has(session.userId)) {
|
|
113
|
+
userSessions.set(session.userId, new Set());
|
|
114
|
+
}
|
|
115
|
+
userSessions.get(session.userId).add(session.id);
|
|
116
|
+
|
|
117
|
+
// Update token index
|
|
118
|
+
if (session.accessToken) {
|
|
119
|
+
tokenIndex.set(session.accessToken, session.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Enforce session limit
|
|
123
|
+
await enforceSessionLimitForUser(session.userId);
|
|
124
|
+
|
|
125
|
+
return session;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Enforce concurrent session limit for a user
|
|
130
|
+
*
|
|
131
|
+
* @param {string} userId - User ID
|
|
132
|
+
*/
|
|
133
|
+
async function enforceSessionLimitForUser(userId) {
|
|
134
|
+
const userSessionIds = userSessions.get(userId);
|
|
135
|
+
if (!userSessionIds || userSessionIds.size <= maxConcurrentSessions) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Get all sessions for this user, sorted by creation time (oldest first)
|
|
140
|
+
const userSessionList = Array.from(userSessionIds)
|
|
141
|
+
.map(id => sessions.get(id))
|
|
142
|
+
.filter(s => s !== undefined)
|
|
143
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
144
|
+
|
|
145
|
+
// Remove oldest sessions to enforce limit
|
|
146
|
+
const sessionsToRemove = userSessionList.slice(0, userSessionList.length - maxConcurrentSessions);
|
|
147
|
+
|
|
148
|
+
for (const session of sessionsToRemove) {
|
|
149
|
+
await destroySession(session.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get a session by ID
|
|
155
|
+
*
|
|
156
|
+
* @param {string} sessionId - Session ID
|
|
157
|
+
* @returns {Promise<Object|null>} Session or null if not found/expired
|
|
158
|
+
*/
|
|
159
|
+
async function getSession(sessionId) {
|
|
160
|
+
const session = sessions.get(sessionId);
|
|
161
|
+
|
|
162
|
+
if (!session) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if session is expired
|
|
167
|
+
if (Date.now() > session.expiresAt) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Update last activity
|
|
172
|
+
session.lastActivityAt = Date.now();
|
|
173
|
+
|
|
174
|
+
return session;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a session by access token
|
|
179
|
+
*
|
|
180
|
+
* @param {string} accessToken - Access token
|
|
181
|
+
* @returns {Promise<Object|null>} Session or null if not found
|
|
182
|
+
*/
|
|
183
|
+
async function getSessionByToken(accessToken) {
|
|
184
|
+
const sessionId = tokenIndex.get(accessToken);
|
|
185
|
+
if (!sessionId) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return getSession(sessionId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Refresh a session
|
|
194
|
+
*
|
|
195
|
+
* @param {string} sessionId - Session ID
|
|
196
|
+
* @returns {Promise<Object|null>} Refreshed session or null
|
|
197
|
+
*/
|
|
198
|
+
async function refreshSession(sessionId) {
|
|
199
|
+
const session = sessions.get(sessionId);
|
|
200
|
+
|
|
201
|
+
if (!session) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if session is expired
|
|
206
|
+
if (Date.now() > session.expiresAt) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
|
|
212
|
+
// Extend session lifetime
|
|
213
|
+
session.expiresAt = now + sessionDuration;
|
|
214
|
+
session.lastActivityAt = now;
|
|
215
|
+
|
|
216
|
+
// Check if IdP tokens need refreshing
|
|
217
|
+
if (session.tokenExpiry && session.refreshToken) {
|
|
218
|
+
const timeUntilExpiry = session.tokenExpiry - now;
|
|
219
|
+
|
|
220
|
+
if (timeUntilExpiry <= tokenRefreshThreshold) {
|
|
221
|
+
await refreshIdpTokens(session);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return session;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Refresh IdP tokens for a session
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} session - Session object
|
|
232
|
+
*/
|
|
233
|
+
async function refreshIdpTokens(session) {
|
|
234
|
+
if (!idpManager || !idpManager.oauthRegistry) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const provider = idpManager.oauthRegistry.getProvider(session.provider);
|
|
239
|
+
if (!provider || !provider.tokenUrl) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetch(provider.tokenUrl, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: {
|
|
247
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
248
|
+
Accept: 'application/json',
|
|
249
|
+
},
|
|
250
|
+
body: new URLSearchParams({
|
|
251
|
+
grant_type: 'refresh_token',
|
|
252
|
+
refresh_token: session.refreshToken,
|
|
253
|
+
client_id: provider.clientId,
|
|
254
|
+
client_secret: provider.clientSecret,
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (response.ok) {
|
|
259
|
+
const tokenData = await response.json();
|
|
260
|
+
|
|
261
|
+
// Remove old token from index
|
|
262
|
+
if (session.accessToken) {
|
|
263
|
+
tokenIndex.delete(session.accessToken);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Update session with new tokens
|
|
267
|
+
session.accessToken = tokenData.access_token;
|
|
268
|
+
if (tokenData.refresh_token) {
|
|
269
|
+
session.refreshToken = tokenData.refresh_token;
|
|
270
|
+
}
|
|
271
|
+
if (tokenData.expires_in) {
|
|
272
|
+
session.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update token index
|
|
276
|
+
tokenIndex.set(session.accessToken, session.id);
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Token refresh failed, session continues with old tokens
|
|
280
|
+
console.error('Token refresh failed:', error.message);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Destroy a session
|
|
286
|
+
*
|
|
287
|
+
* @param {string} sessionId - Session ID
|
|
288
|
+
* @param {Object} [options] - Destroy options
|
|
289
|
+
* @param {boolean} [options.triggerIdpLogout] - Trigger IdP logout
|
|
290
|
+
* @returns {Promise<Object>} Result with optional logout URL
|
|
291
|
+
*/
|
|
292
|
+
async function destroySession(sessionId, options = {}) {
|
|
293
|
+
const session = sessions.get(sessionId);
|
|
294
|
+
|
|
295
|
+
const result = { success: true };
|
|
296
|
+
|
|
297
|
+
if (!session) {
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Handle IdP logout if requested
|
|
302
|
+
if (options.triggerIdpLogout && idpManager) {
|
|
303
|
+
// SAML logout
|
|
304
|
+
if (session.providerType === 'saml' && idpManager.samlProvider) {
|
|
305
|
+
const idp = idpManager.samlProvider.getIdP(session.provider);
|
|
306
|
+
if (idp && idp.sloUrl) {
|
|
307
|
+
const logoutRequest = idpManager.samlProvider.createLogoutRequest(session.provider, {
|
|
308
|
+
nameId: session.nameId,
|
|
309
|
+
sessionIndex: session.id,
|
|
310
|
+
});
|
|
311
|
+
result.logoutUrl = logoutRequest.url;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// OAuth token revocation
|
|
316
|
+
if (session.providerType !== 'saml' && idpManager.oauthRegistry) {
|
|
317
|
+
const provider = idpManager.oauthRegistry.getProvider(session.provider);
|
|
318
|
+
if (provider && provider.revokeUrl && session.accessToken) {
|
|
319
|
+
try {
|
|
320
|
+
await fetch(provider.revokeUrl, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
324
|
+
},
|
|
325
|
+
body: new URLSearchParams({
|
|
326
|
+
token: session.accessToken,
|
|
327
|
+
client_id: provider.clientId,
|
|
328
|
+
client_secret: provider.clientSecret,
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
// Token revocation failed, continue with session destruction
|
|
333
|
+
console.error('Token revocation failed:', error.message);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove from token index
|
|
340
|
+
if (session.accessToken) {
|
|
341
|
+
tokenIndex.delete(session.accessToken);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Remove from user sessions index
|
|
345
|
+
const userSessionSet = userSessions.get(session.userId);
|
|
346
|
+
if (userSessionSet) {
|
|
347
|
+
userSessionSet.delete(sessionId);
|
|
348
|
+
if (userSessionSet.size === 0) {
|
|
349
|
+
userSessions.delete(session.userId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Remove session
|
|
354
|
+
sessions.delete(sessionId);
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get all active sessions for a user
|
|
361
|
+
*
|
|
362
|
+
* @param {string} userId - User ID
|
|
363
|
+
* @returns {Promise<Object[]>} List of active sessions
|
|
364
|
+
*/
|
|
365
|
+
async function getActiveSessions(userId) {
|
|
366
|
+
const userSessionIds = userSessions.get(userId);
|
|
367
|
+
if (!userSessionIds) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
const activeSessions = [];
|
|
373
|
+
|
|
374
|
+
for (const sessionId of userSessionIds) {
|
|
375
|
+
const session = sessions.get(sessionId);
|
|
376
|
+
if (session && now <= session.expiresAt) {
|
|
377
|
+
// Return sanitized session data (no tokens in list view)
|
|
378
|
+
activeSessions.push({
|
|
379
|
+
id: session.id,
|
|
380
|
+
userId: session.userId,
|
|
381
|
+
provider: session.provider,
|
|
382
|
+
createdAt: session.createdAt,
|
|
383
|
+
expiresAt: session.expiresAt,
|
|
384
|
+
lastActivityAt: session.lastActivityAt,
|
|
385
|
+
userAgent: session.userAgent,
|
|
386
|
+
ipAddress: session.ipAddress,
|
|
387
|
+
mfaVerified: session.mfaVerified,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return activeSessions;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Cleanup expired sessions
|
|
397
|
+
*
|
|
398
|
+
* @returns {Promise<number>} Number of sessions removed
|
|
399
|
+
*/
|
|
400
|
+
async function cleanupExpiredSessions() {
|
|
401
|
+
const now = Date.now();
|
|
402
|
+
let removed = 0;
|
|
403
|
+
|
|
404
|
+
for (const [sessionId, session] of sessions) {
|
|
405
|
+
if (now > session.expiresAt) {
|
|
406
|
+
await destroySession(sessionId);
|
|
407
|
+
removed++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return removed;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Destroy all sessions for a user
|
|
416
|
+
*
|
|
417
|
+
* @param {string} userId - User ID
|
|
418
|
+
* @param {Object} [options] - Destroy options
|
|
419
|
+
* @returns {Promise<number>} Number of sessions destroyed
|
|
420
|
+
*/
|
|
421
|
+
async function destroyAllUserSessions(userId, options = {}) {
|
|
422
|
+
const userSessionIds = userSessions.get(userId);
|
|
423
|
+
if (!userSessionIds) {
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Copy the set since we'll be modifying it during iteration
|
|
428
|
+
const sessionIds = Array.from(userSessionIds);
|
|
429
|
+
let removed = 0;
|
|
430
|
+
|
|
431
|
+
for (const sessionId of sessionIds) {
|
|
432
|
+
await destroySession(sessionId, options);
|
|
433
|
+
removed++;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return removed;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get session statistics
|
|
441
|
+
*
|
|
442
|
+
* @returns {Promise<Object>} Session statistics
|
|
443
|
+
*/
|
|
444
|
+
async function getSessionStats() {
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
const byProvider = {};
|
|
447
|
+
const uniqueUsers = new Set();
|
|
448
|
+
let totalSessions = 0;
|
|
449
|
+
|
|
450
|
+
for (const session of sessions.values()) {
|
|
451
|
+
if (now <= session.expiresAt) {
|
|
452
|
+
totalSessions++;
|
|
453
|
+
uniqueUsers.add(session.userId);
|
|
454
|
+
|
|
455
|
+
if (!byProvider[session.provider]) {
|
|
456
|
+
byProvider[session.provider] = 0;
|
|
457
|
+
}
|
|
458
|
+
byProvider[session.provider]++;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
totalSessions,
|
|
464
|
+
uniqueUsers: uniqueUsers.size,
|
|
465
|
+
byProvider,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
// Session lifecycle
|
|
471
|
+
createSession,
|
|
472
|
+
getSession,
|
|
473
|
+
getSessionByToken,
|
|
474
|
+
refreshSession,
|
|
475
|
+
destroySession,
|
|
476
|
+
|
|
477
|
+
// User session management
|
|
478
|
+
getActiveSessions,
|
|
479
|
+
destroyAllUserSessions,
|
|
480
|
+
|
|
481
|
+
// Maintenance
|
|
482
|
+
cleanupExpiredSessions,
|
|
483
|
+
|
|
484
|
+
// Statistics
|
|
485
|
+
getSessionStats,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
createSsoSessionManager,
|
|
491
|
+
SESSION_DEFAULTS,
|
|
492
|
+
};
|