@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 (507 loc) • 16.2 kB
JavaScript
/**
* 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),
};
}