UNPKG

ssh-bridge-ai

Version:

AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c

232 lines (204 loc) 7.49 kB
const { ERROR_CODES } = require('./constants'); const logger = require('./logger'); /** * Custom error classes for better error handling */ class SSHBridgeError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'SSHBridgeError'; this.code = code || ERROR_CODES.INVALID_INPUT; this.details = details; this.timestamp = new Date().toISOString(); // Ensure proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, SSHBridgeError); } } } class SSHConnectionError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.SSH_CONNECTION_FAILED, details); this.name = 'SSHConnectionError'; } } class SSHAuthenticationError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.SSH_AUTHENTICATION_FAILED, details); this.name = 'SSHAuthenticationError'; } } class FileSystemError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.FILE_NOT_FOUND, details); this.name = 'FileSystemError'; } } class SecurityError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.FORBIDDEN, details); this.name = 'SecurityError'; } } class ConfigurationError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.CONFIG_INVALID, details); this.name = 'ConfigurationError'; } } class NetworkError extends SSHBridgeError { constructor(message, details = {}) { super(message, ERROR_CODES.NETWORK_TIMEOUT, details); this.name = 'NetworkError'; } } /** * Error handling utilities */ class ErrorHandler { /** * Handle and log errors appropriately * @param {Error} error - Error to handle * @param {string} context - Context where error occurred * @param {Object} additionalData - Additional data for logging */ static handleError(error, context = 'Unknown', additionalData = {}) { // Log the error logger.error(`Error in ${context}: ${error.message}`, { error: { name: error.name, code: error.code, stack: error.stack, ...additionalData } }); // Log security events for certain error types if (error.code === ERROR_CODES.UNAUTHORIZED || error.code === ERROR_CODES.FORBIDDEN || error.name === 'SecurityError') { logger.securityEvent('Security violation detected', { context, error: error.message, code: error.code, ...additionalData }); } // Return sanitized error for user display return this.sanitizeError(error); } /** * Sanitize error for user display (remove sensitive information) * @param {Error} error - Error to sanitize * @returns {Object} - Sanitized error object */ static sanitizeError(error) { const sanitized = { message: this.sanitizeErrorMessage(error.message), code: error.code || ERROR_CODES.INVALID_INPUT, name: error.name || 'Error' }; // Only include safe details in development if (process.env.NODE_ENV === 'development') { sanitized.stack = error.stack; sanitized.details = error.details; } return sanitized; } /** * Sanitize error message to remove sensitive information * @param {string} message - Error message to sanitize * @returns {string} - Sanitized message */ static sanitizeErrorMessage(message) { if (!message || typeof message !== 'string') { return 'An unknown error occurred'; } // Remove potential sensitive data patterns const sensitivePatterns = [ /password\s*[:=]\s*\S+/gi, /key\s*[:=]\s*\S+/gi, /token\s*[:=]\s*\S+/gi, /secret\s*[:=]\s*\S+/gi, /api[_-]?key\s*[:=]\s*\S+/gi, /private[_-]?key\s*[:=]\s*\S+/gi, /ssh[_-]?key\s*[:=]\s*\S+/gi, /\/home\/\w+\//g, /\/Users\/\w+\//g, /\/root\//g, ]; let sanitized = message; for (const pattern of sensitivePatterns) { sanitized = sanitized.replace(pattern, '[REDACTED]'); } return sanitized; } /** * Create a user-friendly error message * @param {Error} error - Error to create message for * @returns {string} - User-friendly message */ static createUserMessage(error) { const code = error.code || ERROR_CODES.INVALID_INPUT; const userMessages = { [ERROR_CODES.SSH_CONNECTION_FAILED]: 'Failed to connect to the server. Please check your connection and try again.', [ERROR_CODES.SSH_AUTHENTICATION_FAILED]: 'Authentication failed. Please check your credentials and try again.', [ERROR_CODES.SSH_KEY_INVALID]: 'SSH key is invalid or corrupted. Please check your key file.', [ERROR_CODES.SSH_KEY_PERMISSION_DENIED]: 'SSH key file has incorrect permissions. Please fix the file permissions.', [ERROR_CODES.FILE_NOT_FOUND]: 'File not found. Please check the file path and try again.', [ERROR_CODES.FILE_PERMISSION_DENIED]: 'Permission denied. You do not have access to this file.', [ERROR_CODES.FILE_TOO_LARGE]: 'File is too large to process. Please use a smaller file.', [ERROR_CODES.FILE_TYPE_BLOCKED]: 'File type is not allowed for security reasons.', [ERROR_CODES.NETWORK_TIMEOUT]: 'Network operation timed out. Please check your connection and try again.', [ERROR_CODES.NETWORK_UNREACHABLE]: 'Network is unreachable. Please check your connection and try again.', [ERROR_CODES.RATE_LIMIT_EXCEEDED]: 'Too many requests. Please wait a moment and try again.', [ERROR_CODES.INVALID_INPUT]: 'Invalid input provided. Please check your input and try again.', [ERROR_CODES.UNAUTHORIZED]: 'You are not authorized to perform this action.', [ERROR_CODES.FORBIDDEN]: 'This action is forbidden.', [ERROR_CODES.CONFIG_INVALID]: 'Configuration is invalid. Please check your settings.', [ERROR_CODES.CONFIG_MISSING]: 'Configuration is missing. Please run the setup command.', [ERROR_CODES.UPDATE_FAILED]: 'Update failed. Please try again later.', [ERROR_CODES.UPDATE_ROLLBACK_FAILED]: 'Update rollback failed. Please contact support.', }; return userMessages[code] || 'An unexpected error occurred. Please try again.'; } /** * Check if error is retryable * @param {Error} error - Error to check * @returns {boolean} - True if error is retryable */ static isRetryable(error) { const retryableCodes = [ ERROR_CODES.NETWORK_TIMEOUT, ERROR_CODES.NETWORK_UNREACHABLE, ERROR_CODES.SSH_CONNECTION_FAILED, ]; return retryableCodes.includes(error.code); } /** * Get retry delay for error * @param {Error} error - Error to get delay for * @param {number} attempt - Current attempt number * @returns {number} - Delay in milliseconds */ static getRetryDelay(error, attempt = 1) { if (!this.isRetryable(error)) { return 0; } // Exponential backoff with jitter const baseDelay = 1000; // 1 second const maxDelay = 30000; // 30 seconds const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); const jitter = Math.random() * 0.1 * delay; // 10% jitter return delay + jitter; } } module.exports = { SSHBridgeError, SSHConnectionError, SSHAuthenticationError, FileSystemError, SecurityError, ConfigurationError, NetworkError, ErrorHandler, };