/** * @file Tests server/lib/privacy.js — the secret patterns (each shape redacted, * look-alikes such as `max_tokens: 5` left alone), secret-named JSON keys, the * payload entry point being a byte-identical no-op while off or when nothing * matched, `trim_event_data` redacting before it trims, and the ignored-folder * list's path-boundary match and validation. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const privacy = require("../lib/privacy"); const { trimEventData } = require("../lib/event-payload"); const R = privacy.REDACTED; describe("redactText", () => { const cases = [ ["anthropic key", "key sk-ant-api03-AbCdEfGhIjKlMnOpQrStUv here", `key ${R} here`], ["openai key", "OPENAI sk-proj-AbCdEfGhIjKlMnOpQrStUvWx", `OPENAI ${R}`], ["github token", "ghp_0123456789abcdefghijABCDEFGHIJ", R], ["aws key id", "id AKIAABCDEFGHIJKLMNOP", `id ${R}`], ["slack", "xoxb-1234567890-abcdef", R], [ "bearer header", "curl -H 'Authorization: Bearer abc.def-ghi_jkl'", `curl -H 'Authorization: Bearer ${R}'`, ], ["password assignment", "mysql password=hunter22 -h db", `mysql password=${R} -h db`], ["env token", "export GITHUB_TOKEN=abcd1234efgh", `export GITHUB_TOKEN=${R}`], ["json-ish key", '{"api_key": "abcd1234"}', `{"api_key": ${R}}`], [ "private key", "a\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaA\n-----END OPENSSH PRIVATE KEY-----\nz", `a\n${R}\nz`, ], ]; for (const [name, input, want] of cases) { it(`redacts ${name}`, () => assert.equal(privacy.redactText(input), want)); } it("leaves look-alikes alone", () => { for (const s of [ "max_tokens: 4096", "token_count=5 input_tokens: 9", "the author wrote a secretive note", "sk-short", "Bearer of bad news", ]) { assert.equal(privacy.redactText(s), s); } }); it("is idempotent", () => { const once = privacy.redactText("password=hunter22 and ghp_0123456789abcdefghijABCDEFGHIJ"); assert.equal(privacy.redactText(once), once); }); it("stays linear on a pathological run", () => { const t = Date.now(); privacy.redactText("a-".repeat(200_000)); assert.ok(Date.now() - t < 2000); }); }); describe("redactValue", () => { it("redacts any string under a secret-named key, whatever it looks like", () => { const out = privacy.redactValue({ tool_input: { env: { DB_PASSWORD: "x", API_KEY: "y", max_tokens: "5", authorization: "z" } }, author: "me", }); assert.deepEqual(out, { tool_input: { env: { DB_PASSWORD: R, API_KEY: R, max_tokens: "5", authorization: R } }, author: "me", }); }); }); describe("redactEventJson / trim_event_data", () => { let saved; beforeEach(() => { saved = process.env[privacy.REDACT_KEY]; }); afterEach(() => { if (saved === undefined) delete process.env[privacy.REDACT_KEY]; else process.env[privacy.REDACT_KEY] = saved; }); const payload = JSON.stringify({ session_id: "s", tool_input: { command: "curl -H 'Authorization: Bearer abcdefgh12345678' x" }, }); it("is a byte-identical no-op while off", () => { delete process.env[privacy.REDACT_KEY]; assert.equal(privacy.redactEventJson(payload), payload); assert.equal(trimEventData(payload), payload); }); it("redacts and counts when on, and leaves a clean payload byte-identical", () => { process.env[privacy.REDACT_KEY] = "1"; const out = JSON.parse(trimEventData(payload)); assert.equal(out.tool_input.command, `curl -H 'Authorization: Bearer ${R}' x`); assert.equal(out._redacted, 1); const clean = JSON.stringify({ session_id: "s", tool_input: { command: "ls" } }); assert.equal(trimEventData(clean), clean); }); it("redacts a private key before the trim could cut it in two", () => { process.env[privacy.REDACT_KEY] = "1"; const key = `-----BEGIN RSA PRIVATE KEY-----\n${"A".repeat(20_000)}\n-----END RSA PRIVATE KEY-----`; const json = JSON.stringify({ tool_name: "Read", tool_response: { file: { text: key } } }); const out = trimEventData(json); assert.ok(!out.includes("AAAAAAAAAA")); assert.ok(out.includes(R)); }); it("leaves summaries alone while off", () => { delete process.env[privacy.REDACT_KEY]; assert.equal(privacy.redactSummary("password=hunter22"), "password=hunter22"); process.env[privacy.REDACT_KEY] = "1"; assert.equal(privacy.redactSummary("password=hunter22"), `password=${R}`); }); }); describe("ignored folders", () => { const env = (list) => ({ [privacy.IGNORE_KEY]: list.join(path.delimiter) }); it("matches the folder and what lies under it, never a sibling sharing the prefix", () => { const e = env(["/work/secret", "/tmp/x/"]); assert.equal(privacy.isIgnoredCwd("/work/secret", e), true); assert.equal(privacy.isIgnoredCwd("/work/secret/app", e), true); assert.equal(privacy.isIgnoredCwd("/work/secret-two", e), false); assert.equal(privacy.isIgnoredCwd("/tmp/x/y", e), true); assert.equal(privacy.isIgnoredCwd("/work", e), false); assert.equal(privacy.isIgnoredCwd(undefined, e), false); assert.equal(privacy.isIgnoredCwd("/anything", {}), false); }); it("never treats a root or a relative entry as a folder to ignore", () => { assert.deepEqual(privacy.ignoredCwds(env(["/", "rel/dir", "/ok"])).length, 2); assert.equal(privacy.isIgnoredCwd("/home/u", env(["/"])), false); }); it("validates the Settings list", () => { assert.deepEqual(privacy.validateIgnoreList(["/a/b/", " /a/b ", "", "/c"]), ["/a/b", "/c"]); assert.throws(() => privacy.validateIgnoreList(["relative"]), { code: "EBADPATH" }); assert.throws(() => privacy.validateIgnoreList(["/"]), { code: "EBADPATH" }); assert.throws(() => privacy.validateIgnoreList([`/a${path.delimiter}/b`]), { code: "EBADPATH", }); assert.throws(() => privacy.validateIgnoreList(["/a\nb"]), { code: "EBADPATH" }); assert.throws(() => privacy.validateIgnoreList("/a"), { code: "EBADPATH" }); }); });