Skip to main content
Glama
user-session-manager.ts10.4 kB
import { UmbrellaDualAuth } from './dual-auth.js'; import { UmbrellaApiClient } from './api-client.js'; import { debugLog } from './utils/debug-logger.js'; export interface UserSession { id: string; username: string; auth: UmbrellaDualAuth; apiClient: UmbrellaApiClient; isAuthenticated: boolean; lastActivity: Date; createdAt: Date; authHeaders?: any; accountKey?: string; // Current account key from auth // Cached accounts data from /users/plain-sub-users?IsAccount=true accountsData?: any[]; // Array of accounts with cloudTypeId // Cached user data from /users/plain-sub-users userData?: { user_type: number; accounts?: any[]; customerDivisions?: any; is_reseller_mode?: number; }; } export interface UserCredentials { username: string; password: string; } export class UserSessionManager { private sessions: Map<string, UserSession> = new Map(); private sessionTimeoutMs: number; private cleanupIntervalMs: number; private cleanupTimer?: NodeJS.Timeout; private baseURL: string; private frontendBaseURL: string; private currentOAuthSessionId?: string; // Track the current OAuth session for this request constructor( baseURL: string, frontendBaseURL?: string, sessionTimeoutMs: number = 24 * 60 * 60 * 1000, // 24 hours cleanupIntervalMs: number = 5 * 60 * 1000 // 5 minutes ) { this.baseURL = baseURL; this.frontendBaseURL = frontendBaseURL || baseURL; this.sessionTimeoutMs = sessionTimeoutMs; this.cleanupIntervalMs = cleanupIntervalMs; this.startCleanupTimer(); } /** * @deprecated Regular authentication is deprecated. Use OAuth flow instead. * This method is kept for backwards compatibility but will be removed. */ async authenticateUser(credentials: UserCredentials, sessionId?: string): Promise<{ success: boolean; sessionId: string; error?: string }> { console.error('[AUTH] Warning: Regular authentication is deprecated. Please use OAuth authentication.'); return { success: false, sessionId: sessionId || this.generateSessionId(credentials.username), error: 'Regular authentication is disabled. Please use OAuth authentication.' }; } /** * Set authentication from a verified OAuth token (for HTTPS transport) */ async setAuthFromToken(tokenData: { Authorization: string; // This contains the actual Umbrella API authorization token userEmail: string; clientId: string; userManagementInfo?: { isKeycloak: boolean; authMethod: string }; // Auth method from JWT token }): Promise<{ success: boolean; sessionId: string }> { try { // Use clientId as session identifier for multi-tenant support const sessionId = `oauth-${tokenData.clientId}`; // Create auth and API client instances const auth = new UmbrellaDualAuth(this.baseURL); const apiClient = new UmbrellaApiClient(this.baseURL, this.frontendBaseURL); // IMPORTANT: tokenData.Authorization contains the actual Umbrella API authorization // (not the OAuth Bearer token - that was already verified and extracted) // This is the real Cognito JWT token for Umbrella API auth.setPreAuthenticatedTokens({ Authorization: tokenData.Authorization // This is the actual Umbrella API auth token }); // Set the userManagementInfo from the JWT token (critical for correct API key building) if (tokenData.userManagementInfo) { (auth as any).userManagementInfo = tokenData.userManagementInfo; console.error(`[AUTH] Set userManagementInfo from token: ${JSON.stringify(tokenData.userManagementInfo)}`); } const authHeaders = { Authorization: tokenData.Authorization // Use the actual Umbrella API auth token }; // Set auth headers on API client apiClient.setAuthToken(authHeaders); apiClient.setAuth(auth); // Create or update session const session: UserSession = { id: sessionId, username: tokenData.userEmail, auth, apiClient, isAuthenticated: true, lastActivity: new Date(), createdAt: this.sessions.get(sessionId)?.createdAt || new Date(), authHeaders }; this.sessions.set(sessionId, session); // DEFERRED: User data will be fetched lazily when first needed by a tool // This prevents timeout issues during MCP initialization for accounts with many sub-users console.error(`[AUTH] OAuth - User authenticated - data will be fetched on first tool use`) // Set this as the current OAuth session for this request this.currentOAuthSessionId = sessionId; if (process.env.DEBUG === 'true') { console.error(`[AUTH] OAuth session created for ${tokenData.userEmail} (tenant: ${tokenData.clientId})`); } return { success: true, sessionId }; } catch (error: any) { console.error(`[AUTH] Failed to set auth from token: ${error.message}`); return { success: false, sessionId: '' }; } } /** * Get user session by session ID */ getUserSession(sessionId: string): UserSession | null { const session = this.sessions.get(sessionId); if (!session) { return null; } // Check if session has expired if (this.isSessionExpired(session)) { this.removeSession(sessionId); return null; } // Update last activity session.lastActivity = new Date(); return session; } /** * @deprecated Get user session by username (for backwards compatibility) */ getUserSessionByUsername(username: string): UserSession | null { // OAuth sessions don't use username-based session IDs return null; } /** * Remove a user session (logout) */ removeSession(sessionId: string): boolean { const session = this.sessions.get(sessionId); if (session) { console.error(`[SESSION] Removing session for user ${session.username} (session: ${sessionId})`); this.sessions.delete(sessionId); return true; } return false; } /** * @deprecated Remove user session by username */ removeUserSession(username: string): boolean { // OAuth sessions don't use username-based session IDs return false; } /** * Get all active sessions (for admin/debugging) */ getActiveSessions(): Array<{ id: string; username: string; lastActivity: Date; isAuthenticated: boolean }> { const activeSessions: Array<{ id: string; username: string; lastActivity: Date; isAuthenticated: boolean }> = []; for (const [id, session] of this.sessions.entries()) { if (!this.isSessionExpired(session)) { activeSessions.push({ id, username: session.username, lastActivity: session.lastActivity, isAuthenticated: session.isAuthenticated }); } } return activeSessions; } /** * Get the current active session (for single-user mode) */ getCurrentSession(): UserSession | null { // If we have a current OAuth session ID, return that session first if (this.currentOAuthSessionId) { const oauthSession = this.sessions.get(this.currentOAuthSessionId); if (oauthSession && oauthSession.isAuthenticated && !this.isSessionExpired(oauthSession)) { oauthSession.lastActivity = new Date(); return oauthSession; } } // Otherwise, return the first active session for (const [id, session] of this.sessions.entries()) { if (session.isAuthenticated && !this.isSessionExpired(session)) { session.lastActivity = new Date(); return session; } } return null; } /** * Clear the current OAuth session ID (called after request completes) */ clearCurrentOAuthSession(): void { this.currentOAuthSessionId = undefined; } /** * Generate consistent session ID from username */ private generateSessionId(username: string): string { // Create a consistent session ID based on username // This allows the same user to have the same session ID across restarts return `session_${Buffer.from(username).toString('base64').replace(/[^a-zA-Z0-9]/g, '')}`; } /** * Check if a session has expired */ private isSessionExpired(session: UserSession): boolean { const now = new Date(); const timeSinceLastActivity = now.getTime() - session.lastActivity.getTime(); return timeSinceLastActivity > this.sessionTimeoutMs; } /** * Start the cleanup timer to remove expired sessions */ private startCleanupTimer(): void { this.cleanupTimer = setInterval(() => { this.cleanupExpiredSessions(); }, this.cleanupIntervalMs); } /** * Clean up expired sessions */ private cleanupExpiredSessions(): void { const now = new Date(); const expiredSessions: string[] = []; for (const [id, session] of this.sessions.entries()) { if (this.isSessionExpired(session)) { expiredSessions.push(id); } } if (expiredSessions.length > 0) { console.error(`[CLEANUP] Cleaning up ${expiredSessions.length} expired sessions`); expiredSessions.forEach(id => { const session = this.sessions.get(id); if (session) { console.error(` - Expired: ${session.username} (inactive for ${Math.round((now.getTime() - session.lastActivity.getTime()) / 1000 / 60)} minutes)`); } this.sessions.delete(id); }); } } /** * Stop the cleanup timer (for graceful shutdown) */ shutdown(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } console.error(`[SHUTDOWN] Shutting down UserSessionManager with ${this.sessions.size} active sessions`); this.sessions.clear(); } /** * Get session statistics */ getStats(): { totalSessions: number; activeSessions: number; expiredSessions: number } { let activeSessions = 0; let expiredSessions = 0; for (const session of this.sessions.values()) { if (this.isSessionExpired(session)) { expiredSessions++; } else { activeSessions++; } } return { totalSessions: this.sessions.size, activeSessions, expiredSessions }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/daviddraiumbrella/invoice-monitoring'

If you have feedback or need assistance with the MCP directory API, please join our Discord server