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
JavaScript
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,
};