import { describe, expect, test } from "bun:test"; import { redact, redactDeep } from "./redact"; describe("redact", () => { test("AWS access keys", () => { expect(redact("key AKIAAAAABBBBCCCCDDDD in output")).toBe("key [REDACTED:aws-access-key] in output"); }); test("GitHub tokens, all prefixes", () => { for (const prefix of ["ghp", "gho", "ghu", "ghs", "ghr"]) { expect(redact(`${prefix}_abcdefghijklmnopqrst123456`)).toBe("[REDACTED:github-token]"); } expect(redact("github_pat_11ABCDEFG_abcdefghijklmnop")).toBe("[REDACTED:github-token]"); }); test("Slack tokens", () => { expect(redact("xoxb-1234567890-abcdefgh")).toBe("[REDACTED:slack-token]"); expect(redact("xoxp-1234567890-abcdefgh")).toBe("[REDACTED:slack-token]"); }); test("sk- API keys", () => { expect(redact("using sk-proj-AbCdEfGhIjKlMnOpQrStUv")).toBe("using [REDACTED:api-key]"); }); test("JWTs", () => { const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c"; expect(redact(`Authorization: ${jwt}`)).toBe("Authorization: [REDACTED:jwt]"); }); test("Bearer tokens in headers", () => { expect(redact("authorization: Bearer tsk_live_0123456789abcdef")).toBe( "authorization: Bearer [REDACTED:bearer-token]", ); }); test("PEM private key blocks, including multi-line bodies", () => { const pem = "-----BEGIN RSA PRIVATE KEY-----\nMIIEsynthetic\nlines\n-----END RSA PRIVATE KEY-----"; expect(redact(`before\n${pem}\nafter`)).toBe("before\n[REDACTED:private-key]\nafter"); }); test("home directories collapse to ~ — the head names the machine's owner, not the file", () => { expect(redact("cat /Users/goran/Projects/x/docs/plan.md")).toBe("cat ~/Projects/x/docs/plan.md"); expect(redact("ls -la /home/runner/work")).toBe("ls -la ~/work"); expect(redact("the path /Users/goran alone")).toBe("the path ~ alone"); expect(redact('"/Users/dev/project/a.ts"')).toBe('"~/project/a.ts"'); expect(redact("DIR=/Users/dev/tmp")).toBe("DIR=~/tmp"); }); test("path segments that merely contain /Users/ are someone else's namespace and survive", () => { expect(redact("https://example.com/Users/123/profile")).toBe("https://example.com/Users/123/profile"); expect(redact("see /srv/data/Users/archive")).toBe("see /srv/data/Users/archive"); }); test("generic credential assignments keep the key, lose the value", () => { expect(redact("password=hunter2")).toBe("password=[REDACTED:credential]"); expect(redact("API_KEY: abc123def")).toBe("API_KEY: [REDACTED:credential]"); expect(redact("secret = topsecret")).toBe("secret = [REDACTED:credential]"); expect(redact("token=deadbeef")).toBe("token=[REDACTED:credential]"); }); test("does not double-wrap a value another rule already redacted", () => { expect(redact("api_key=sk-AbCdEfGhIjKlMnOpQrStUv")).toBe("api_key=[REDACTED:api-key]"); }); // Both of these escaped the first redactor and were caught by the publish gate // when sealing a real session — the underscore in a prefixed name is a word char, // so a bare \b before the keyword never fired. test("prefixed env-var names, not just bare keywords", () => { expect(redact("TALESEAL_API_KEY=abc123def456")).toBe("TALESEAL_API_KEY=[REDACTED:credential]"); expect(redact("MY_DB_PASSWORD=hunter2")).toBe("MY_DB_PASSWORD=[REDACTED:credential]"); expect(redact("BETTER_AUTH_SECRET: s3cr3tvalue")).toBe("BETTER_AUTH_SECRET: [REDACTED:credential]"); }); test("taleseal publish keys, bare or assigned", () => { expect(redact("seal --key tk_SAgfQqBrlFlbPf5AsEK3zwEPccV8AI61")).toBe("seal --key [REDACTED:taleseal-key]"); }); test("taleseal claim secrets — the tc_ own-capability for an anonymous page", () => { // pinned tokens on purpose: a random nanoid can end in "-", which backtracks the trailing \b expect(redact("keep it: https://taleseal.com/claim/tc_9hXk2mQ7RfLpZw4TnB8eVs6yUdCa01Gt")).toBe( "keep it: https://taleseal.com/claim/[REDACTED:taleseal-claim]", ); expect(redact("claim secret tc_V4nT8kRw2bYq6mXzCe0uJdHs")).toBe("claim secret [REDACTED:taleseal-claim]"); }); test("passwords inside connection strings", () => { expect(redact("postgresql://neon:hunter2@ep-x.aws.neon.tech/db")).toBe( "postgresql://neon:[REDACTED:connection-password]@ep-x.aws.neon.tech/db", ); // DATABASE_URL is not a credential-named var, so the connection rule carries it: // the password dies, the host stays — which is what a reader needs to see anyway. expect(redact("DATABASE_URL=postgres://u:p4ss@host:5432/x")).toBe( "DATABASE_URL=postgres://u:[REDACTED:connection-password]@host:5432/x", ); }); test("no false positives on prose", () => { const prose = "The token budget was fine; we discussed the secret to good tests and the password policy. Skip the risky bits."; expect(redact(prose)).toBe(prose); expect(redact("The AKIA prefix marks AWS keys")).toBe("The AKIA prefix marks AWS keys"); expect(redact("Bearer of good news")).toBe("Bearer of good news"); }); }); describe("redactDeep", () => { test("scrubs every string field of a JSON-shaped value, at any depth", () => { const scrubbed = redactDeep({ title: "use AKIAAAAABBBBCCCCDDDD please", blocks: [ { kind: "prose", markdown: "found ghp_abcdefghijklmnopqrst123456", nested: { command: "curl -H 'authorization: Bearer tsk_live_0123456789abcdef' https://api.example.com", files: ["/home/dev/secret=hunter2.txt"], url: "https://example.com/?token=deadbeef", output: "xoxb-1234567890-abcdefgh", }, }, ], }); expect(JSON.stringify(scrubbed)).not.toMatch(/AKIAAAAABBBBCCCCDDDD|ghp_|hunter2|xoxb-|tsk_live_|deadbeef/); }); });