export type JsonPrimitive = string | number | boolean | null; export type JsonSafeValue = | JsonPrimitive | JsonSafeValue[] | { [key: string]: JsonSafeValue }; const REDACTED = '[Redacted]'; const CIRCULAR = '[Circular]'; const MAX_DEPTH = 6; const MAX_COLLECTION_ENTRIES = 100; function isSensitiveKey(key: string): boolean { return /^(?:authorization|cookie|set-cookie|password|passwd|secret|token|access[_-]?token|refresh[_-]?token|api[_-]?key)$/i.test(key); } function constructorLabel(value: object): string { const constructorName = value.constructor?.name; return constructorName ? `[${constructorName}]` : '[Object]'; } /** * Convert arbitrary diagnostic metadata into a bounded, JSON-safe value. * Errors intentionally expose only useful diagnostics; request/response and * socket internals can be circular and may contain credentials. */ export function toJsonSafe( value: unknown, depth = 0, ancestors = new WeakSet(), ): JsonSafeValue | undefined { if (value === undefined) return undefined; if (value === null || typeof value === 'string' || typeof value === 'boolean') return value; if (typeof value === 'number') return Number.isFinite(value) ? value : String(value); if (typeof value === 'bigint') return value.toString(); if (typeof value === 'symbol') return value.description ? `[Symbol: ${value.description}]` : '[Symbol]'; if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`; if (value instanceof Date) { return Number.isNaN(value.getTime()) ? '[Invalid Date]' : value.toISOString(); } if (value instanceof Error) { const result: { [key: string]: JsonSafeValue } = { name: value.name, message: value.message, }; if (value.stack) result.stack = value.stack; const code = Reflect.get(value, 'code'); if (typeof code === 'string' || typeof code === 'number') result.code = code; if (value.cause !== undefined && depth < MAX_DEPTH) { const cause = toJsonSafe(value.cause, depth + 1, ancestors); if (cause !== undefined) result.cause = cause; } return result; } if (depth >= MAX_DEPTH) return constructorLabel(value); if (ancestors.has(value)) return CIRCULAR; ancestors.add(value); try { if (Array.isArray(value)) { const items = value .slice(0, MAX_COLLECTION_ENTRIES) .map((item) => toJsonSafe(item, depth + 1, ancestors) ?? null); if (value.length > MAX_COLLECTION_ENTRIES) items.push(`[Truncated ${value.length - MAX_COLLECTION_ENTRIES} items]`); return items; } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { return constructorLabel(value); } const result: { [key: string]: JsonSafeValue } = {}; const keys = Object.keys(value).slice(0, MAX_COLLECTION_ENTRIES); for (const key of keys) { if (isSensitiveKey(key)) { result[key] = REDACTED; continue; } try { const child = toJsonSafe(Reflect.get(value, key), depth + 1, ancestors); if (child !== undefined) result[key] = child; } catch { result[key] = '[Unreadable]'; } } const omitted = Object.keys(value).length - keys.length; if (omitted > 0) result.__truncated__ = `${omitted} properties omitted`; return result; } finally { ancestors.delete(value); } }