UNPKG

digital-samba-mcp-server

Version:

Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API

345 lines 12.2 kB
/** * Digital Samba MCP Server - Token Manager Module * * This module provides functionality for managing API tokens, including automatic * refresh of tokens when they expire and connection management for maintaining * a stable connection to the Digital Samba API. * * Features include: * - Token refresh based on expiration monitoring * - Background token refresh to avoid service interruption * - Automatic retry with exponential backoff for failed refresh attempts * - Event emission for token refresh lifecycle events * - Configurable refresh settings * * @module token-manager * @author Digital Samba Team * @version 0.1.0 */ // Node.js built-in modules import { EventEmitter } from 'events'; // Local modules import logger from './logger.js'; import apiKeyContext from './auth.js'; import { AuthenticationError } from './errors.js'; import { DigitalSambaApiClient } from './digital-samba-api.js'; /** * Token Manager class * * Manages room tokens for the Digital Samba API, handling automatic refresh * and expiration monitoring. */ export class TokenManager extends EventEmitter { /** * Token Manager constructor * @param options Token manager options */ constructor(options) { super(); this.tokens = new Map(); this.refreshTimers = new Map(); this.refreshing = new Map(); // Set default options this.options = { apiUrl: process.env.DIGITAL_SAMBA_API_URL || 'https://api.digitalsamba.com/api/v1', roomId: options.roomId, tokenOptions: options.tokenOptions, refreshMarginMs: options.refreshMarginMs || 5 * 60 * 1000, // 5 minutes maxRefreshAttempts: options.maxRefreshAttempts || 3, initialBackoffMs: options.initialBackoffMs || 1000, // 1 second maxBackoffMs: options.maxBackoffMs || 30 * 1000, // 30 seconds retryOnAuthError: options.retryOnAuthError || false }; // Initialize API client this.apiClient = new DigitalSambaApiClient(undefined, this.options.apiUrl); logger.info('Token Manager initialized', { roomId: this.options.roomId, refreshMargin: `${this.options.refreshMarginMs / 1000} seconds`, maxRefreshAttempts: this.options.maxRefreshAttempts }); } /** * Generate a new token * @param sessionId Session ID for the token * @param apiKey API key to use for token generation * @returns Promise resolving to the generated token */ async generateToken(sessionId, apiKey) { try { logger.info('Generating new token', { sessionId, roomId: this.options.roomId }); // If API key is provided, use it directly const client = apiKey ? new DigitalSambaApiClient(apiKey, this.options.apiUrl) : this.apiClient; const response = await client.generateRoomToken(this.options.roomId, this.options.tokenOptions); // Calculate expiration time const expiresInMinutes = this.options.tokenOptions.exp ? parseInt(this.options.tokenOptions.exp, 10) : 60; // Default to 1 hour if not specified const createdAt = new Date(); const expiresAt = new Date(createdAt.getTime() + (expiresInMinutes * 60 * 1000)); // Create token object const token = { token: response.token, link: response.link, createdAt, expiresAt, sessionId }; // Store token this.tokens.set(sessionId, token); // Schedule refresh this.scheduleRefresh(sessionId); logger.info('Token generated successfully', { sessionId, expires: expiresAt.toISOString() }); // Emit event this.emit('token:generated', { sessionId, expiresAt }); return token; } catch (error) { logger.error('Error generating token', { sessionId, roomId: this.options.roomId, error: error instanceof Error ? error.message : String(error) }); // Emit event this.emit('token:error', { sessionId, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Get a token for a session * @param sessionId Session ID * @returns The token or undefined if not found */ getToken(sessionId) { return this.tokens.get(sessionId); } /** * Check if a token is expired * @param token Token to check * @returns Whether the token is expired */ isTokenExpired(token) { return token.expiresAt.getTime() <= Date.now(); } /** * Check if a token is about to expire (within the refresh margin) * @param token Token to check * @returns Whether the token is about to expire */ isTokenExpiring(token) { return token.expiresAt.getTime() <= (Date.now() + this.options.refreshMarginMs); } /** * Schedule a token refresh * @param sessionId Session ID */ scheduleRefresh(sessionId) { // Clear any existing timer this.clearRefreshTimer(sessionId); const token = this.tokens.get(sessionId); if (!token) { logger.warn('Cannot schedule refresh for non-existent token', { sessionId }); return; } // Calculate time until refresh const now = Date.now(); const refreshTime = token.expiresAt.getTime() - this.options.refreshMarginMs; const timeUntilRefresh = Math.max(0, refreshTime - now); logger.debug('Scheduling token refresh', { sessionId, refreshIn: `${Math.round(timeUntilRefresh / 1000)} seconds`, expiresAt: token.expiresAt.toISOString() }); // Set refresh timer const timerId = setTimeout(() => { this.refreshToken(sessionId); }, timeUntilRefresh); // Store timer reference this.refreshTimers.set(sessionId, timerId); } /** * Clear a refresh timer * @param sessionId Session ID */ clearRefreshTimer(sessionId) { const timerId = this.refreshTimers.get(sessionId); if (timerId) { clearTimeout(timerId); this.refreshTimers.delete(sessionId); } } /** * Refresh a token * @param sessionId Session ID * @param attempt Attempt number (for retries) */ async refreshToken(sessionId, attempt = 1) { // Check if already refreshing if (this.refreshing.get(sessionId)) { logger.debug('Token refresh already in progress', { sessionId }); return; } this.refreshing.set(sessionId, true); try { logger.info('Refreshing token', { sessionId, attempt }); // Emit event this.emit('token:refreshing', { sessionId, attempt }); // Get API key from context const apiKey = apiKeyContext.getApiKey(sessionId); if (!apiKey) { throw new AuthenticationError('No API key found for session'); } // Generate new token const token = await this.generateToken(sessionId, apiKey); // Emit success event this.emit('token:refreshed', { sessionId, expiresAt: token.expiresAt }); logger.info('Token refreshed successfully', { sessionId, expiresAt: token.expiresAt.toISOString() }); } catch (error) { logger.error('Error refreshing token', { sessionId, attempt, error: error instanceof Error ? error.message : String(error) }); // Check if we should retry const isAuthError = error instanceof AuthenticationError; if (attempt < this.options.maxRefreshAttempts && (!isAuthError || this.options.retryOnAuthError)) { // Calculate backoff time with exponential backoff const backoffTime = Math.min(this.options.initialBackoffMs * Math.pow(2, attempt - 1), this.options.maxBackoffMs); logger.info('Scheduling token refresh retry', { sessionId, attempt: attempt + 1, backoffTime: `${backoffTime / 1000} seconds` }); // Schedule retry setTimeout(() => { this.refreshToken(sessionId, attempt + 1); }, backoffTime); // Emit retry event this.emit('token:refresh-retry', { sessionId, attempt, nextAttempt: attempt + 1, backoffMs: backoffTime }); } else { // Max attempts reached or auth error logger.error('Token refresh failed after max attempts', { sessionId, maxAttempts: this.options.maxRefreshAttempts }); // Emit failure event this.emit('token:refresh-failed', { sessionId, attempts: attempt, error: error instanceof Error ? error.message : String(error) }); } } finally { this.refreshing.set(sessionId, false); } } /** * Remove a token * @param sessionId Session ID * @returns Whether the token was removed */ removeToken(sessionId) { // Clear refresh timer this.clearRefreshTimer(sessionId); // Remove from refreshing map this.refreshing.delete(sessionId); // Remove from tokens map const removed = this.tokens.delete(sessionId); if (removed) { logger.info('Token removed', { sessionId }); // Emit event this.emit('token:removed', { sessionId }); } return removed; } /** * Get all managed tokens * @returns All tokens */ getAllTokens() { return new Map(this.tokens); } /** * Get statistics about managed tokens * @returns Token statistics */ getStats() { const now = Date.now(); let expiredTokens = 0; let expiringTokens = 0; for (const token of this.tokens.values()) { if (token.expiresAt.getTime() <= now) { expiredTokens++; } else if (token.expiresAt.getTime() <= (now + this.options.refreshMarginMs)) { expiringTokens++; } } return { totalTokens: this.tokens.size, expiredTokens, expiringTokens, activeRefreshes: Array.from(this.refreshing.values()).filter(r => r).length }; } /** * Clean up resources */ destroy() { // Clear all refresh timers for (const timerId of this.refreshTimers.values()) { clearTimeout(timerId); } // Clear maps this.refreshTimers.clear(); this.refreshing.clear(); this.tokens.clear(); // Remove all listeners this.removeAllListeners(); logger.info('Token Manager destroyed'); } } /** * Create a token manager for a room * @param roomId Room ID * @param tokenOptions Token options * @param options Additional token manager options * @returns A new token manager instance */ export function createTokenManager(roomId, tokenOptions, options = {}) { return new TokenManager({ roomId, tokenOptions, ...options }); } /** * Export default token manager utilities */ export default { TokenManager, createTokenManager }; //# sourceMappingURL=token-manager.js.map