/** * Error Sanitizer * * Removes sensitive information from error messages before * displaying to users or logging to external systems. * * Part of Sacred Pipeline Phase 3: Enhanced Error Handling */ import { logger } from './logger'; /** * Patterns that indicate sensitive data in error messages */ const SENSITIVE_PATTERNS = [ // Authentication & Tokens /bearer\s+[a-zA-Z0-9\-_.]+/gi, /token[:\s]+[a-zA-Z0-9\-_.]+/gi, /api[_-]?key[:\s]+[a-zA-Z0-9\-_.]+/gi, /password[:\s]+[^\s]+/gi, /secret[:\s]+[a-zA-Z0-9\-_.]+/gi, // Database connection strings /postgresql:\/\/[^\s]+/gi, /postgres:\/\/[^\s]+/gi, /mysql:\/\/[^\s]+/gi, // IP addresses (internal) /\b(?:10|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/g, // Email addresses (in error messages) /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // UUIDs (can reveal internal IDs) /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, // File paths (Unix & Windows) /\/(?:home|root|usr|var|opt|mnt)\/[^\s]*/g, /[A-Z]:\\(?:Users|Windows|Program Files)[^\s]*/g, // Stack traces with file paths /at\s+[^\s]+\s+\([^\)]+\)/g, // SQL queries (can reveal schema) /SELECT\s+.+?\s+FROM\s+[^\s]+/gi, /INSERT\s+INTO\s+[^\s]+/gi, /UPDATE\s+[^\s]+\s+SET/gi, /DELETE\s+FROM\s+[^\s]+/gi, ]; /** * Common error message patterns to make more user-friendly */ const ERROR_MESSAGE_REPLACEMENTS: Array<[RegExp, string]> = [ // Database errors [/duplicate key value violates unique constraint/gi, 'This item already exists'], [/foreign key constraint/gi, 'Related item not found'], [/violates check constraint/gi, 'Invalid data provided'], [/null value in column/gi, 'Required field is missing'], [/invalid input syntax for type/gi, 'Invalid data format'], // Auth errors [/JWT expired/gi, 'Session expired. Please log in again'], [/Invalid JWT/gi, 'Authentication failed. Please log in again'], [/No API key found/gi, 'Authentication required'], // Network errors [/ECONNREFUSED/gi, 'Service temporarily unavailable'], [/ETIMEDOUT/gi, 'Request timed out. Please try again'], [/ENOTFOUND/gi, 'Service not available'], [/Network request failed/gi, 'Network error. Check your connection'], // Rate limiting [/Too many requests/gi, 'Rate limit exceeded. Please wait before trying again'], // Supabase specific [/permission denied for table/gi, 'Access denied'], [/row level security policy/gi, 'Access denied'], ]; /** * Sanitize error message by removing sensitive information * * @param error - Error object or message * @param context - Optional context for logging * @returns Sanitized error message safe for display */ export function sanitizeErrorMessage( error: unknown, context?: { action?: string; userId?: string } ): string { let message: string; // Extract message from error object if (error instanceof Error) { message = error.message; } else if (typeof error === 'string') { message = error; } else if (error && typeof error === 'object' && 'message' in error) { message = String((error as { message: unknown }).message); } else { message = 'An unexpected error occurred'; } // Store original for logging const originalMessage = message; // Remove sensitive patterns SENSITIVE_PATTERNS.forEach(pattern => { message = message.replace(pattern, '[REDACTED]'); }); // Apply user-friendly replacements ERROR_MESSAGE_REPLACEMENTS.forEach(([pattern, replacement]) => { if (pattern.test(message)) { message = replacement; } }); // If message was heavily sanitized, provide generic message if (message.includes('[REDACTED]')) { message = 'An error occurred. Please contact support if this persists'; } // Log sanitization if message changed significantly if (originalMessage !== message && originalMessage.length > message.length + 10) { logger.info('[ErrorSanitizer] Sanitized error message', { action: context?.action, userId: context?.userId, originalLength: originalMessage.length, sanitizedLength: message.length, containsRedacted: message.includes('[REDACTED]'), }); } return message; } /** * Check if error message contains sensitive data * * @param message - Error message to check * @returns True if message contains sensitive patterns */ export function containsSensitiveData(message: string): boolean { return SENSITIVE_PATTERNS.some(pattern => pattern.test(message)); } /** * Sanitize error object for logging to external systems * * @param error - Error object to sanitize * @returns Sanitized error object */ export function sanitizeErrorForLogging(error: unknown): { message: string; name?: string; code?: string; stack?: string; } { const sanitized: { message: string; name?: string; code?: string; stack?: string; } = { message: sanitizeErrorMessage(error), }; if (error instanceof Error) { sanitized.name = error.name; // Sanitize stack trace if (error.stack) { let stack = error.stack; SENSITIVE_PATTERNS.forEach(pattern => { stack = stack.replace(pattern, '[REDACTED]'); }); sanitized.stack = stack; } // Include error code if present if ('code' in error && typeof error.code === 'string') { sanitized.code = error.code; } } return sanitized; } /** * Create a user-safe error response * * @param error - Original error * @param fallbackMessage - Optional fallback message * @returns User-safe error object */ export function createSafeErrorResponse( error: unknown, fallbackMessage = 'An error occurred' ): { message: string; code?: string; } { const sanitized = sanitizeErrorMessage(error); return { message: sanitized || fallbackMessage, code: error instanceof Error && 'code' in error ? String((error as { code: string }).code) : undefined, }; }