UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

327 lines 11.8 kB
/** * Session manager for MCP connections */ import { MCPError } from "../utils/errors.js"; import { createHash, timingSafeEqual } from "node:crypto"; /** * Session manager implementation */ export class SessionManager { config; logger; sessions = new Map(); authConfig; sessionTimeout; maxSessions; cleanupInterval; constructor(config, logger) { this.config = config; this.logger = logger; this.authConfig = config.auth ?? { enabled: false, method: "token" }; this.sessionTimeout = config.sessionTimeout ?? 3600000; // 1 hour default this.maxSessions = config.maxSessions ?? 100; // Start cleanup timer - use shorter interval in test environment const cleanupInterval = process.env.NODE_ENV === "test" ? 1000 : 60000; // 1 second in tests, 1 minute in production this.cleanupInterval = setInterval(() => { this.cleanupExpiredSessions(); }, cleanupInterval); } createSession(transport) { // Check session limit if (this.sessions.size >= this.maxSessions) { // Try to clean up expired sessions first this.cleanupExpiredSessions(); if (this.sessions.size >= this.maxSessions) { throw new MCPError("Maximum number of sessions reached"); } } const sessionId = this.generateSessionId(); const now = new Date(); const session = { id: sessionId, clientInfo: { name: "unknown", version: "unknown" }, protocolVersion: { major: 0, minor: 0, patch: 0 }, capabilities: {}, isInitialized: false, createdAt: now, lastActivity: now, transport, authenticated: !this.authConfig.enabled, // If auth disabled, session is authenticated }; this.sessions.set(sessionId, session); this.logger.info("Session created", { sessionId, transport, totalSessions: this.sessions.size, }); return session; } getSession(id) { const session = this.sessions.get(id); if (session && this.isSessionExpired(session)) { this.removeSession(id); return undefined; } return session; } initializeSession(sessionId, params) { const session = this.getSession(sessionId); if (!session) { throw new MCPError(`Session not found: ${sessionId}`); } // Validate protocol version this.validateProtocolVersion(params.protocolVersion); // Update session with initialization params session.clientInfo = params.clientInfo; session.protocolVersion = params.protocolVersion; session.capabilities = params.capabilities; session.isInitialized = true; session.lastActivity = new Date(); this.logger.info("Session initialized", { sessionId, clientInfo: params.clientInfo, protocolVersion: params.protocolVersion, }); } authenticateSession(sessionId, credentials) { const session = this.getSession(sessionId); if (!session) { return false; } if (!this.authConfig.enabled) { session.authenticated = true; return true; } let authenticated = false; switch (this.authConfig.method) { case "token": authenticated = this.authenticateToken(credentials); break; case "basic": authenticated = this.authenticateBasic(credentials); break; case "oauth": authenticated = this.authenticateOAuth(credentials); break; default: this.logger.warn("Unknown authentication method", { method: this.authConfig.method, }); return false; } if (authenticated) { session.authenticated = true; session.authData = this.extractAuthData(credentials); session.lastActivity = new Date(); this.logger.info("Session authenticated", { sessionId, method: this.authConfig.method, }); } else { this.logger.warn("Session authentication failed", { sessionId, method: this.authConfig.method, }); } return authenticated; } updateActivity(sessionId) { const session = this.getSession(sessionId); if (session) { session.lastActivity = new Date(); } } removeSession(sessionId) { const session = this.sessions.get(sessionId); if (session) { this.sessions.delete(sessionId); this.logger.info("Session removed", { sessionId, duration: Date.now() - session.createdAt.getTime(), transport: session.transport, }); } } getActiveSessions() { const activeSessions = []; for (const session of this.sessions.values()) { if (!this.isSessionExpired(session)) { activeSessions.push(session); } } return activeSessions; } cleanupExpiredSessions() { const expiredSessions = []; for (const [sessionId, session] of this.sessions) { if (this.isSessionExpired(session)) { expiredSessions.push(sessionId); } } for (const sessionId of expiredSessions) { this.removeSession(sessionId); } if (expiredSessions.length > 0) { this.logger.info("Cleaned up expired sessions", { count: expiredSessions.length, remainingSessions: this.sessions.size, }); } } getSessionMetrics() { let active = 0; let authenticated = 0; let expired = 0; for (const session of this.sessions.values()) { if (this.isSessionExpired(session)) { expired++; } else { active++; if (session.authenticated) { authenticated++; } } } return { total: this.sessions.size, active, authenticated, expired, }; } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } this.sessions.clear(); } generateSessionId() { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substr(2, 9); return `session_${timestamp}_${random}`; } isSessionExpired(session) { const now = Date.now(); const sessionAge = now - session.lastActivity.getTime(); return sessionAge > this.sessionTimeout; } validateProtocolVersion(version) { // Currently supporting MCP version 2024-11-05 const supportedVersions = [ { major: 2024, minor: 11, patch: 5 }, ]; const isSupported = supportedVersions.some((supported) => supported.major === version.major && supported.minor === version.minor && supported.patch === version.patch); if (!isSupported) { throw new MCPError(`Unsupported protocol version: ${version.major}.${version.minor}.${version.patch}`, { supportedVersions }); } } authenticateToken(credentials) { if (!this.authConfig.tokens || this.authConfig.tokens.length === 0) { return false; } const token = this.extractToken(credentials); if (!token) { return false; } // Use timing-safe comparison to prevent timing attacks return this.authConfig.tokens.some((validToken) => { const encoder = new TextEncoder(); const validTokenBytes = encoder.encode(validToken); const providedTokenBytes = encoder.encode(token); if (validTokenBytes.length !== providedTokenBytes.length) { return false; } return timingSafeEqual(validTokenBytes, providedTokenBytes); }); } authenticateBasic(credentials) { if (!this.authConfig.users || this.authConfig.users.length === 0) { return false; } const { username, password } = this.extractBasicAuth(credentials); if (!username || !password) { return false; } const user = this.authConfig.users.find((u) => u.username === username); if (!user) { return false; } // Hash the provided password and compare const hashedPassword = this.hashPassword(password); const expectedHashedPassword = this.hashPassword(user.password); const encoder = new TextEncoder(); const hashedPasswordBytes = encoder.encode(hashedPassword); const expectedHashedPasswordBytes = encoder.encode(expectedHashedPassword); if (hashedPasswordBytes.length !== expectedHashedPasswordBytes.length) { return false; } return timingSafeEqual(hashedPasswordBytes, expectedHashedPasswordBytes); } authenticateOAuth(credentials) { // TODO: Implement OAuth authentication // This would typically involve validating JWT tokens this.logger.warn("OAuth authentication not yet implemented"); return false; } extractToken(credentials) { if (typeof credentials === "string") { return credentials; } if (typeof credentials === "object" && credentials !== null) { const creds = credentials; if (typeof creds.token === "string") { return creds.token; } if (typeof creds.authorization === "string") { const match = creds.authorization.match(/^Bearer\s+(.+)$/); return match ? match[1] : null; } } return null; } extractBasicAuth(credentials) { if (typeof credentials === "object" && credentials !== null) { const creds = credentials; if (typeof creds.username === "string" && typeof creds.password === "string") { return { username: creds.username, password: creds.password, }; } if (typeof creds.authorization === "string") { const match = creds.authorization.match(/^Basic\s+(.+)$/); if (match) { try { const decoded = atob(match[1]); const [username, password] = decoded.split(":", 2); return { username, password }; } catch { return {}; } } } } return {}; } extractAuthData(credentials) { if (typeof credentials === "object" && credentials !== null) { const creds = credentials; return { token: this.extractToken(credentials) || undefined, user: (creds.username || creds.user), permissions: (creds.permissions || []), }; } return {}; } hashPassword(password) { return createHash("sha256").update(password).digest("hex"); } } //# sourceMappingURL=session-manager.js.map