UNPKG

@ufdevsllc/auth-me

Version:

Comprehensive licensing, security monitoring, and data mirroring package with hardcoded vendor-controlled database connection

549 lines (471 loc) 17.9 kB
/** * FallbackManager - Handles graceful degradation and fallback mechanisms * Provides fallback functionality when secure database or network services are unavailable */ const SecureGuardError = require('./SecureGuardError'); /** * @typedef {Object} FallbackConfig * @property {boolean} enableFallbacks - Whether fallbacks are enabled * @property {number} networkTimeoutMs - Network timeout before fallback * @property {boolean} allowDegradedMode - Allow degraded operation mode * @property {Object} fallbackLimits - Reduced limits for fallback mode * @property {boolean} logFallbackUsage - Log when fallbacks are used */ /** * @typedef {Object} FallbackOperation * @property {string} operationType - Type of operation * @property {Function} primaryFunction - Primary operation function * @property {Function} fallbackFunction - Fallback operation function * @property {Object} [options] - Operation options */ class FallbackManager { constructor() { /** @type {FallbackConfig} */ this.config = { enableFallbacks: true, networkTimeoutMs: 5000, allowDegradedMode: true, fallbackLimits: { maxWrites: 100, maxUsers: 5, maxDeployments: 1 }, logFallbackUsage: true }; /** @type {boolean} */ this.isDegradedMode = false; /** @type {Map<string, number>} */ this.fallbackUsageStats = new Map(); /** @type {Date|null} */ this.degradedModeStartTime = null; /** @type {boolean} */ this.verboseLogging = false; /** @type {Array<string>} */ this.degradationReasons = []; } /** * Initialize the fallback manager * @param {FallbackConfig} [config] - Configuration options * @param {boolean} [verboseLogging] - Enable verbose logging * @returns {void} */ initialize(config = {}, verboseLogging = false) { this.config = { ...this.config, ...config }; this.verboseLogging = verboseLogging; if (this.verboseLogging) { console.log('[FallbackManager] Initialized with config:', this.config); } } /** * Execute operation with fallback support * @param {FallbackOperation} operation - Operation configuration * @returns {Promise<any>} Operation result */ async executeWithFallback(operation) { if (!operation.primaryFunction || !operation.fallbackFunction) { throw new Error('Both primary and fallback functions are required'); } const operationType = operation.operationType || 'unknown'; const options = operation.options || {}; try { // Try primary operation first if (this.verboseLogging) { console.log(`[FallbackManager] Attempting primary operation: ${operationType}`); } const result = await this._executeWithTimeout( operation.primaryFunction, options.timeout || this.config.networkTimeoutMs ); // Primary operation succeeded if (this.isDegradedMode && options.canExitDegradedMode !== false) { this._exitDegradedMode(); } return result; } catch (primaryError) { if (this.verboseLogging) { console.warn(`[FallbackManager] Primary operation failed: ${primaryError.message}`); } // Check if fallbacks are enabled if (!this.config.enableFallbacks) { throw primaryError; } // Check if this is a network-related error that should trigger fallback if (!this._shouldUseFallback(primaryError, options)) { throw primaryError; } // Enter degraded mode if not already in it if (!this.isDegradedMode) { this._enterDegradedMode(`Primary ${operationType} failed: ${primaryError.message}`); } try { if (this.verboseLogging) { console.log(`[FallbackManager] Executing fallback for: ${operationType}`); } // Execute fallback operation const fallbackResult = await operation.fallbackFunction(); // Track fallback usage this._trackFallbackUsage(operationType); // Add metadata to indicate fallback was used if (fallbackResult && typeof fallbackResult === 'object') { fallbackResult._fallbackUsed = true; fallbackResult._fallbackReason = primaryError.message; fallbackResult._operationType = operationType; } return fallbackResult; } catch (fallbackError) { if (this.verboseLogging) { console.error(`[FallbackManager] Fallback also failed: ${fallbackError.message}`); } // Both primary and fallback failed const combinedError = new SecureGuardError( 'FALLBACK_FAILED', SecureGuardError.SEVERITY.HIGH, `Both primary and fallback operations failed for ${operationType}`, false, { operationType, primaryError: primaryError.message, fallbackError: fallbackError.message }, primaryError ); throw combinedError; } } } /** * Create a license validation fallback * @param {Function} primaryValidator - Primary validation function * @param {Function} cacheValidator - Cache-based validation function * @returns {FallbackOperation} Fallback operation */ createLicenseValidationFallback(primaryValidator, cacheValidator) { return { operationType: 'license_validation', primaryFunction: primaryValidator, fallbackFunction: async () => { const result = await cacheValidator(); if (result.isValid) { // Apply degraded mode limits if (result.license && result.license.usageLimits) { result.license.usageLimits = this._applyDegradedLimits(result.license.usageLimits); } } return result; }, options: { timeout: this.config.networkTimeoutMs, canExitDegradedMode: true } }; } /** * Create a data mirroring fallback * @param {Function} primaryMirror - Primary mirroring function * @param {Function} localStorage - Local storage function * @returns {FallbackOperation} Fallback operation */ createDataMirroringFallback(primaryMirror, localStorage) { return { operationType: 'data_mirroring', primaryFunction: primaryMirror, fallbackFunction: async () => { // Store data locally for later sync const result = await localStorage(); if (this.verboseLogging) { console.log('[FallbackManager] Data stored locally, will sync when connection restored'); } return { ...result, _storedLocally: true, _syncPending: true }; }, options: { timeout: this.config.networkTimeoutMs / 2, // Shorter timeout for mirroring canExitDegradedMode: false // Don't exit degraded mode for mirroring } }; } /** * Create a usage tracking fallback * @param {Function} primaryTracker - Primary tracking function * @param {Function} localTracker - Local tracking function * @returns {FallbackOperation} Fallback operation */ createUsageTrackingFallback(primaryTracker, localTracker) { return { operationType: 'usage_tracking', primaryFunction: primaryTracker, fallbackFunction: async () => { const result = await localTracker(); // Apply degraded mode limits if (result && result.limits) { result.limits = this._applyDegradedLimits(result.limits); } return result; }, options: { timeout: this.config.networkTimeoutMs, canExitDegradedMode: false } }; } /** * Enter degraded mode * @private * @param {string} reason - Reason for entering degraded mode * @returns {void} */ _enterDegradedMode(reason) { if (!this.config.allowDegradedMode) { if (this.verboseLogging) { console.warn('[FallbackManager] Degraded mode disabled, cannot enter'); } return; } this.isDegradedMode = true; this.degradedModeStartTime = new Date(); this.degradationReasons.push({ reason, timestamp: new Date() }); if (this.verboseLogging) { console.warn(`[FallbackManager] Entering degraded mode: ${reason}`); } if (this.config.logFallbackUsage) { console.warn(`SecureGuard: Operating in degraded mode - ${reason}`); } } /** * Exit degraded mode * @private * @returns {void} */ _exitDegradedMode() { if (!this.isDegradedMode) { return; } const duration = Date.now() - this.degradedModeStartTime.getTime(); this.isDegradedMode = false; this.degradedModeStartTime = null; if (this.verboseLogging) { console.log(`[FallbackManager] Exiting degraded mode after ${duration}ms`); } if (this.config.logFallbackUsage) { console.log(`SecureGuard: Restored normal operation after ${Math.round(duration / 1000)}s in degraded mode`); } } /** * Check if fallback should be used for the given error * @private * @param {Error} error - Error that occurred * @param {Object} options - Operation options * @returns {boolean} True if fallback should be used */ _shouldUseFallback(error, options) { // Always use fallback if explicitly requested if (options.forceFallback) { return true; } // Don't use fallback if explicitly disabled if (options.disableFallback) { return false; } // Check for network-related errors const networkErrorPatterns = [ /network/i, /connection/i, /timeout/i, /ECONNREFUSED/i, /ENOTFOUND/i, /ETIMEDOUT/i, /ECONNRESET/i, /MongoNetworkError/i, /MongoTimeoutError/i, /MongoServerSelectionError/i ]; const errorMessage = error.message || ''; const isNetworkError = networkErrorPatterns.some(pattern => pattern.test(errorMessage)); if (isNetworkError) { return true; } // Check for specific SecureGuard error codes that should trigger fallback if (error.code) { const fallbackErrorCodes = [ 'CONNECTION_FAILED', 'DATABASE_UNAVAILABLE', 'NETWORK_ERROR', 'TIMEOUT_ERROR' ]; return fallbackErrorCodes.includes(error.code); } return false; } /** * Execute function with timeout * @private * @param {Function} fn - Function to execute * @param {number} timeoutMs - Timeout in milliseconds * @returns {Promise<any>} Function result */ async _executeWithTimeout(fn, timeoutMs) { return new Promise(async (resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); // Use unref() to prevent the timer from keeping the process alive timeoutId.unref(); try { const result = await fn(); clearTimeout(timeoutId); resolve(result); } catch (error) { clearTimeout(timeoutId); reject(error); } }); } /** * Apply degraded mode limits to usage limits * @private * @param {Object} originalLimits - Original usage limits * @returns {Object} Degraded limits */ _applyDegradedLimits(originalLimits) { const degradedLimits = { ...originalLimits }; // Apply fallback limits (more restrictive) for (const [key, fallbackValue] of Object.entries(this.config.fallbackLimits)) { if (originalLimits[key] !== undefined) { // Use the more restrictive limit degradedLimits[key] = Math.min(originalLimits[key] || Infinity, fallbackValue); } else { degradedLimits[key] = fallbackValue; } } return degradedLimits; } /** * Track fallback usage statistics * @private * @param {string} operationType - Type of operation * @returns {void} */ _trackFallbackUsage(operationType) { const currentCount = this.fallbackUsageStats.get(operationType) || 0; this.fallbackUsageStats.set(operationType, currentCount + 1); if (this.config.logFallbackUsage && this.verboseLogging) { console.log(`[FallbackManager] Fallback used for ${operationType} (${currentCount + 1} times)`); } } /** * Check if currently in degraded mode * @returns {boolean} True if in degraded mode */ isInDegradedMode() { return this.isDegradedMode; } /** * Get degraded mode status and statistics * @returns {Object} Degraded mode status */ getDegradedModeStatus() { const now = new Date(); const duration = this.isDegradedMode && this.degradedModeStartTime ? now.getTime() - this.degradedModeStartTime.getTime() : 0; return { isDegradedMode: this.isDegradedMode, startTime: this.degradedModeStartTime, durationMs: duration, reasons: [...this.degradationReasons], fallbackUsageStats: Object.fromEntries(this.fallbackUsageStats), config: { ...this.config } }; } /** * Get fallback usage statistics * @returns {Object} Usage statistics */ getFallbackStats() { return { totalFallbacks: Array.from(this.fallbackUsageStats.values()).reduce((sum, count) => sum + count, 0), fallbacksByType: Object.fromEntries(this.fallbackUsageStats), isDegradedMode: this.isDegradedMode, degradedModeDuration: this.isDegradedMode && this.degradedModeStartTime ? Date.now() - this.degradedModeStartTime.getTime() : 0 }; } /** * Reset fallback statistics * @returns {void} */ resetStats() { this.fallbackUsageStats.clear(); this.degradationReasons = []; if (this.verboseLogging) { console.log('[FallbackManager] Statistics reset'); } } /** * Force exit from degraded mode * @returns {void} */ forceExitDegradedMode() { if (this.isDegradedMode) { this._exitDegradedMode(); if (this.verboseLogging) { console.log('[FallbackManager] Forced exit from degraded mode'); } } } /** * Test network connectivity * @param {string} [testUrl] - URL to test connectivity * @returns {Promise<boolean>} True if network is available */ async testNetworkConnectivity(testUrl = 'https://www.google.com') { try { const https = require('https'); const url = require('url'); return new Promise((resolve) => { const parsedUrl = url.parse(testUrl); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || 443, path: parsedUrl.path || '/', method: 'HEAD', timeout: this.config.networkTimeoutMs }; const req = https.request(options, (res) => { resolve(res.statusCode >= 200 && res.statusCode < 400); }); req.on('error', () => resolve(false)); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); }); } catch (error) { return false; } } /** * Create a health check fallback * @param {Function} primaryHealthCheck - Primary health check function * @param {Function} fallbackHealthCheck - Fallback health check function * @returns {FallbackOperation} Fallback operation */ createHealthCheckFallback(primaryHealthCheck, fallbackHealthCheck) { return { operationType: 'health_check', primaryFunction: primaryHealthCheck, fallbackFunction: fallbackHealthCheck, options: { timeout: this.config.networkTimeoutMs / 2, canExitDegradedMode: true } }; } } module.exports = FallbackManager;