Skip to main content
Glama
marco-looy
by marco-looy
session-manager.js8.39 kB
import crypto from 'crypto'; /** * Session Manager for handling multi-user credential caching * Supports both OAuth credential-based and direct token-based authentication */ export class SessionManager { constructor(options = {}) { this.sessions = new Map(); this.defaultTTL = options.defaultTTL || 2 * 60 * 60 * 1000; // 2 hours default this.cleanupInterval = options.cleanupInterval || 15 * 60 * 1000; // 15 minutes // Start periodic cleanup this.startCleanup(); } /** * Create a new session with credentials * @param {Object} credentials - Session credentials * @param {string} credentials.baseUrl - Pega base URL (required) * @param {string} [credentials.apiVersion='v2'] - API version * @param {string} [credentials.clientId] - OAuth client ID * @param {string} [credentials.clientSecret] - OAuth client secret * @param {string} [credentials.accessToken] - Direct access token * @param {number} [credentials.tokenExpiry] - Token expiry in seconds from now * @param {number} [ttl] - Session TTL in milliseconds * @returns {string} Session ID */ createSession(credentials, ttl = this.defaultTTL) { this.validateCredentials(credentials); const sessionId = this.generateSessionId(); const expiresAt = Date.now() + ttl; // Determine authentication mode const authMode = credentials.accessToken ? 'token' : 'oauth'; // Calculate token expiry for direct token mode let tokenExpiry = null; if (authMode === 'token' && credentials.tokenExpiry) { tokenExpiry = Date.now() + (credentials.tokenExpiry * 1000); } const sessionData = { sessionId, createdAt: Date.now(), expiresAt, lastAccessed: Date.now(), authMode, credentials: { baseUrl: credentials.baseUrl, apiVersion: credentials.apiVersion || 'v2', ...(authMode === 'oauth' ? { clientId: credentials.clientId, clientSecret: credentials.clientSecret } : { accessToken: credentials.accessToken, tokenExpiry }) } }; this.sessions.set(sessionId, sessionData); console.log(`📋 Session created: ${sessionId} (${authMode} mode, expires in ${Math.round(ttl / (1000 * 60))} minutes)`); return sessionId; } /** * Get session data by session ID * @param {string} sessionId - Session ID * @returns {Object|null} Session data or null if not found/expired */ getSession(sessionId) { if (!sessionId) { return null; } const session = this.sessions.get(sessionId); if (!session) { return null; } // Check if session is expired if (Date.now() > session.expiresAt) { this.sessions.delete(sessionId); console.log(`🕒 Session expired and removed: ${sessionId}`); return null; } // Check if token is expired (for token mode) if (session.authMode === 'token' && session.credentials.tokenExpiry) { if (Date.now() > session.credentials.tokenExpiry) { this.sessions.delete(sessionId); console.log(`🔐 Token expired, session removed: ${sessionId}`); return null; } } // Update last accessed time session.lastAccessed = Date.now(); return session; } /** * Update session credentials * @param {string} sessionId - Session ID * @param {Object} newCredentials - Updated credentials * @returns {boolean} Success status */ updateSession(sessionId, newCredentials) { const session = this.getSession(sessionId); if (!session) { return false; } this.validateCredentials(newCredentials); // Update credentials while preserving session metadata const authMode = newCredentials.accessToken ? 'token' : 'oauth'; let tokenExpiry = null; if (authMode === 'token' && newCredentials.tokenExpiry) { tokenExpiry = Date.now() + (newCredentials.tokenExpiry * 1000); } session.authMode = authMode; session.credentials = { baseUrl: newCredentials.baseUrl, apiVersion: newCredentials.apiVersion || session.credentials.apiVersion || 'v2', ...(authMode === 'oauth' ? { clientId: newCredentials.clientId, clientSecret: newCredentials.clientSecret } : { accessToken: newCredentials.accessToken, tokenExpiry }) }; console.log(`🔄 Session updated: ${sessionId} (${authMode} mode)`); return true; } /** * Delete a session * @param {string} sessionId - Session ID * @returns {boolean} Success status */ deleteSession(sessionId) { const existed = this.sessions.delete(sessionId); if (existed) { console.log(`🗑️ Session deleted: ${sessionId}`); } return existed; } /** * Get session statistics * @returns {Object} Session statistics */ getStats() { const now = Date.now(); const activeSessions = Array.from(this.sessions.values()); return { totalSessions: activeSessions.length, oauthSessions: activeSessions.filter(s => s.authMode === 'oauth').length, tokenSessions: activeSessions.filter(s => s.authMode === 'token').length, expiredSessions: activeSessions.filter(s => now > s.expiresAt).length, oldestSession: activeSessions.length > 0 ? Math.min(...activeSessions.map(s => s.createdAt)) : null }; } /** * Clean up expired sessions * @returns {number} Number of sessions cleaned up */ cleanup() { const now = Date.now(); let cleanedUp = 0; for (const [sessionId, session] of this.sessions.entries()) { // Check session expiry if (now > session.expiresAt) { this.sessions.delete(sessionId); cleanedUp++; continue; } // Check token expiry for token-based sessions if (session.authMode === 'token' && session.credentials.tokenExpiry) { if (now > session.credentials.tokenExpiry) { this.sessions.delete(sessionId); cleanedUp++; } } } if (cleanedUp > 0) { console.log(`🧹 Cleaned up ${cleanedUp} expired sessions`); } return cleanedUp; } /** * Start periodic cleanup */ startCleanup() { setInterval(() => { this.cleanup(); }, this.cleanupInterval); } /** * Generate a unique session ID * @returns {string} Session ID */ generateSessionId() { return `session_${crypto.randomBytes(16).toString('hex')}`; } /** * Validate credentials object * @param {Object} credentials - Credentials to validate * @throws {Error} If credentials are invalid */ validateCredentials(credentials) { console.log('🔍 SessionManager validateCredentials called with:', typeof credentials, credentials); if (!credentials || typeof credentials !== 'object') { console.log('❌ Validation failed: credentials is not an object'); throw new Error('Credentials must be an object'); } if (!credentials.baseUrl || typeof credentials.baseUrl !== 'string') { throw new Error('baseUrl is required and must be a string'); } // Validate authentication mode const hasOAuthCreds = credentials.clientId && credentials.clientSecret; const hasToken = credentials.accessToken; if (!hasOAuthCreds && !hasToken) { throw new Error('Either provide clientId+clientSecret or accessToken'); } if (hasOAuthCreds && hasToken) { throw new Error('Cannot provide both OAuth credentials and access token'); } // Validate OAuth credentials if (hasOAuthCreds) { if (typeof credentials.clientId !== 'string' || typeof credentials.clientSecret !== 'string') { throw new Error('clientId and clientSecret must be strings'); } } // Validate token credentials if (hasToken) { if (typeof credentials.accessToken !== 'string') { throw new Error('accessToken must be a string'); } if (credentials.tokenExpiry && typeof credentials.tokenExpiry !== 'number') { throw new Error('tokenExpiry must be a number (seconds)'); } } // Validate optional fields if (credentials.apiVersion && typeof credentials.apiVersion !== 'string') { throw new Error('apiVersion must be a string'); } } } // Global session manager instance export const sessionManager = new SessionManager();

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/marco-looy/pega-dx-mcp'

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