- 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]>
46 lines
2.0 KiB
TypeScript
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;
|
|
}
|