mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -05:00
214 lines
5.8 KiB
TypeScript
214 lines
5.8 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|