Files
UHD_Media_Tool/packages/agent/src/redact.ts
T
rootandClaude Opus 5.5 f48e1aac96 fix(security): W4-E1 approvals admin-session only, shared redact table, TMDB key redaction, prod mock gate
- POST /approvals and PATCH /approvals/{id} require a console admin session
  (EndpointDef auth:"session"); API keys incl. admin scope get 403, so an
  organize.execute key can no longer approve its own plan (S1)
- tests for single-use approval checks: expired, revision/payload hash
  mismatch, consumed, revoked, pending/rejected, plan mismatch; re-approve
  after plan change succeeds once and cannot be replayed (S4)
- one redact key table in @umt/contracts shared by server logger and agent
  redactForModel: rsskey, torrent_pass, authkey, downhash, c_secure_pass,
  passkey variants, x-api-key, cookie headers, nested objects/arrays (S2)
- TMDB network errors: message and cause redacted via redactUrl (S3)
- web Agent ?mock=1 only in DEV builds or when pi.provider is fake (S7)
- EndpointDef auth annotations for /auth/*, asserted against
  PUBLIC_AUTH_PATHS (S10); doc drift fixes (S11)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:24:44 +08:00

46 lines
2.0 KiB
TypeScript

/**
* 敏感信息守卫:工具输入输出进入模型上下文、事件流、审计前统一经过这里。
* 键表见 @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<T>(value: T): T {
return redactValue(value, new WeakSet()) as T;
}
function redactValue(value: unknown, seen: WeakSet<object>): 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<string, unknown> = {};
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;
}