/** * 凭证脱敏的唯一键表与字符串规则(服务端 logger、Agent 模型上下文、适配器错误信息共用)。 * 覆盖 cookie / passkey / rsskey / torrent_pass / authkey / downhash / c_secure_pass / token / apiKey / * authorization / x-api-key 头 / URL 内嵌账号密码。键名先规范化(小写、去掉 - _ 空白)再判断, * 所以 PassKey、pass-key、pass_key、X-Api-Key 都能命中。 * 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送进日志或外部模型。 */ export const REDACTED = "[REDACTED]"; /** 规范化后的键名只要包含其中一项即视为敏感 */ export const SENSITIVE_KEY_PARTS: readonly string[] = [ "cookie", "passkey", "authkey", "torrentpass", "securepass", "rsskey", "downhash", "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 查询参数/片段中视为凭证的参数名(正则片段,大小写不敏感;- _ 可选的写成 [_-]?) */ export const SENSITIVE_QUERY_PARAMS: readonly string[] = [ "pass[_-]?key", "auth[_-]?key", "torrent[_-]?pass", "rss[_-]?key", "c[_-]?secure[_-]?pass", "down[_-]?hash", "api[_-]?key", "access[_-]?token", "refresh[_-]?token", "token", "auth", "sign", "signature", "credential", "secret", "password", "uid[_-]?key", "key", ]; const PARAM_NAME_RE = new RegExp(`^(?:${SENSITIVE_QUERY_PARAMS.join("|")})$`, "i"); /** * 单个 URL 查询参数名是否为凭证(整名匹配 SENSITIVE_QUERY_PARAMS,或按 isSensitiveKey 的键名规则命中)。 * rss / browser 等适配器的参数判定都走这里,不再各自维护键表(验收 07 S-4)。 */ export function isSensitiveQueryParam(name: string): boolean { return PARAM_NAME_RE.test(name) || isSensitiveKey(name); } const PARAM_RE = new RegExp(`([?&;#]|\\b)(${SENSITIVE_QUERY_PARAMS.join("|")})=([^&;#\\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; /** 文本脱敏:请求头行、URL userinfo、query 凭证参数、Bearer/Basic、已知格式的密钥 */ 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); } /** * URL(或含 URL 的错误信息)脱敏。`secrets` 为调用方已知的明文凭证,出现在任何位置都会被替换 * (兜底 fetch 实现把 URL 编码后再拼进错误信息之类的情况)。 */ export function redactUrl(input: string, secrets: readonly (string | undefined | null)[] = []): string { let out = redactString(input); for (const s of secrets) { if (!s || s.length < 4) continue; out = out.split(s).join(REDACTED); const enc = encodeURIComponent(s); if (enc !== s) out = out.split(enc).join(REDACTED); } return out; }