@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
JavaScript
/**
* 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;