/** * 敏感信息守卫:工具输入输出进入模型上下文、事件流、审计前统一经过这里。 * 覆盖 cookie / passkey / token / apiKey / authorization / 含 passkey 等私密参数的 URL / URL 内嵌账号密码。 * 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送给外部模型。 */ export const REDACTED = "[REDACTED]"; const SENSITIVE_KEY_PARTS = [ "cookie", "passkey", "authkey", "torrentpass", "rsskey", "apikey", "authorization", "secret", "password", "passwd", "credential", "privatekey", ]; /** 规范化键名(去掉 - _ 并小写)后判断。以 token 结尾算敏感,tokens(用量统计)不算。 */ export function isSensitiveKey(key: string): boolean { const k = key.toLowerCase().replace(/[-_\s]/g, ""); if (k.endsWith("token")) return true; if (k === "sign" || k === "signature") return true; return SENSITIVE_KEY_PARTS.some((part) => k.includes(part)); } /** URL 查询参数/片段中视为凭证的参数名 */ const SENSITIVE_PARAM = "passkey|authkey|torrent_pass|rsskey|api[_-]?key|access[_-]?token|refresh[_-]?token|token|auth|sign|signature|credential|secret|password|key|uid_key|downhash"; const PARAM_RE = new RegExp(`([?&;#]|\\b)(${SENSITIVE_PARAM})=([^&;#\\s"'<>]+)`, "gi"); const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/gi; const BEARER_RE = /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{6,}/g; const HEADER_RE = /\b(cookie|set-cookie|authorization|x-api-key)\s*:\s*[^\r\n]+/gi; const KNOWN_KEY_RE = /\b(sk-[A-Za-z0-9_-]{8,}|sk-ant-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{16,})\b/g; export function redactString(input: string): string { return input .replace(HEADER_RE, (_m, name: string) => `${name}: ${REDACTED}`) .replace(URL_USERINFO_RE, (_m, scheme: string) => `${scheme}${REDACTED}@`) .replace(PARAM_RE, (_m, lead: string, name: string) => `${lead}${name}=${REDACTED}`) .replace(BEARER_RE, (_m, scheme: string) => `${scheme} ${REDACTED}`) .replace(KNOWN_KEY_RE, REDACTED); } /** 深度脱敏,返回新对象;不修改入参。循环引用替换为 "[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; }