import { redactSecrets } from "@/lib/crypto/keys"; type Level = "debug" | "info" | "warn" | "error"; const LEVELS: Record = { debug: 10, info: 20, warn: 30, error: 40 }; const threshold = LEVELS[(process.env.LOG_LEVEL as Level) ?? (process.env.NODE_ENV === "production" ? "info" : "debug")] ?? 20; const FORBIDDEN_KEYS = /(api[_-]?key|password|secret|token|authorization|cookie|prompt|content|messages)/i; function sanitize(value: unknown, depth = 0): unknown { if (depth > 4) return "[depth]"; if (typeof value === "string") return redactSecrets(value).slice(0, 2000); if (Array.isArray(value)) return value.slice(0, 50).map((v) => sanitize(v, depth + 1)); if (value && typeof value === "object") { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k] = FORBIDDEN_KEYS.test(k) ? "[redacted]" : sanitize(v, depth + 1); } return out; } return value; } function emit(level: Level, msg: string, meta?: Record) { if (LEVELS[level] < threshold) return; const line = JSON.stringify({ t: new Date().toISOString(), level, msg: redactSecrets(msg), ...(meta ? (sanitize(meta) as object) : {}) }); if (level === "error") console.error(line); else if (level === "warn") console.warn(line); else console.log(line); } /** * Structured JSON logger. Keys that look sensitive are redacted automatically, * and any string is passed through `redactSecrets`. Never log prompts or keys on purpose. */ export const log = { debug: (msg: string, meta?: Record) => emit("debug", msg, meta), info: (msg: string, meta?: Record) => emit("info", msg, meta), warn: (msg: string, meta?: Record) => emit("warn", msg, meta), error: (msg: string, meta?: Record) => emit("error", msg, meta), };