UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

611 lines (538 loc) 17.6 kB
/** * Professional Cookie-Based Token Manager * Industry-standard implementation that completely eliminates infinite loops * * SECURITY & RELIABILITY FEATURES: * - Single source of truth with secure cookie storage * - Mutex pattern prevents concurrent token operations * - Emergency stop mechanism detects and prevents infinite recursion * - Rate limiting with exponential backoff * - Cross-tab synchronization via cookies * - Graceful degradation on authentication failures * - Comprehensive audit logging for debugging * - Timeout protection on all operations * * INFINITE LOOP PREVENTION: * 1. Mutex locks prevent multiple simultaneous refresh operations * 2. Emergency stop detects rapid successive calls and enforces cooldown * 3. Functions never call each other - only read/write cookies * 4. Fail-fast pattern returns null instead of retrying indefinitely * 5. Call tracking prevents same function from requesting tokens repeatedly */ class ProfessionalCookieTokenManager { constructor() { // Cookie names for token storage this.cookies = { token: 'bsp_secure_token', expiry: 'bsp_token_expiry', userInfo: 'bsp_user_data', refreshLock: 'bsp_refresh_lock', emergencyStop: 'bsp_emergency_stop', }; // Security configuration this.config = { tokenLifetime: 45 * 60 * 1000, // 45 minutes refreshTimeout: 8000, // 8 seconds max refresh time maxRefreshAttempts: 3, // Maximum retry attempts refreshCooldown: 10000, // 10 seconds between attempts emergencyStopDuration: 60000, // 1 minute emergency stop callTrackingWindow: 5000, // 5 seconds call tracking window maxCallsPerWindow: 5, // Max calls per function per window }; // State tracking for infinite loop prevention this.state = { isRefreshing: false, refreshPromise: null, lastRefreshAttempt: 0, refreshAttempts: 0, callTracker: new Map(), // Track function calls emergencyStopActive: false, }; // Initialize emergency stop check this.checkEmergencyStop(); console.log( '[ProfessionalToken] [SECURITY] Initialized with infinite-loop protection' ); } /** * Check if emergency stop is active and clear if expired */ checkEmergencyStop() { const emergencyStop = this.getCookie(this.cookies.emergencyStop); if (emergencyStop) { const stopTime = parseInt(emergencyStop, 10); if (Date.now() < stopTime) { this.state.emergencyStopActive = true; console.warn( '[ProfessionalToken] 🚨 Emergency stop active until:', new Date(stopTime).toISOString() ); return; } else { this.clearCookie(this.cookies.emergencyStop); } } this.state.emergencyStopActive = false; } /** * Track function calls to detect infinite loops */ trackFunctionCall(functionName) { const now = Date.now(); const calls = this.state.callTracker.get(functionName) || []; // Remove old calls outside tracking window const recentCalls = calls.filter( callTime => now - callTime < this.config.callTrackingWindow ); // Add current call recentCalls.push(now); this.state.callTracker.set(functionName, recentCalls); // Check for infinite loop pattern if (recentCalls.length > this.config.maxCallsPerWindow) { this.activateEmergencyStop(functionName); return false; // Block this call } return true; // Allow this call } /** * Activate emergency stop to prevent infinite loops */ activateEmergencyStop(triggeringFunction) { const stopUntil = Date.now() + this.config.emergencyStopDuration; this.setCookie( this.cookies.emergencyStop, stopUntil.toString(), this.config.emergencyStopDuration ); this.state.emergencyStopActive = true; console.error( `[ProfessionalToken] 🚨 EMERGENCY STOP activated by ${triggeringFunction} - too many rapid calls detected. Cooling down for ${this.config.emergencyStopDuration / 1000} seconds.` ); } /** * Get secure cookie value with error handling */ getCookie(cookieName) { if (typeof document === 'undefined') return null; try { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === cookieName) { return decodeURIComponent(value); } } return null; } catch (error) { console.warn( `[ProfessionalToken] Cookie read error for ${cookieName}:`, error ); return null; } } /** * Set secure cookie with all security flags */ setCookie(cookieName, value, expiryMs = this.config.tokenLifetime) { if (typeof document === 'undefined') return; try { const expires = new Date(Date.now() + expiryMs).toUTCString(); const isSecure = window.location.protocol === 'https:'; const cookieString = [ `${cookieName}=${encodeURIComponent(value)}`, `expires=${expires}`, 'path=/', isSecure ? 'Secure' : '', 'SameSite=Strict', 'HttpOnly', // Where possible ] .filter(Boolean) .join('; '); document.cookie = cookieString; } catch (error) { console.error( `[ProfessionalToken] Cookie set error for ${cookieName}:`, error ); } } /** * Clear cookie securely */ clearCookie(cookieName) { if (typeof document === 'undefined') return; try { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`; } catch (error) { console.warn( `[ProfessionalToken] Cookie clear error for ${cookieName}:`, error ); } } /** * Check if current token is valid */ isTokenValid() { const token = this.getCookie(this.cookies.token); const expiry = this.getCookie(this.cookies.expiry); if (!token || !expiry) { return false; } const expiryTime = parseInt(expiry, 10); const currentTime = Date.now(); if (currentTime >= expiryTime) { console.log('[ProfessionalToken] 🕐 Token expired, clearing'); this.clearAllTokens(); return false; } return true; } /** * Get current token synchronously */ getTokenSync() { if (this.isTokenValid()) { return this.getCookie(this.cookies.token); } return null; } /** * Main token retrieval function - INFINITE LOOP PROOF * This is the only function that should be called by external code */ async getToken(requestingFunction = 'unknown') { // Check emergency stop first this.checkEmergencyStop(); if (this.state.emergencyStopActive) { console.warn( `[ProfessionalToken] 🚨 ${requestingFunction} blocked by emergency stop` ); return null; } // Track this function call for infinite loop detection if (!this.trackFunctionCall(requestingFunction)) { console.warn( `[ProfessionalToken] 🚨 ${requestingFunction} blocked - too many rapid calls` ); return null; } try { // Fast path: Return valid token immediately const existingToken = this.getTokenSync(); if (existingToken) { console.log( `[ProfessionalToken] ⚡ ${requestingFunction} using cached token` ); return existingToken; } // Token missing or expired - need refresh console.log( `[ProfessionalToken] [LOADING] ${requestingFunction} needs token refresh` ); // Check if refresh is already in progress (MUTEX) if (this.state.isRefreshing) { console.log( `[ProfessionalToken] ⏳ ${requestingFunction} waiting for ongoing refresh` ); // Wait for existing refresh to complete if (this.state.refreshPromise) { try { const result = await this.state.refreshPromise; return result; } catch (error) { console.warn( `[ProfessionalToken] ${requestingFunction} shared refresh failed:`, error ); return null; } } // Fallback: wait with timeout return await this.waitForRefresh(requestingFunction); } // Start atomic token refresh return await this.performAtomicRefresh(requestingFunction); } catch (error) { console.error( `[ProfessionalToken] [ERROR] ${requestingFunction} error:`, error ); return null; // NEVER throw - always return null } } /** * Wait for ongoing refresh to complete */ async waitForRefresh(requestingFunction) { const maxWaitTime = this.config.refreshTimeout + 2000; // Extra 2 seconds const checkInterval = 200; const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { if (!this.state.isRefreshing) { const token = this.getTokenSync(); if (token) { console.log( `[ProfessionalToken] [SUCCESS] ${requestingFunction} got token from completed refresh` ); return token; } } await new Promise(resolve => setTimeout(resolve, checkInterval)); } console.warn( `[ProfessionalToken] ⏰ ${requestingFunction} refresh wait timeout` ); return null; } /** * Perform atomic token refresh with full protection */ async performAtomicRefresh(requestingFunction) { const now = Date.now(); // Rate limiting check if (now - this.state.lastRefreshAttempt < this.config.refreshCooldown) { if (this.state.refreshAttempts >= this.config.maxRefreshAttempts) { console.warn( `[ProfessionalToken] 🛑 ${requestingFunction} rate limited` ); return null; } } else { this.state.refreshAttempts = 0; // Reset after cooldown } // Set atomic lock this.state.isRefreshing = true; this.setCookie(this.cookies.refreshLock, 'true', 30000); // 30 second lock timeout this.state.refreshAttempts++; this.state.lastRefreshAttempt = now; console.log( `[ProfessionalToken] [SECURITY] ${requestingFunction} starting atomic refresh (attempt ${this.state.refreshAttempts})` ); try { // Create refresh promise for coordination this.state.refreshPromise = this.executeTokenRefresh(); // Race against timeout const newToken = await Promise.race([ this.state.refreshPromise, new Promise((_, reject) => setTimeout( () => reject(new Error('Refresh timeout')), this.config.refreshTimeout ) ), ]); if (newToken) { this.storeToken(newToken); console.log( `[ProfessionalToken] [SUCCESS] ${requestingFunction} refresh successful` ); return newToken; } else { console.log( `[ProfessionalToken] [ERROR] ${requestingFunction} refresh failed - no authentication available` ); return null; } } catch (error) { console.error( `[ProfessionalToken] [ERROR] ${requestingFunction} refresh error:`, error ); return null; } finally { // Always clear atomic lock this.state.isRefreshing = false; this.state.refreshPromise = null; this.clearCookie(this.cookies.refreshLock); } } /** * Execute actual token refresh from available sources */ async executeTokenRefresh() { try { // Method 1: PowerPages authentication endpoint if (typeof window !== 'undefined') { try { const response = await fetch('/_services/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', }); if (response.ok) { const tokenData = await response.json(); if (tokenData.token) { console.log('[ProfessionalToken] Token from PowerPages endpoint'); this.storeUserInfo(tokenData); return tokenData.token; } } } catch (error) { console.warn( '[ProfessionalToken] PowerPages endpoint failed:', error.message ); } // Method 2: Global variables (fallback) if (window.bsp_token) { console.log('[ProfessionalToken] Using global bsp_token'); this.storeUserInfoFromGlobals(); return window.bsp_token; } if (window.PowerPagesAuth?.token) { console.log('[ProfessionalToken] Using PowerPagesAuth token'); this.storeUserInfoFromGlobals(); return window.PowerPagesAuth.token; } // Method 3: Request verification token (last resort) const verificationToken = document.querySelector( 'input[name="__RequestVerificationToken"]' ); if (verificationToken?.value) { console.log('[ProfessionalToken] Using RequestVerificationToken'); return verificationToken.value; } } console.log('[ProfessionalToken] No authentication source available'); return null; } catch (error) { console.error( '[ProfessionalToken] Token refresh execution error:', error ); return null; } } /** * Store token with expiry */ storeToken(token, lifetimeMs = this.config.tokenLifetime) { const expiryTime = Date.now() + lifetimeMs; this.setCookie(this.cookies.token, token, lifetimeMs); this.setCookie(this.cookies.expiry, expiryTime.toString(), lifetimeMs); console.log( `[ProfessionalToken] 💾 Token stored (expires: ${new Date(expiryTime).toISOString()})` ); } /** * Store user information */ storeUserInfo(userInfo) { try { const userData = { contactId: userInfo.contactId || userInfo.contact_id, userId: userInfo.userId || userInfo.user_id, workspaceId: userInfo.workspaceId || userInfo.workspace_id, accountId: userInfo.accountId || userInfo.account_id, subscriptionId: userInfo.subscriptionId || userInfo.subscription_id, email: userInfo.email, name: userInfo.name, }; this.setCookie(this.cookies.userInfo, JSON.stringify(userData)); } catch (error) { console.warn('[ProfessionalToken] Failed to store user info:', error); } } /** * Store user info from global variables */ storeUserInfoFromGlobals() { if (typeof window === 'undefined') return; this.storeUserInfo({ contactId: window.contact_id || window.user_contactid, userId: window.user_id, workspaceId: window.workspace_id, accountId: window.account_id, subscriptionId: window.subscription_id, email: window.user_email, name: window.user_name, }); } /** * Get user information */ getUserInfo() { try { const userInfoString = this.getCookie(this.cookies.userInfo); if (userInfoString) { return JSON.parse(userInfoString); } } catch (error) { console.warn('[ProfessionalToken] Failed to parse user info:', error); } return { contactId: null, userId: null, workspaceId: null, accountId: null, subscriptionId: null, email: null, name: null, }; } /** * Check if user is authenticated */ isAuthenticated() { return this.isTokenValid(); } /** * Clear all tokens and data */ clearAllTokens() { Object.values(this.cookies).forEach(cookieName => { this.clearCookie(cookieName); }); // Reset state this.state.isRefreshing = false; this.state.refreshPromise = null; this.state.callTracker.clear(); console.log('[ProfessionalToken] 🗑️ All tokens and data cleared'); } /** * Force token refresh by clearing current and generating new */ async forceRefresh(requestingFunction = 'force-refresh') { console.log( `[ProfessionalToken] [LOADING] ${requestingFunction} forcing token refresh` ); this.clearAllTokens(); return await this.getToken(requestingFunction); } /** * Get comprehensive debug information */ getDebugInfo() { const token = this.getCookie(this.cookies.token); const expiry = this.getCookie(this.cookies.expiry); return { hasToken: !!token, tokenLength: token ? token.length : 0, expiry: expiry ? new Date(parseInt(expiry, 10)).toISOString() : null, isValid: this.isTokenValid(), isRefreshing: this.state.isRefreshing, refreshAttempts: this.state.refreshAttempts, lastRefreshAttempt: this.state.lastRefreshAttempt ? new Date(this.state.lastRefreshAttempt).toISOString() : null, emergencyStopActive: this.state.emergencyStopActive, callTrackerSize: this.state.callTracker.size, userInfo: this.getUserInfo(), config: this.config, }; } } // Create singleton instance const professionalTokenManager = new ProfessionalCookieTokenManager(); // Export for module use export default professionalTokenManager; export { ProfessionalCookieTokenManager }; // Make available globally for universal access if (typeof window !== 'undefined') { window.professionalTokenManager = professionalTokenManager; console.log( '[ProfessionalToken] [API] Professional token manager available globally' ); }