UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

508 lines 16.2 kB
/** * Error Serializer Utility * * Safe error serialization with full context preservation. * Features: * - Handles circular references * - Filters sensitive data (PII, credentials) * - Generates error fingerprints for deduplication * - Preserves custom error properties * - Filters stack traces in production * * @module processors/errors */ import { createHash } from "crypto"; /** * Fields that should be redacted for security/privacy. */ const SENSITIVE_FIELDS = [ "password", "token", "secret", "apiKey", "api_key", "authorization", "cookie", "session", "credentials", "privateKey", "private_key", "accessToken", "access_token", "refreshToken", "refresh_token", "apiSecret", "api_secret", "clientSecret", "client_secret", "bearer", "auth", "ssn", "socialSecurity", "creditCard", "credit_card", "cvv", "pin", "jwt", "x-api-key", "passphrase", "connectionString", "connection_string", ]; /** * Maximum size limits for serialization. */ const MAX_METADATA_SIZE = 2000; const MAX_STACK_FRAMES = 20; const MAX_DEPTH = 5; /** * Safely serialize an error with full context preservation. * Handles circular references, redacts sensitive data, and preserves error metadata. * * @param error - Error instance or unknown value to serialize * @param options - Serialization options * @returns Serialized error with full context * * @example * ```typescript * try { * await riskyOperation(); * } catch (error) { * const serialized = serializeError(error); * logger.error("Operation failed", serialized); * } * ``` */ export function serializeError(error, options) { const { includeStack = true, maxDepth = MAX_DEPTH, filterStacks = process.env.NODE_ENV === "production", } = options || {}; const timestamp = new Date().toISOString(); // Handle non-Error objects if (!(error instanceof Error)) { return { errorId: generateErrorId(), errorFingerprint: generateFingerprintFromString(String(error)), errorType: "UnknownError", message: String(error), metadata: { originalValue: safeStringify(error, maxDepth) }, timestamp, }; } // Extract basic error info const serialized = { errorId: generateErrorId(), errorType: error.name, message: error.message, errorFingerprint: generateErrorFingerprint(error), timestamp, }; // Add stack trace if (includeStack && error.stack) { const frames = error.stack.split("\n"); if (filterStacks) { serialized.stackFrames = filterStackFrames(frames, MAX_STACK_FRAMES); serialized.stack = serialized.stackFrames.join("\n"); } else { serialized.stack = error.stack; serialized.stackFrames = frames.slice(0, MAX_STACK_FRAMES); } } // Add custom properties from error objects if ("statusCode" in error) { serialized.statusCode = error.statusCode; } if ("isOperational" in error) { serialized.isOperational = error.isOperational; } if ("isRetryable" in error) { serialized.isRetryable = error.isRetryable; } if ("retryable" in error) { serialized.isRetryable = error.retryable; } if ("retriable" in error) { serialized.isRetryable = error.retriable; } if ("code" in error) { serialized.code = String(error.code); } // Extract additional metadata const metadata = {}; const errorKeys = Object.keys(error); const excludedKeys = [ "name", "message", "stack", "statusCode", "isOperational", "isRetryable", "retryable", "retriable", "code", "cause", ]; for (const key of errorKeys) { if (!excludedKeys.includes(key)) { const value = error[key]; metadata[key] = sanitizeAndTruncate(key, value, MAX_METADATA_SIZE, maxDepth); } } // Add context if provided if (options?.context) { for (const [key, value] of Object.entries(options.context)) { metadata[`context.${key}`] = sanitizeAndTruncate(key, value, MAX_METADATA_SIZE, maxDepth); } } if (Object.keys(metadata).length > 0) { serialized.metadata = metadata; } // Handle error chaining (cause) - use type assertion for ES2022 cause property const errorWithCause = error; if (errorWithCause.cause instanceof Error) { serialized.cause = serializeError(errorWithCause.cause, { ...options, maxDepth: Math.max(1, maxDepth - 1), }); } return serialized; } /** * Generate a unique error instance ID. * * @returns Unique error ID in format "err_<timestamp>_<random>" */ function generateErrorId() { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 11); return `err_${timestamp}_${random}`; } /** * Generate a deterministic fingerprint for error aggregation. * Normalizes dynamic values (IDs, timestamps, paths) to group similar errors together. * * @param error - Error to fingerprint * @param context - Optional context with operation name * @returns 16-character hex fingerprint hash * * @example * ```typescript * const fp1 = generateErrorFingerprint(new Error("User 123 not found")); * const fp2 = generateErrorFingerprint(new Error("User 456 not found")); * // fp1 === fp2 (same error pattern, different IDs) * ``` */ export function generateErrorFingerprint(error, context) { // Normalize message by replacing dynamic values const normalizedMessage = normalizeErrorMessage(error.message); // Get first relevant stack frame (most relevant location) const firstFrame = extractFirstRelevantFrame(error.stack); const components = [ error.name, normalizedMessage, firstFrame, context?.operation || "", ]; return createHash("sha256") .update(components.join("|")) .digest("hex") .substring(0, 16); } /** * Generate a fingerprint from a plain string (for non-Error values). * * @param value - String value to fingerprint * @returns 16-character hex fingerprint hash */ function generateFingerprintFromString(value) { const normalized = normalizeErrorMessage(value); return createHash("sha256").update(normalized).digest("hex").substring(0, 16); } /** * Normalize an error message by replacing dynamic values with placeholders. * This allows grouping similar errors together regardless of specific IDs, paths, etc. * * @param message - Original error message * @returns Normalized message with placeholders */ function normalizeErrorMessage(message) { return (message // Numbers (IDs, counts, etc.) .replace(/\d+/g, "N") // UUIDs (v4) .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, "UUID") // MongoDB ObjectIDs (24 hex chars) .replace(/[a-f0-9]{24}/gi, "OBJID") // File paths .replace(/\/[\w/.~-]+/g, "PATH") // Windows paths .replace(/[A-Z]:\\[\w\\.~-]+/gi, "PATH") // URLs .replace(/https?:\/\/[^\s]+/g, "URL") // Single-quoted strings .replace(/'[^']*'/g, "STR") // Double-quoted strings .replace(/"[^"]*"/g, "STR") // Timestamps (ISO format) .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, "TIMESTAMP") // Email addresses .replace(/[\w.-]+@[\w.-]+\.\w+/g, "EMAIL") // IP addresses .replace(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "IP")); } /** * Extract the first relevant stack frame (application code, not node_modules). * * @param stack - Full stack trace * @returns First relevant frame or empty string */ function extractFirstRelevantFrame(stack) { if (!stack) { return ""; } const frames = stack.split("\n"); for (const frame of frames) { const trimmed = frame.trim(); if (trimmed.startsWith("at ") && !trimmed.includes("node_modules") && !trimmed.includes("node:internal")) { return trimmed; } } // Fall back to first "at" frame const firstAtFrame = frames.find((f) => f.trim().startsWith("at ")); return firstAtFrame?.trim() || ""; } /** * Filter stack frames to application code only. * Removes node_modules and internal Node.js frames. * * @param frames - Array of stack frame strings * @param maxFrames - Maximum number of frames to include * @returns Filtered stack frames */ function filterStackFrames(frames, maxFrames) { const filtered = frames.filter((frame) => { // Keep error message line (first line) if (!frame.trim().startsWith("at ")) { return true; } // Filter out node internals and dependencies return (!frame.includes("node_modules") && !frame.includes("node:internal") && !frame.includes("node:async_hooks") && !frame.includes("node:events")); }); return filtered.slice(0, maxFrames); } /** * Safely stringify an object with circular reference handling. * * @param obj - Object to stringify * @param maxDepth - Maximum depth for nested objects * @returns JSON string representation */ export function safeStringify(obj, maxDepth = MAX_DEPTH) { const seen = new WeakSet(); const replacer = (key, value, depth = 0) => { // Depth limit if (depth > maxDepth) { return "[max depth reached]"; } // Check for sensitive fields if (key && isSensitiveField(key)) { return "[REDACTED]"; } // Handle null/undefined if (value === null) { return null; } if (value === undefined) { return "[undefined]"; } // Handle circular references if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "[Circular]"; } seen.add(value); } // Handle special types if (typeof value === "bigint") { return value.toString() + "n"; } if (value instanceof Error) { return { name: value.name, message: value.message, stack: value.stack?.split("\n").slice(0, 5).join("\n"), }; } if (value instanceof Date) { return value.toISOString(); } if (value instanceof RegExp) { return value.toString(); } if (typeof value === "function") { return `[Function: ${value.name || "anonymous"}]`; } if (typeof value === "symbol") { return value.toString(); } if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) { const length = value.byteLength; return `[Buffer: ${length} bytes]`; } if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { return `[Buffer: ${value.length} bytes]`; } if (value instanceof Map) { return { __type: "Map", entries: Array.from(value.entries()).slice(0, 100), }; } if (value instanceof Set) { return { __type: "Set", values: Array.from(value).slice(0, 100) }; } return value; }; try { // Custom JSON.stringify with depth tracking const stringifyWithDepth = (val, currentDepth) => { if (currentDepth > maxDepth) { return '"[max depth reached]"'; } if (val === null) { return "null"; } if (val === undefined) { return '"[undefined]"'; } const processed = replacer("", val, currentDepth); if (processed === null) { return "null"; } if (typeof processed === "string") { return JSON.stringify(processed); } if (typeof processed === "number" || typeof processed === "boolean") { return String(processed); } if (Array.isArray(processed)) { const items = processed.map((item) => stringifyWithDepth(item, currentDepth + 1)); return `[${items.join(",")}]`; } if (typeof processed === "object") { const entries = Object.entries(processed).map(([k, v]) => { const stringifiedValue = stringifyWithDepth(v, currentDepth + 1); return `${JSON.stringify(k)}:${stringifiedValue}`; }); return `{${entries.join(",")}}`; } return JSON.stringify(processed); }; return stringifyWithDepth(obj, 0); } catch { return String(obj); } } /** * Check if a field name is sensitive and should be redacted. * * @param fieldName - Field name to check * @returns true if the field is sensitive */ function isSensitiveField(fieldName) { const lowerName = fieldName.toLowerCase(); return SENSITIVE_FIELDS.some((field) => lowerName.includes(field.toLowerCase())); } /** * Sanitize and truncate metadata values. * Redacts sensitive fields and limits size. * * @param key - Field key * @param value - Field value * @param maxSize - Maximum size in characters * @param maxDepth - Maximum depth for nested objects * @returns Sanitized and truncated value */ function sanitizeAndTruncate(key, value, maxSize, maxDepth) { // Check for sensitive fields if (isSensitiveField(key)) { return "[REDACTED]"; } // Serialize value const serialized = typeof value === "string" ? value : safeStringify(value, maxDepth); // Truncate if too large if (serialized.length > maxSize) { return `${serialized.substring(0, maxSize)}... [truncated, total: ${serialized.length} chars]`; } // Return original value if it's a primitive, for cleaner logs if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return value; } // For objects, try to return parsed version for cleaner logs try { return JSON.parse(serialized); } catch { return serialized; } } /** * Extract safe metadata from an object. * Sanitizes sensitive fields and handles truncation. * * @param obj - Object to extract metadata from * @param options - Extraction options * @returns Sanitized metadata record * * @example * ```typescript * const metadata = extractSafeMetadata({ * userId: "123", * password: "secret", * data: largeObject, * }); * // Result: { userId: "123", password: "[REDACTED]", data: truncated } * ``` */ export function extractSafeMetadata(obj, options) { const { maxSize = MAX_METADATA_SIZE, maxDepth = MAX_DEPTH } = options || {}; const metadata = {}; if (!obj || typeof obj !== "object") { return { value: String(obj) }; } for (const [key, value] of Object.entries(obj)) { metadata[key] = sanitizeAndTruncate(key, value, maxSize, maxDepth); } return metadata; } /** * Create a minimal error representation for logging. * Useful when you need just the essentials without full serialization. * * @param error - Error to summarize * @returns Minimal error representation */ export function summarizeError(error) { if (error instanceof Error) { return { type: error.name, message: error.message.substring(0, 200), code: "code" in error ? String(error.code) : undefined, fingerprint: generateErrorFingerprint(error), }; } const message = String(error).substring(0, 200); return { type: "UnknownError", message, fingerprint: generateFingerprintFromString(message), }; } //# sourceMappingURL=errorSerializer.js.map