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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 22:24:44 +08:00
1 parent 79cdd26457
commit f48e1aac96
25 files changed
+825 -235

No files matched your search

+31
View File
@@ -142,6 +142,37 @@ describe("TmdbClient 缓存/限流/错误分类", () => {
expect((await dead.details("movie", 1).catch((e) => e)).kind).toBe("network");
});
it("网络/HTTP 错误的 message 与 cause 不含 api_key(验收 S3)", async () => {
const KEY = "tmdbSecretKey0123456789abcdef";
// 真实连接失败:Deno fetch 的错误信息带完整 URL
const dead = createTmdbClient({ baseUrl: "http://127.0.0.1:1/3", apiKey: KEY, retries: 0 });
const e1 = await dead.details("movie", 1).catch((x) => x);
expect(e1.kind).toBe("network");
// 模拟把 URL(含 api_key 参数与编码后的明文)拼进信息的 fetch 实现
const leaky: typeof fetch = async (input) => {
throw new TypeError(`error sending request for url (${String(input)}): key ${encodeURIComponent(KEY)}`);
};
const e2 = await createTmdbClient({ baseUrl: "http://tmdb.local/3", apiKey: KEY, retries: 0, fetch: leaky })
.details("movie", 1)
.catch((x) => x);
expect(e2.kind).toBe("network");
expect(e2.message).toContain("api_key=[REDACTED]");
// HTTP 错误
const { mock } = await setup();
mock.fault("/3/movie/872585", { status: 503 });
const e3 = await createTmdbClient({ baseUrl: `${mock.url}/3`, apiKey: KEY, retries: 0 })
.details("movie", 872585)
.catch((x) => x);
expect(e3.kind).toBe("protocol");
for (const e of [e1, e2, e3]) {
expect(e).toBeInstanceOf(TmdbError);
expect(e.message).not.toContain(KEY);
expect(e.stack ?? "").not.toContain(KEY);
expect(JSON.stringify(e.detail)).not.toContain(KEY);
expect(e.detail.cause instanceof Error).toBe(false);
}
});
it("Bearer token 鉴权可用", async () => {
mock = await createMockTmdb({ accessToken: "v4-token" });
const tmdb = createTmdbClient({ baseUrl: `${mock.url}/3`, accessToken: "v4-token" });