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
JavaScript
/**
* 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'
);
}