UNPKG

@ufdevsllc/authme2.0

Version:

SDK for license management and remote monitoring with automatic system tracking, license validation, and remote control capabilities

726 lines (632 loc) 24.3 kB
/** * Centralized Error Handling and Logging System * Provides comprehensive error handling, logging, and retry mechanisms */ class ErrorHandler { constructor() { this.databaseManager = null; // Will be injected if needed this.logLevels = { ERROR: 'error', WARN: 'warn', INFO: 'info', DEBUG: 'debug' }; this.maxRetries = 3; this.baseRetryDelay = 1000; // 1 second this.errorCounts = new Map(); this.circuitBreakers = new Map(); } /** * Initialize error handler with database connection * @param {Object} databaseManager - Optional database manager instance * @returns {Promise<void>} */ async initialize(databaseManager = null) { if (databaseManager) { this.databaseManager = databaseManager; } if (this.databaseManager) { try { await this.databaseManager.initMonitoringConnection(); } catch (error) { console.error('Failed to initialize error handler database connection:', error.message); // Don't throw here - error handler should work even without database } } } /** * Handle and log errors with context * @param {Error} error - The error to handle * @param {Object} context - Additional context information * @param {string} severity - Error severity level * @returns {Promise<Object>} Error handling result */ async handleError(error, context = {}, severity = this.logLevels.ERROR) { const errorId = this._generateErrorId(); const timestamp = new Date(); const errorInfo = { id: errorId, message: error.message || 'Unknown error', stack: error.stack, name: error.name || 'Error', code: error.code, severity, timestamp, context: this._sanitizeContext(context), component: context.component || 'unknown', operation: context.operation || 'unknown', licenseKey: context.licenseKey ? this._maskLicenseKey(context.licenseKey) : null }; // Log to console this._logToConsole(errorInfo); // Track error frequency this._trackErrorFrequency(errorInfo); // Log to database if available try { await this._logToDatabase(errorInfo); } catch (dbError) { console.error('Failed to log error to database:', dbError.message); } // Check if circuit breaker should be triggered this._checkCircuitBreaker(errorInfo); return { errorId, handled: true, severity, timestamp, shouldRetry: this._shouldRetry(error, context), circuitBreakerTriggered: this._isCircuitBreakerOpen(errorInfo.component) }; } /** * Handle license validation errors with specific messaging * @param {Error} error - License validation error * @param {string} licenseKey - License key being validated * @param {Object} validationContext - Validation context * @returns {Promise<Object>} Formatted license error */ async handleLicenseError(error, licenseKey, validationContext = {}) { const context = { component: 'license-validator', operation: 'license-validation', licenseKey, ...validationContext }; const result = await this.handleError(error, context, this.logLevels.ERROR); // Create user-friendly license error messages const licenseErrorMessages = { 'LICENSE_NOT_FOUND': 'The provided license key was not found. Please verify your license key.', 'LICENSE_EXPIRED': 'Your license has expired. Please renew your license to continue.', 'LICENSE_INACTIVE': 'Your license has been deactivated. Please contact support.', 'DISTRIBUTION_LIMIT_EXCEEDED': 'License distribution limit has been reached. Please upgrade your license.', 'VALIDATION_ERROR': 'License validation failed due to a technical error. Please try again.', 'CONNECTION_ERROR': 'Unable to validate license due to connection issues. Please check your internet connection.' }; const errorType = this._determineLicenseErrorType(error); const userMessage = licenseErrorMessages[errorType] || 'License validation failed. Please contact support.'; return { ...result, errorType, userMessage, technicalMessage: error.message, canRetry: ['CONNECTION_ERROR', 'VALIDATION_ERROR'].includes(errorType) }; } /** * Handle database operation errors with retry logic * @param {Error} error - Database error * @param {Object} operation - Database operation details * @param {number} attemptNumber - Current attempt number * @returns {Promise<Object>} Database error handling result */ async handleDatabaseError(error, operation = {}, attemptNumber = 1) { const context = { component: 'database-manager', operation: operation.type || 'database-operation', collection: operation.collection, attemptNumber, maxRetries: this.maxRetries }; const result = await this.handleError(error, context); // Determine if this is a retryable database error const retryableErrors = [ 'MongoNetworkError', 'MongoTimeoutError', 'MongoServerSelectionError', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND' ]; const isRetryable = retryableErrors.some(retryableError => error.name === retryableError || error.code === retryableError || error.message.includes(retryableError) ); const shouldRetry = isRetryable && attemptNumber < this.maxRetries; const retryDelay = shouldRetry ? this._calculateRetryDelay(attemptNumber) : 0; return { ...result, isRetryable, shouldRetry, retryDelay, attemptNumber, maxRetries: this.maxRetries }; } /** * Execute operation with retry logic and error handling * @param {Function} operation - Operation to execute * @param {Object} context - Operation context * @param {Object} options - Retry options * @returns {Promise<*>} Operation result */ async executeWithRetry(operation, context = {}, options = {}) { const { maxRetries = this.maxRetries, baseDelay = this.baseRetryDelay, backoffMultiplier = 2, maxDelay = 30000 } = options; let lastError; let attemptNumber = 1; while (attemptNumber <= maxRetries + 1) { try { const result = await operation(); // Log successful retry if this wasn't the first attempt if (attemptNumber > 1) { await this.logInfo(`Operation succeeded on attempt ${attemptNumber}`, { ...context, operation: 'retry-success', attemptNumber, totalAttempts: attemptNumber }); } return result; } catch (error) { lastError = error; const errorResult = await this.handleError(error, { ...context, attemptNumber, maxRetries: maxRetries + 1 }); // If this is the last attempt or error is not retryable, throw if (attemptNumber > maxRetries || !errorResult.shouldRetry) { throw error; } // Calculate delay for next attempt const delay = Math.min( baseDelay * Math.pow(backoffMultiplier, attemptNumber - 1), maxDelay ); await this.logWarn(`Retrying operation in ${delay}ms (attempt ${attemptNumber + 1}/${maxRetries + 1})`, { ...context, operation: 'retry-attempt', attemptNumber, delay, error: error.message }); await this._sleep(delay); attemptNumber++; } } throw lastError; } /** * Validate and sanitize input data * @param {*} data - Data to validate * @param {Object} schema - Validation schema * @param {Object} context - Validation context * @returns {Object} Validation result */ validateInput(data, schema, context = {}) { const errors = []; const warnings = []; try { // Basic type validation if (schema.type && typeof data !== schema.type) { errors.push(`Expected ${schema.type}, got ${typeof data}`); } // Required field validation if (schema.required && (data === null || data === undefined || data === '')) { errors.push('Field is required'); } // String validations if (schema.type === 'string' && typeof data === 'string') { if (schema.minLength && data.length < schema.minLength) { errors.push(`Minimum length is ${schema.minLength}, got ${data.length}`); } if (schema.maxLength && data.length > schema.maxLength) { errors.push(`Maximum length is ${schema.maxLength}, got ${data.length}`); } if (schema.pattern && !schema.pattern.test(data)) { errors.push('Value does not match required pattern'); } } // Number validations if (schema.type === 'number' && typeof data === 'number') { if (schema.min !== undefined && data < schema.min) { errors.push(`Minimum value is ${schema.min}, got ${data}`); } if (schema.max !== undefined && data > schema.max) { errors.push(`Maximum value is ${schema.max}, got ${data}`); } } // Object validations if (schema.type === 'object' && typeof data === 'object' && data !== null) { if (schema.properties) { for (const [key, propSchema] of Object.entries(schema.properties)) { const propResult = this.validateInput(data[key], propSchema, { ...context, field: key }); errors.push(...propResult.errors.map(err => `${key}: ${err}`)); warnings.push(...propResult.warnings.map(warn => `${key}: ${warn}`)); } } } // Security validations if (typeof data === 'string') { // Check for potential injection attacks const suspiciousPatterns = [ /<script/i, /javascript:/i, /on\w+\s*=/i, /\$\{.*\}/, /\{\{.*\}\}/ ]; for (const pattern of suspiciousPatterns) { if (pattern.test(data)) { warnings.push('Potentially unsafe content detected'); break; } } } } catch (error) { errors.push(`Validation error: ${error.message}`); } const isValid = errors.length === 0; const result = { isValid, errors, warnings, sanitizedData: this._sanitizeData(data, schema) }; // Log validation failures if (!isValid) { this.logWarn('Input validation failed', { ...context, component: 'error-handler', operation: 'input-validation', errors, warnings }); } return result; } /** * Log informational messages * @param {string} message - Log message * @param {Object} context - Log context * @returns {Promise<void>} */ async logInfo(message, context = {}) { await this._log(message, context, this.logLevels.INFO); } /** * Log warning messages * @param {string} message - Log message * @param {Object} context - Log context * @returns {Promise<void>} */ async logWarn(message, context = {}) { await this._log(message, context, this.logLevels.WARN); } /** * Log debug messages * @param {string} message - Log message * @param {Object} context - Log context * @returns {Promise<void>} */ async logDebug(message, context = {}) { await this._log(message, context, this.logLevels.DEBUG); } /** * Get error statistics * @returns {Object} Error statistics */ getErrorStatistics() { const stats = { totalErrors: 0, errorsByComponent: {}, errorsByType: {}, circuitBreakers: {}, timestamp: new Date() }; for (const [key, count] of this.errorCounts.entries()) { const [component, errorType] = key.split(':'); stats.totalErrors += count; if (!stats.errorsByComponent[component]) { stats.errorsByComponent[component] = 0; } stats.errorsByComponent[component] += count; if (!stats.errorsByType[errorType]) { stats.errorsByType[errorType] = 0; } stats.errorsByType[errorType] += count; } for (const [component, breaker] of this.circuitBreakers.entries()) { stats.circuitBreakers[component] = { isOpen: breaker.isOpen, errorCount: breaker.errorCount, lastError: breaker.lastError, openedAt: breaker.openedAt }; } return stats; } // Private methods /** * Generate unique error ID * @private * @returns {string} Error ID */ _generateErrorId() { return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Sanitize context data for logging * @private * @param {Object} context - Context to sanitize * @returns {Object} Sanitized context */ _sanitizeContext(context) { const sanitized = { ...context }; // Remove or mask sensitive data const sensitiveKeys = ['password', 'secret', 'token', 'key', 'auth']; for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) { sanitized[key] = '[REDACTED]'; } } return sanitized; } /** * Mask license key for logging * @private * @param {string} licenseKey - License key to mask * @returns {string} Masked license key */ _maskLicenseKey(licenseKey) { if (!licenseKey || typeof licenseKey !== 'string') { return '[INVALID_LICENSE_KEY]'; } return licenseKey.substring(0, 8) + '...'; } /** * Log error information to console * @private * @param {Object} errorInfo - Error information */ _logToConsole(errorInfo) { const logMethod = errorInfo.severity === this.logLevels.ERROR ? console.error : errorInfo.severity === this.logLevels.WARN ? console.warn : console.log; logMethod(`[${errorInfo.severity.toUpperCase()}] ${errorInfo.component}:${errorInfo.operation} - ${errorInfo.message}`); if (errorInfo.severity === this.logLevels.ERROR && errorInfo.stack) { console.error(errorInfo.stack); } } /** * Log error to database * @private * @param {Object} errorInfo - Error information * @returns {Promise<void>} */ async _logToDatabase(errorInfo) { if (!this.databaseManager || !this.databaseManager.isMonitoringConnected()) { return; } try { const db = this.databaseManager.getMonitoringDB(); const collection = db.collection('errorLogs'); await collection.insertOne({ ...errorInfo, createdAt: new Date() }); } catch (error) { // Don't throw database logging errors console.error('Failed to log error to database:', error.message); } } /** * Track error frequency for circuit breaker * @private * @param {Object} errorInfo - Error information */ _trackErrorFrequency(errorInfo) { const key = `${errorInfo.component}:${errorInfo.name}`; const currentCount = this.errorCounts.get(key) || 0; this.errorCounts.set(key, currentCount + 1); } /** * Check and update circuit breaker status * @private * @param {Object} errorInfo - Error information */ _checkCircuitBreaker(errorInfo) { const component = errorInfo.component; const threshold = 10; // Open circuit after 10 errors const timeWindow = 5 * 60 * 1000; // 5 minutes if (!this.circuitBreakers.has(component)) { this.circuitBreakers.set(component, { errorCount: 0, isOpen: false, lastError: null, openedAt: null }); } const breaker = this.circuitBreakers.get(component); breaker.errorCount++; breaker.lastError = new Date(); if (breaker.errorCount >= threshold && !breaker.isOpen) { breaker.isOpen = true; breaker.openedAt = new Date(); console.warn(`Circuit breaker opened for component: ${component}`); } // Auto-reset circuit breaker after time window if (breaker.isOpen && breaker.openedAt && (Date.now() - breaker.openedAt.getTime()) > timeWindow) { breaker.isOpen = false; breaker.errorCount = 0; breaker.openedAt = null; console.info(`Circuit breaker reset for component: ${component}`); } } /** * Check if circuit breaker is open for component * @private * @param {string} component - Component name * @returns {boolean} True if circuit breaker is open */ _isCircuitBreakerOpen(component) { const breaker = this.circuitBreakers.get(component); return breaker ? breaker.isOpen : false; } /** * Determine if error should be retried * @private * @param {Error} error - Error to check * @param {Object} context - Error context * @returns {boolean} True if should retry */ _shouldRetry(error, context) { // Don't retry if circuit breaker is open if (this._isCircuitBreakerOpen(context.component)) { return false; } // Don't retry validation errors if (error.name === 'ValidationError' || error.message.includes('validation')) { return false; } // Don't retry authentication errors if (error.name === 'AuthenticationError' || error.message.includes('authentication')) { return false; } // Retry network and timeout errors const retryableErrors = [ 'NetworkError', 'TimeoutError', 'ConnectionError', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ]; return retryableErrors.some(retryableError => error.name === retryableError || error.code === retryableError || error.message.includes(retryableError) ); } /** * Calculate retry delay with exponential backoff * @private * @param {number} attemptNumber - Current attempt number * @returns {number} Delay in milliseconds */ _calculateRetryDelay(attemptNumber) { const baseDelay = this.baseRetryDelay; const maxDelay = 30000; // 30 seconds max const delay = baseDelay * Math.pow(2, attemptNumber - 1); return Math.min(delay, maxDelay); } /** * Determine license error type from error * @private * @param {Error} error - License error * @returns {string} Error type */ _determineLicenseErrorType(error) { const message = error.message.toLowerCase(); if (message.includes('not found')) return 'LICENSE_NOT_FOUND'; if (message.includes('expired')) return 'LICENSE_EXPIRED'; if (message.includes('inactive') || message.includes('deactivated')) return 'LICENSE_INACTIVE'; if (message.includes('distribution limit') || message.includes('limit exceeded')) return 'DISTRIBUTION_LIMIT_EXCEEDED'; if (message.includes('connection') || message.includes('network')) return 'CONNECTION_ERROR'; return 'VALIDATION_ERROR'; } /** * Sanitize data based on schema * @private * @param {*} data - Data to sanitize * @param {Object} schema - Sanitization schema * @returns {*} Sanitized data */ _sanitizeData(data, schema) { if (typeof data === 'string') { // Remove potentially dangerous characters let sanitized = data.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); sanitized = sanitized.replace(/javascript:/gi, ''); sanitized = sanitized.replace(/on\w+\s*=/gi, ''); // Trim whitespace if specified if (schema.trim !== false) { sanitized = sanitized.trim(); } return sanitized; } return data; } /** * Generic logging method * @private * @param {string} message - Log message * @param {Object} context - Log context * @param {string} level - Log level * @returns {Promise<void>} */ async _log(message, context, level) { const logEntry = { message, level, timestamp: new Date(), context: this._sanitizeContext(context), component: context.component || 'unknown' }; // Log to console const logMethod = level === this.logLevels.ERROR ? console.error : level === this.logLevels.WARN ? console.warn : console.log; logMethod(`[${level.toUpperCase()}] ${logEntry.component} - ${message}`); // Log to database if available try { if (this.databaseManager && this.databaseManager.isMonitoringConnected()) { const db = this.databaseManager.getMonitoringDB(); const collection = db.collection('systemLogs'); await collection.insertOne(logEntry); } } catch (error) { // Don't throw database logging errors console.error('Failed to log to database:', error.message); } } /** * Sleep utility function * @private * @param {number} ms - Milliseconds to sleep * @returns {Promise<void>} */ _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Cleanup error handler resources * @returns {Promise<void>} */ async cleanup() { try { this.errorCounts.clear(); this.circuitBreakers.clear(); if (this.databaseManager) { await this.databaseManager.closeConnection(); } } catch (error) { console.error('Error during error handler cleanup:', error.message); } } } export default ErrorHandler;