/** * 敏感信息守卫:工具输入输出进入模型上下文、事件流、审计前统一经过这里。 * 键表见 @umt/contracts 的 redact.ts(cookie / passkey / rsskey / torrent_pass / authkey / downhash / c_secure_pass / token / apiKey …)。 * 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送给外部模型。 */ import { isSensitiveKey, REDACTED, redactString } from "@umt/contracts"; // 键表与字符串规则只保留一份,在 @umt/contracts(redact.ts);服务端 logger 与这里共用。 export { isSensitiveKey, REDACTED, redactString }; /** 深度脱敏,返回新对象;不修改入参。循环引用替换为 "[Circular]"。 */ export function redactForModel(value: T): T { return redactValue(value, new WeakSet()) as T; } function redactValue(value: unknown, seen: WeakSet): unknown { if (typeof value === "string") return redactString(value); if (value === null || typeof value !== "object") return value; if (value instanceof Date) return value; if (value instanceof URL) return redactString(value.toString()); if (seen.has(value)) return "[Circular]"; seen.add(value); if (Array.isArray(value)) return value.map((item) => redactValue(item, seen)); if (value instanceof Map) { return Object.fromEntries( [...value.entries()].map(([k, v]) => [String(k), isSensitiveKey(String(k)) ? REDACTED : redactValue(v, seen)]), ); } const out: Record = {}; for (const [k, v] of Object.entries(value)) { out[k] = isSensitiveKey(k) && v !== null && v !== undefined && v !== "" ? REDACTED : redactValue(v, seen); } return out; } /** 事件/审计用的短摘要:先脱敏再截断 */ export function summarizeForLog(value: unknown, maxLength = 500): string { let text: string; try { text = typeof value === "string" ? redactString(value) : (JSON.stringify(redactForModel(value)) ?? "undefined"); } catch { text = "[unserializable]"; } return text.length > maxLength ? `${text.slice(0, maxLength)}…(${text.length - maxLength} more)` : text; }