claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
436 lines (435 loc) • 18.4 kB
JavaScript
/**
* Session Management Service
*
* Advanced session management with concurrent session limits,
* session monitoring, and security features.
*/ import { createLogger } from '../lib/logging.js';
import { StandardError, ErrorCode } from '../lib/errors.js';
const logger = createLogger('session-management');
export class SessionManagementService {
redis;
config;
cleanupInterval;
constructor(redis, config = {}){
this.redis = redis;
this.config = {
maxSessionsPerUser: config.maxSessionsPerUser || 3,
sessionTimeoutMs: config.sessionTimeoutMs || 24 * 60 * 60 * 1000,
cleanupIntervalMs: config.cleanupIntervalMs || 60 * 60 * 1000,
enableSessionMonitoring: config.enableSessionMonitoring ?? true,
enableGeolocation: config.enableGeolocation ?? false
};
if (this.config.enableSessionMonitoring) {
this.startSessionCleanup();
}
}
/**
* Start automatic session cleanup
*/ startSessionCleanup() {
this.cleanupInterval = setInterval(async ()=>{
try {
await this.cleanupExpiredSessions();
} catch (error) {
logger.error('Session cleanup failed:', error);
}
}, this.config.cleanupIntervalMs);
logger.info('Session cleanup started', {
interval: this.config.cleanupIntervalMs
});
}
/**
* Stop session cleanup
*/ stopSessionCleanup() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
logger.info('Session cleanup stopped');
}
}
/**
* Create a new session
*/ async createSession(sessionId, userId, sessionData) {
try {
const sessionInfo = {
...sessionData,
sessionId,
lastActivity: new Date(),
isActive: true
};
// Check concurrent session limit
await this.enforceSessionLimit(userId);
// Store session data
const sessionKey = `session:${sessionId}`;
await this.redis.setex(sessionKey, Math.ceil(this.config.sessionTimeoutMs / 1000), JSON.stringify(sessionInfo));
// Add to user's session set
const userSessionsKey = `sessions:${userId}`;
await this.redis.sadd(userSessionsKey, sessionId);
await this.redis.expire(userSessionsKey, Math.ceil(this.config.sessionTimeoutMs / 1000));
// Track session statistics
if (this.config.enableSessionMonitoring) {
await this.trackSessionStats(sessionInfo);
}
logger.debug('Session created', {
sessionId,
userId
});
} catch (error) {
logger.error('Failed to create session:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to create session', {}, error);
}
}
/**
* Get session information
*/ async getSession(sessionId) {
try {
const sessionKey = `session:${sessionId}`;
const sessionData = await this.redis.get(sessionKey);
if (!sessionData) {
return null;
}
const sessionInfo = JSON.parse(sessionData);
sessionInfo.createdAt = new Date(sessionInfo.createdAt);
sessionInfo.lastActivity = new Date(sessionInfo.lastActivity);
// Update last activity
sessionInfo.lastActivity = new Date();
await this.updateSessionActivity(sessionId, sessionInfo);
return sessionInfo;
} catch (error) {
logger.error('Failed to get session:', error);
return null;
}
}
/**
* Update session activity
*/ async updateSessionActivity(sessionId, sessionInfo) {
try {
const sessionKey = `session:${sessionId}`;
sessionInfo.lastActivity = new Date();
await this.redis.setex(sessionKey, Math.ceil(this.config.sessionTimeoutMs / 1000), JSON.stringify(sessionInfo));
} catch (error) {
logger.error('Failed to update session activity:', error);
}
}
/**
* Remove a session
*/ async removeSession(sessionId, userId) {
try {
// Get session info before deletion for logging
const sessionInfo = await this.getSession(sessionId);
// Delete session data
const sessionKey = `session:${sessionId}`;
await this.redis.del(sessionKey);
// Remove from user's session set
if (userId || sessionInfo?.userId) {
const userSessionsKey = `sessions:${userId || sessionInfo.userId}`;
await this.redis.srem(userSessionsKey, sessionId);
}
logger.debug('Session removed', {
sessionId,
userId: userId || sessionInfo?.userId
});
} catch (error) {
logger.error('Failed to remove session:', error);
}
}
/**
* Remove all sessions for a user
*/ async removeAllUserSessions(userId) {
try {
const userSessionsKey = `sessions:${userId}`;
const sessionIds = await this.redis.smembers(userSessionsKey);
// Remove all sessions
const removePromises = sessionIds.map((sessionId)=>this.removeSession(sessionId, userId));
await Promise.all(removePromises);
// Clear session set
await this.redis.del(userSessionsKey);
logger.info('All user sessions removed', {
userId,
sessionCount: sessionIds.length
});
return sessionIds;
} catch (error) {
logger.error('Failed to remove all user sessions:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to remove all sessions', {}, error);
}
}
/**
* Get all active sessions for a user
*/ async getUserSessions(userId) {
try {
const userSessionsKey = `sessions:${userId}`;
const sessionIds = await this.redis.smembers(userSessionsKey);
if (sessionIds.length === 0) {
return [];
}
// Get session data for all sessions
const sessionPromises = sessionIds.map(async (sessionId)=>{
const sessionKey = `session:${sessionId}`;
const sessionData = await this.redis.get(sessionKey);
if (sessionData) {
const sessionInfo = JSON.parse(sessionData);
sessionInfo.createdAt = new Date(sessionInfo.createdAt);
sessionInfo.lastActivity = new Date(sessionInfo.lastActivity);
return sessionInfo;
}
return null;
});
const sessions = (await Promise.all(sessionPromises)).filter((session)=>session !== null);
// Clean up any stale session IDs
const validSessionIds = sessions.map((s)=>s.sessionId);
const staleSessionIds = sessionIds.filter((id)=>!validSessionIds.includes(id));
if (staleSessionIds.length > 0) {
for (const staleId of staleSessionIds){
await this.redis.srem(userSessionsKey, staleId);
}
}
return sessions.sort((a, b)=>b.lastActivity.getTime() - a.lastActivity.getTime());
} catch (error) {
logger.error('Failed to get user sessions:', error);
return [];
}
}
/**
* Enforce concurrent session limit
*/ async enforceSessionLimit(userId) {
try {
const sessions = await this.getUserSessions(userId);
if (sessions.length >= this.config.maxSessionsPerUser) {
// Remove oldest session(s) to make room
const sessionsToRemove = sessions.sort((a, b)=>a.lastActivity.getTime() - b.lastActivity.getTime()).slice(0, sessions.length - this.config.maxSessionsPerUser + 1);
for (const session of sessionsToRemove){
await this.removeSession(session.sessionId, userId);
logger.info('Session removed due to limit enforcement', {
sessionId: session.sessionId,
userId,
lastActivity: session.lastActivity
});
}
}
} catch (error) {
logger.error('Failed to enforce session limit:', error);
}
}
/**
* Clean up expired sessions
*/ async cleanupExpiredSessions() {
try {
// Get all session keys
const sessionKeys = await this.redis.keys('session:*');
let cleanedCount = 0;
for (const sessionKey of sessionKeys){
const sessionData = await this.redis.get(sessionKey);
if (sessionData) {
try {
const sessionInfo = JSON.parse(sessionData);
const lastActivity = new Date(sessionInfo.lastActivity);
const now = new Date();
// Check if session has expired
if (now.getTime() - lastActivity.getTime() > this.config.sessionTimeoutMs) {
const sessionId = sessionKey.replace('session:', '');
await this.removeSession(sessionId, sessionInfo.userId);
cleanedCount++;
}
} catch (parseError) {
// Remove corrupted session data
await this.redis.del(sessionKey);
cleanedCount++;
}
} else {
// Remove expired key
await this.redis.del(sessionKey);
cleanedCount++;
}
}
if (cleanedCount > 0) {
logger.info('Session cleanup completed', {
cleanedCount,
totalKeys: sessionKeys.length
});
}
} catch (error) {
logger.error('Session cleanup failed:', error);
}
}
/**
* Track session statistics
*/ async trackSessionStats(sessionInfo) {
try {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
// Track daily session count
await this.redis.incr(`stats:sessions:daily:${today}`);
await this.redis.expire(`stats:sessions:daily:${today}`, 7 * 24 * 60 * 60); // Keep for 7 days
// Track sessions by device type (simple user agent parsing)
const deviceType = this.parseDeviceType(sessionInfo.userAgent || '');
await this.redis.incr(`stats:sessions:device:${deviceType}`);
await this.redis.expire(`stats:sessions:device:${deviceType}`, 30 * 24 * 60 * 60); // Keep for 30 days
// Track sessions by location (if geolocation is enabled)
if (this.config.enableGeolocation && sessionInfo.location) {
const country = sessionInfo.location.split(',')[0] || 'unknown';
await this.redis.incr(`stats:sessions:location:${country}`);
await this.redis.expire(`stats:sessions:location:${country}`, 30 * 24 * 60 * 60);
}
} catch (error) {
logger.error('Failed to track session stats:', error);
}
}
/**
* Get session statistics
*/ async getSessionStats() {
try {
const today = new Date().toISOString().split('T')[0];
// Get total active sessions
const sessionKeys = await this.redis.keys('session:*');
const totalSessions = sessionKeys.length;
// Get active sessions (non-expired)
let activeSessions = 0;
const sessionsByDevice = {};
const sessionsByLocation = {};
let oldestSession;
let newestSession;
for (const sessionKey of sessionKeys){
const sessionData = await this.redis.get(sessionKey);
if (sessionData) {
try {
const sessionInfo = JSON.parse(sessionData);
activeSessions++;
const createdAt = new Date(sessionInfo.createdAt);
if (!oldestSession || createdAt < oldestSession) {
oldestSession = createdAt;
}
if (!newestSession || createdAt > newestSession) {
newestSession = createdAt;
}
// Count by device type
const deviceType = this.parseDeviceType(sessionInfo.userAgent || '');
sessionsByDevice[deviceType] = (sessionsByDevice[deviceType] || 0) + 1;
// Count by location
if (sessionInfo.location) {
const country = sessionInfo.location.split(',')[0] || 'unknown';
sessionsByLocation[country] = (sessionsByLocation[country] || 0) + 1;
}
} catch (parseError) {
// Skip corrupted data
}
}
}
// Get average session duration (approximate)
const averageSessionDuration = 0; // Would need additional tracking
return {
totalSessions,
activeSessions,
sessionsByDevice,
sessionsByLocation,
averageSessionDuration,
oldestSession,
newestSession
};
} catch (error) {
logger.error('Failed to get session stats:', error);
return {
totalSessions: 0,
activeSessions: 0,
sessionsByDevice: {},
sessionsByLocation: {},
averageSessionDuration: 0
};
}
}
/**
* Parse device type from user agent
*/ parseDeviceType(userAgent) {
const ua = userAgent.toLowerCase();
if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) {
return 'mobile';
} else if (ua.includes('tablet') || ua.includes('ipad')) {
return 'tablet';
} else if (ua.includes('bot') || ua.includes('crawler') || ua.includes('spider')) {
return 'bot';
} else {
return 'desktop';
}
}
/**
* Terminate suspicious sessions
*/ async terminateSuspiciousSessions(userId, criteria) {
try {
const sessions = await this.getUserSessions(userId);
const terminatedSessions = [];
const now = new Date();
for (const session of sessions){
let shouldTerminate = false;
// Check age
if (criteria.maxAgeHours) {
const ageHours = (now.getTime() - session.createdAt.getTime()) / (1000 * 60 * 60);
if (ageHours > criteria.maxAgeHours) {
shouldTerminate = true;
}
}
// Check IP
if (criteria.suspiciousIPs?.includes(session.ipAddress || '')) {
shouldTerminate = true;
}
// Check user agent
if (criteria.suspiciousUserAgents?.some((ua)=>session.userAgent?.toLowerCase().includes(ua.toLowerCase()))) {
shouldTerminate = true;
}
if (shouldTerminate) {
await this.removeSession(session.sessionId, userId);
terminatedSessions.push(session.sessionId);
logger.warn('Suspicious session terminated', {
sessionId: session.sessionId,
userId,
ipAddress: session.ipAddress,
userAgent: session.userAgent
});
}
}
return terminatedSessions;
} catch (error) {
logger.error('Failed to terminate suspicious sessions:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to terminate suspicious sessions', {}, error);
}
}
/**
* Get session activity timeline
*/ async getSessionActivityTimeline(userId, hours = 24) {
try {
const sessions = await this.getUserSessions(userId);
const timeline = [];
for (const session of sessions){
timeline.push({
timestamp: session.createdAt,
action: 'session_created',
details: {
sessionId: session.sessionId,
ipAddress: session.ipAddress,
userAgent: session.userAgent
}
});
timeline.push({
timestamp: session.lastActivity,
action: 'last_activity',
details: {
sessionId: session.sessionId,
duration: session.lastActivity.getTime() - session.createdAt.getTime()
}
});
}
// Sort by timestamp
timeline.sort((a, b)=>b.timestamp.getTime() - a.timestamp.getTime());
// Filter by time range
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
return timeline.filter((entry)=>entry.timestamp > cutoff);
} catch (error) {
logger.error('Failed to get session activity timeline:', error);
return [];
}
}
/**
* Cleanup resources
*/ cleanup() {
this.stopSessionCleanup();
logger.info('Session management service cleaned up');
}
}
//# sourceMappingURL=session-management.js.map