|
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
|
|
} from "@oh-my-pi/pi-coding-agent/secrets";
|
|
|
|
|
import {
|
|
|
|
|
deobfuscateAgentMessages,
|
|
|
|
|
deobfuscateSessionContext,
|
|
|
|
|
deobfuscateToolArguments,
|
|
|
|
|
obfuscateMessages,
|
|
|
|
|
obfuscateProviderContext,
|
|
|
|
@@ -313,7 +312,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const input = `use ${secret} now`;
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/#GITHUBTOKEN_[A-Z0-9]+:L#/);
|
|
|
|
|
expect(obfuscated).toMatch(/\$\$GITHUBTOKEN_[A-Z0-9]+:L\$\$/);
|
|
|
|
|
expect(obfuscated).not.toContain(secret);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
@@ -335,7 +334,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const collidingObfuscated = collidingObfuscator.obfuscate(collidingSecret);
|
|
|
|
|
|
|
|
|
|
expect(collidingObfuscated).not.toMatch(/GITHUBPATABC123_/);
|
|
|
|
|
expect(collidingObfuscated).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(collidingObfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(collidingObfuscator.deobfuscate(collidingObfuscated)).toBe(collidingSecret);
|
|
|
|
|
|
|
|
|
|
const distinctSecret = "github_pat_xyz789";
|
|
|
|
@@ -344,7 +343,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
]);
|
|
|
|
|
const distinctObfuscated = distinctObfuscator.obfuscate(distinctSecret);
|
|
|
|
|
|
|
|
|
|
expect(distinctObfuscated).toMatch(/^#GITHUBTOKEN_[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(distinctObfuscated).toMatch(/^\$\$GITHUBTOKEN_[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(distinctObfuscator.deobfuscate(distinctObfuscated)).toBe(distinctSecret);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -369,7 +368,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(longSecret);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toMatch(/GITHUBPAT/);
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(longSecret);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -389,7 +388,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(longSecret);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toContain(leakedPrefix);
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(longSecret);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -399,7 +398,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const input = `use ${secret} please`;
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/#APIKEY_[A-Z0-9]+:L#/);
|
|
|
|
|
expect(obfuscated).toMatch(/\$\$APIKEY_[A-Z0-9]+:L\$\$/);
|
|
|
|
|
expect(obfuscated).not.toContain(secret);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
@@ -424,7 +423,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toMatch(/TOKABC123_/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]+:L# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]+:L\$\$ now$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -445,7 +444,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toMatch(/TOKABC123_/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]+:L# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]+:L\$\$ now$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -467,7 +466,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(obfuscated).not.toContain("TOKABC123_");
|
|
|
|
|
expect(obfuscated).not.toContain("zeta_secret1");
|
|
|
|
|
expect(obfuscated).not.toContain("tok_abc123");
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]{4,}(?::[ULCM])?# and #[A-Z0-9]{4,}(?::[ULCM])?# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ and \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ now$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -492,7 +491,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(obfuscated).not.toContain("TOKABC123_");
|
|
|
|
|
expect(obfuscated).not.toContain("zeta_secret1");
|
|
|
|
|
expect(obfuscated).not.toContain("tok_abc123");
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]{4,}(?::[ULCM])?# and #[A-Z0-9]{4,}(?::[ULCM])?# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ and \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ now$/);
|
|
|
|
|
// Deobfuscation restores the two obfuscate-mode (regex-discovered)
|
|
|
|
|
// placeholders to the values that were actually matched — the replace-
|
|
|
|
|
// produced `tok_abc123` and the raw `zeta_secret1` — but the one-way
|
|
|
|
@@ -522,7 +521,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(obfuscated).not.toContain("TOKABC123_");
|
|
|
|
|
expect(obfuscated).not.toContain("zeta_secret1");
|
|
|
|
|
expect(obfuscated).not.toContain("tok_abc123");
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]{4,}(?::[ULCM])?# and #[A-Z0-9]{4,}(?::[ULCM])?# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ and \$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$ now$/);
|
|
|
|
|
// Deobfuscation restores the two obfuscate-mode (regex-discovered)
|
|
|
|
|
// placeholders to the values that were actually matched — the
|
|
|
|
|
// regex-replace-produced `tok_abc123` and the raw `zeta_secret1` — but
|
|
|
|
@@ -556,7 +555,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(obfuscated).not.toContain("TOKABC123_");
|
|
|
|
|
expect(obfuscated).not.toContain("OTHERSECRET");
|
|
|
|
|
expect(obfuscated).not.toContain("tok_abc123");
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]{4,}:U# and #[A-Z0-9]{4,}:L# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]{4,}:U\$\$ and \$\$[A-Z0-9]{4,}:L\$\$ now$/);
|
|
|
|
|
// Deobfuscation restores the plain secret's placeholder to `OTHERSECRET`
|
|
|
|
|
// and the regex-discovered placeholder to the value actually matched
|
|
|
|
|
// (`tok_abc123`); the one-way regex replace mapping never restores `X`.
|
|
|
|
@@ -590,7 +589,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(obfuscated).not.toContain("TOKABC123_");
|
|
|
|
|
expect(obfuscated).not.toContain("OTHERSECRET");
|
|
|
|
|
expect(obfuscated).not.toContain("tok_abc123");
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]{4,}:U# and #[A-Z0-9]{4,}:L# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]{4,}:U\$\$ and \$\$[A-Z0-9]{4,}:L\$\$ now$/);
|
|
|
|
|
// Deobfuscation restores the plain secret's placeholder to `OTHERSECRET`
|
|
|
|
|
// and the regex-discovered placeholder to the value actually matched
|
|
|
|
|
// (`tok_abc123`); the default `X` -> `Z` hop is one-way, so neither `X`
|
|
|
|
@@ -603,10 +602,10 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "OTHERSECRET", friendlyName: "TOKABC123" },
|
|
|
|
|
{ type: "regex", content: "tok_[a-z0-9]+" },
|
|
|
|
|
]);
|
|
|
|
|
const stalePlaceholder = "#TOKABC123_OLDHASH:L#";
|
|
|
|
|
const stalePlaceholder = "$$TOKABC123_OLDHASH:L$$";
|
|
|
|
|
|
|
|
|
|
expect(obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(stalePlaceholder, new Set(["tok_abc123"]))).toBe(
|
|
|
|
|
"#OLDHASH:L#",
|
|
|
|
|
"$$OLDHASH:L$$",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
it("strips unsafe prefixes from overlapping historical placeholders", () => {
|
|
|
|
@@ -614,10 +613,10 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "OTHERSECRET", friendlyName: "TOKABC123" },
|
|
|
|
|
{ type: "regex", content: "tok_[a-z0-9]+" },
|
|
|
|
|
]);
|
|
|
|
|
const stalePlaceholders = "#FOOO#TOKABC123_OLDHASH:L#";
|
|
|
|
|
const stalePlaceholders = "$$FOOO$$$$TOKABC123_OLDHASH:L$$";
|
|
|
|
|
|
|
|
|
|
expect(obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(stalePlaceholders, new Set(["tok_abc123"]))).toBe(
|
|
|
|
|
"#FOOO#OLDHASH:L#",
|
|
|
|
|
"$$FOOO$$$$OLDHASH:L$$",
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -644,9 +643,9 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// Turn 1: tok_abc123 has not appeared anywhere yet, so "TOKABC123" is not
|
|
|
|
|
// a collision — the friendly prefix is applied and persisted into history.
|
|
|
|
|
const turn1 = obfuscator.obfuscate("use OTHERSECRET now");
|
|
|
|
|
expect(turn1).toMatch(/^use #TOKABC123_[A-Z0-9]+:U# now$/);
|
|
|
|
|
const oldPlaceholder = turn1.match(/#TOKABC123_[A-Z0-9]+:U#/)![0];
|
|
|
|
|
const bareAlias = oldPlaceholder.replace(/^#TOKABC123_/, "#");
|
|
|
|
|
expect(turn1).toMatch(/^use \$\$TOKABC123_[A-Z0-9]+:U\$\$ now$/);
|
|
|
|
|
const oldPlaceholder = turn1.match(/\$\$TOKABC123_[A-Z0-9]+:U\$\$/)![0];
|
|
|
|
|
const bareAlias = oldPlaceholder.replace(/^\$\$TOKABC123_/, "$$");
|
|
|
|
|
|
|
|
|
|
// Turn 2: the already-obfuscated turn-1 output re-enters as prior history
|
|
|
|
|
// alongside NEW text that reveals tok_abc123 — a regex-protected value that
|
|
|
|
@@ -677,7 +676,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const input = `${longSecret} ${prefixSecret}`;
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#TOKEN_[A-Z0-9]+:L# #[A-Z0-9]+:U#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$TOKEN_[A-Z0-9]+:L\$\$ \$\$[A-Z0-9]+:U\$\$$/);
|
|
|
|
|
expect(obfuscated).not.toContain(longSecret);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(input);
|
|
|
|
|
});
|
|
|
|
@@ -688,7 +687,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(`value ${secret}`);
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toContain(secret);
|
|
|
|
|
expect(obfuscated).toMatch(/^value #[A-Z0-9]+:U#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^value \$\$[A-Z0-9]+:U\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe(`value ${secret}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -712,7 +711,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("api_key=abcdefghXYZ");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#APIKEY_[A-Z0-9]+:M#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$APIKEY_[A-Z0-9]+:M\$\$$/);
|
|
|
|
|
expect(obfuscated).not.toContain("abcdefgh");
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("api_key=abcdefghXYZ");
|
|
|
|
|
});
|
|
|
|
@@ -725,7 +724,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("api_key=abcdefghXYZ");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#APIKEY_[A-Z0-9]+:M#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$APIKEY_[A-Z0-9]+:M\$\$$/);
|
|
|
|
|
expect(obfuscated).not.toContain("abcdefgh");
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("api_key=abcdefghXYZ");
|
|
|
|
|
});
|
|
|
|
@@ -1323,7 +1322,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const key = "Q".repeat(43);
|
|
|
|
|
const discoveryObf = new SecretObfuscator([{ type: "plain", content: "ABCDEFGH" }], key);
|
|
|
|
|
const collidingPlaceholder = discoveryObf.obfuscate("ABCDEFGH");
|
|
|
|
|
expect(collidingPlaceholder).toMatch(/^#[A-Z0-9]+(?::[ULCM])?#$/);
|
|
|
|
|
expect(collidingPlaceholder).toMatch(/^\$\$[A-Z0-9]+(?::[ULCM])?\$\$$/);
|
|
|
|
|
|
|
|
|
|
for (const entries of [
|
|
|
|
|
[
|
|
|
|
@@ -1398,21 +1397,14 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(after.obfuscate(persisted)).toBe(persisted);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not canonicalize literal placeholder aliases inside regex matches", () => {
|
|
|
|
|
const sharedKey = "F".repeat(43);
|
|
|
|
|
const plain = new SecretObfuscator([{ type: "plain", content: "legacy-secret" }], sharedKey);
|
|
|
|
|
expect(plain.deobfuscateStored("#XRRS#")).toBe("legacy-secret");
|
|
|
|
|
|
|
|
|
|
it("redacts literal hash-delimited text inside regex matches", () => {
|
|
|
|
|
const obfuscator = new SecretObfuscator(
|
|
|
|
|
[
|
|
|
|
|
{ type: "plain", content: "legacy-secret" },
|
|
|
|
|
{ type: "regex", content: "api_key=\\S+", friendlyName: "api-key" },
|
|
|
|
|
],
|
|
|
|
|
sharedKey,
|
|
|
|
|
[{ type: "regex", content: "api_key=\\S+", friendlyName: "api-key" }],
|
|
|
|
|
"F".repeat(43),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("api_key=#XRRS#");
|
|
|
|
|
expect(obfuscated).toMatch(/^#APIKEY_[A-Z0-9]+(?::[ULCM])?#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$APIKEY_[A-Z0-9]+(?::[ULCM])?\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("api_key=#XRRS#");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -1427,7 +1419,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("SECRETUVX1");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:U#REDACTED$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:U\$\$REDACTED$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/X1$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("SECRETUVREDACTED");
|
|
|
|
|
});
|
|
|
|
@@ -1446,7 +1438,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// The 8-char SECRETUVX1 redacts to one placeholder + REDACTED; assert the `X1`
|
|
|
|
|
// suffix is gone via end-anchored structure, not substring absence — the
|
|
|
|
|
// random keyed base can itself contain the two chars "X1".
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:U#REDACTED$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:U\$\$REDACTED$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/X1$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("SECRETUVREDACTED");
|
|
|
|
|
});
|
|
|
|
@@ -1466,7 +1458,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// it must not be duplicated on both sides of the preserved placeholder (the
|
|
|
|
|
// bug produced `REDACTED#…#REDACTED`). Asserted by structure plus an
|
|
|
|
|
// end-anchored guard rather than a base-collidable substring count.
|
|
|
|
|
expect(obfuscated).toMatch(/^REDACTED#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^REDACTED\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/REDACTED$/);
|
|
|
|
|
expect(obfuscated).not.toContain("api_key=");
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("REDACTEDabcdefgh");
|
|
|
|
@@ -1485,7 +1477,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
"D".repeat(43),
|
|
|
|
|
);
|
|
|
|
|
const replaceOnce = replace.obfuscate("SECRETUVX1");
|
|
|
|
|
expect(replaceOnce).toMatch(/^#[A-Z0-9]+:U#REDACTED$/);
|
|
|
|
|
expect(replaceOnce).toMatch(/^\$\$[A-Z0-9]+:U\$\$REDACTED$/);
|
|
|
|
|
expect(replace.obfuscate(replaceOnce)).toBe(replaceOnce);
|
|
|
|
|
expect(replace.obfuscate(replace.obfuscate(replaceOnce))).toBe(replaceOnce);
|
|
|
|
|
expect(replace.deobfuscate(replaceOnce)).toBe("SECRETUVREDACTED");
|
|
|
|
@@ -1524,7 +1516,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
"G".repeat(43),
|
|
|
|
|
);
|
|
|
|
|
const token = obf.obfuscate("abcdefgh");
|
|
|
|
|
expect(token).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(token).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
|
|
|
|
|
// Input carries the prior token literally AND a fresh api_key=abcdefghXYZ (raw `abc`).
|
|
|
|
|
// The fresh occurrence must still be redacted (XYZ gone) while the prior token is
|
|
|
|
@@ -1622,7 +1614,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
);
|
|
|
|
|
const out = obf.obfuscate("XSECRETUVREDACTED");
|
|
|
|
|
|
|
|
|
|
expect(out).toMatch(/^REDACTED#[A-Z0-9]+:U#REDACTED$/);
|
|
|
|
|
expect(out).toMatch(/^REDACTED\$\$[A-Z0-9]+:U\$\$REDACTED$/);
|
|
|
|
|
expect(obf.obfuscate(out)).toBe(out);
|
|
|
|
|
expect(obf.deobfuscate(out)).toBe("REDACTEDSECRETUVREDACTED");
|
|
|
|
|
});
|
|
|
|
@@ -2069,7 +2061,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
);
|
|
|
|
|
const out = obf.obfuscate(`value=${secret}`);
|
|
|
|
|
expect(out).not.toContain(secret);
|
|
|
|
|
expect(out).not.toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(out).not.toMatch(/\$\$[A-Z0-9]/);
|
|
|
|
|
// An unshadowed obfuscate entry still requires the key.
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey([{ type: "plain", content: secret, mode: "obfuscate" }])).toBe(true);
|
|
|
|
|
// A replace entry with DIFFERENT content does not shadow the obfuscate entry.
|
|
|
|
@@ -2088,7 +2080,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(reintroEntries)).toBe(true);
|
|
|
|
|
const reintroOut = new SecretObfuscator(reintroEntries, "test-placeholder-key").obfuscate(`value=${secret}`);
|
|
|
|
|
expect(reintroOut).toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(reintroOut).toMatch(/\$\$[A-Z0-9]/);
|
|
|
|
|
// Among duplicate same-content replace entries the LAST one wins (the
|
|
|
|
|
// obfuscator stores replace mappings in a content-keyed Map), so a safe earlier
|
|
|
|
|
// duplicate must not mask a later reintroducing one: key is still required.
|
|
|
|
@@ -2099,7 +2091,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(dupReintroLast)).toBe(true);
|
|
|
|
|
expect(new SecretObfuscator(dupReintroLast, "test-placeholder-key").obfuscate(`value=${secret}`)).toMatch(
|
|
|
|
|
/#[A-Z0-9]/,
|
|
|
|
|
/\$\$[A-Z0-9]/,
|
|
|
|
|
);
|
|
|
|
|
// Reverse order: a later safe replacement overrides an earlier reintroducing
|
|
|
|
|
// one, so the obfuscate entry is shadowed and no key is needed.
|
|
|
|
@@ -2110,7 +2102,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(dupSafeLast)).toBe(false);
|
|
|
|
|
expect(new SecretObfuscator(dupSafeLast, "test-placeholder-key").obfuscate(`value=${secret}`)).not.toMatch(
|
|
|
|
|
/#[A-Z0-9]/,
|
|
|
|
|
/\$\$[A-Z0-9]/,
|
|
|
|
|
);
|
|
|
|
|
// A transitive replace chain that rewrites a safe alias back into the secret
|
|
|
|
|
// (`SECRET -> ALIAS`, `ALIAS -> SECRET`) reintroduces the value before the
|
|
|
|
@@ -2123,7 +2115,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(chainReintro)).toBe(true);
|
|
|
|
|
expect(new SecretObfuscator(chainReintro, "test-placeholder-key").obfuscate(`value=${secret}`)).toMatch(
|
|
|
|
|
/#[A-Z0-9]/,
|
|
|
|
|
/\$\$[A-Z0-9]/,
|
|
|
|
|
);
|
|
|
|
|
// A replacement fragment that joins with adjacent passthrough bytes to form an
|
|
|
|
|
// obfuscate content (`A -> SEC`, so `ARET12` becomes `SECRET12` during the
|
|
|
|
@@ -2135,7 +2127,9 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "A", mode: "replace", replacement: "SEC" },
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(fragmentJoin)).toBe(true);
|
|
|
|
|
expect(new SecretObfuscator(fragmentJoin, "test-placeholder-key").obfuscate("x ARET12 y")).toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(new SecretObfuscator(fragmentJoin, "test-placeholder-key").obfuscate("x ARET12 y")).toMatch(
|
|
|
|
|
/\$\$[A-Z0-9]/,
|
|
|
|
|
);
|
|
|
|
|
// A delete replacement also joins the passthrough bytes on both sides of the
|
|
|
|
|
// removed token, so it can reconstruct the obfuscate content even though its
|
|
|
|
|
// replacement output is empty.
|
|
|
|
@@ -2145,7 +2139,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "X", mode: "replace", replacement: "" },
|
|
|
|
|
];
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(deleteJoin)).toBe(true);
|
|
|
|
|
expect(new SecretObfuscator(deleteJoin, "test-placeholder-key").obfuscate("SECRETX12")).toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(new SecretObfuscator(deleteJoin, "test-placeholder-key").obfuscate("SECRETX12")).toMatch(/\$\$[A-Z0-9]/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not require the key when a later replacement erases a content-forming fragment", () => {
|
|
|
|
@@ -2165,7 +2159,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// placeholder, confirming the config is genuinely non-placeholding.
|
|
|
|
|
const out = new SecretObfuscator(entries, "test-placeholder-key").obfuscate("AARET12 and AART12");
|
|
|
|
|
expect(out).not.toContain("SECRET12");
|
|
|
|
|
expect(out).not.toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(out).not.toMatch(/\$\$[A-Z0-9]/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not require the key when a later replacement erases the surrounding context bytes", () => {
|
|
|
|
@@ -2186,7 +2180,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(secretEntriesNeedPlaceholderKey(entries)).toBe(false);
|
|
|
|
|
const out = new SecretObfuscator(entries, "test-placeholder-key").obfuscate("AARET12 and SECRET12");
|
|
|
|
|
expect(out).not.toContain("SECRET12");
|
|
|
|
|
expect(out).not.toMatch(/#[A-Z0-9]/);
|
|
|
|
|
expect(out).not.toMatch(/\$\$[A-Z0-9]/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("redacts a raw sentinel-shaped suffix bridged into a match by a prior placeholder", () => {
|
|
|
|
@@ -2206,7 +2200,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
"R".repeat(43),
|
|
|
|
|
);
|
|
|
|
|
const token = obf.obfuscate("SECRETUV");
|
|
|
|
|
expect(token).toMatch(/^#[A-Z0-9]+:U#$/);
|
|
|
|
|
expect(token).toMatch(/^\$\$[A-Z0-9]+:U\$\$$/);
|
|
|
|
|
|
|
|
|
|
const out = obf.obfuscate(`${token}ZZZZ`);
|
|
|
|
|
|
|
|
|
@@ -2300,7 +2294,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("abcdefgh");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toBe("REDACTED");
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("abcdefgh");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -2312,8 +2306,8 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("abcdefgh");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/^#INNER_/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/^\$\$INNER_/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("abcdefgh");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -2325,8 +2319,8 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("secretuvX");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]+:L#X$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/^#PARTIAL_/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]+:L\$\$X$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/^\$\$PARTIAL_/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("secretuvX");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -2350,7 +2344,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
sharedKey,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(secondPlaceholder).toMatch(/^#OTHER_[A-Z0-9]+(?::[ULCM])?#$/);
|
|
|
|
|
expect(secondPlaceholder).toMatch(/^\$\$OTHER_[A-Z0-9]+(?::[ULCM])?\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(secondPlaceholder)).toBe(firstPlaceholder);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -2364,8 +2358,8 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "alpha-secret" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const firstToken = first.obfuscate("alpha-secret").match(/#[A-Z0-9]+:L#/)?.[0];
|
|
|
|
|
const secondToken = second.obfuscate("alpha-secret").match(/#[A-Z0-9]+:L#/)?.[0];
|
|
|
|
|
const firstToken = first.obfuscate("alpha-secret").match(/\$\$[A-Z0-9]+:L\$\$/)?.[0];
|
|
|
|
|
const secondToken = second.obfuscate("alpha-secret").match(/\$\$[A-Z0-9]+:L\$\$/)?.[0];
|
|
|
|
|
|
|
|
|
|
expect(firstToken).toBeDefined();
|
|
|
|
|
expect(firstToken).toBe(secondToken);
|
|
|
|
@@ -2373,60 +2367,12 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
expect(first.deobfuscate(firstToken ?? "")).toBe("alpha-secret");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps legacy aliases aligned with formerly obfuscated secrets", () => {
|
|
|
|
|
const obfuscator = new SecretObfuscator([
|
|
|
|
|
{ type: "plain", content: "abc" },
|
|
|
|
|
{ type: "plain", content: "MYSECRET123" },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(obfuscator.deobfuscateStored("#XRRS#")).toBe("MYSECRET123");
|
|
|
|
|
expect(obfuscator.deobfuscateStored("#NTJ5#")).toBe("#NTJ5#");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("honors legacy index-derived aliases only on the stored-replay path", () => {
|
|
|
|
|
it("leaves hash-delimited tokens inert while restoring current placeholders", () => {
|
|
|
|
|
const obfuscator = new SecretObfuscator([{ type: "plain", content: "legacy-secret" }]);
|
|
|
|
|
|
|
|
|
|
// The generated token is keyed, never the legacy index token.
|
|
|
|
|
expect(obfuscator.obfuscate("legacy-secret")).not.toBe("#XRRS#");
|
|
|
|
|
|
|
|
|
|
// Stored session replay/display restores pre-keyed legacy placeholders so
|
|
|
|
|
// older persisted sessions still resume correctly.
|
|
|
|
|
expect(obfuscator.deobfuscateStored("#XRRS#")).toBe("legacy-secret");
|
|
|
|
|
|
|
|
|
|
// Live provider output and tool-call arguments MUST NOT honor the legacy
|
|
|
|
|
// alias: it is unkeyed and trivially guessable, so a prompt-injected model
|
|
|
|
|
// could synthesize `#XRRS#` in a bash/read argument and exfiltrate the secret.
|
|
|
|
|
expect(obfuscator.deobfuscate("#XRRS#")).toBe("#XRRS#");
|
|
|
|
|
expect(obfuscator.deobfuscateObject({ cmd: "cat #XRRS#" })).toEqual({ cmd: "cat #XRRS#" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("restores keyed placeholders but never legacy aliases on agent-feeding replay", () => {
|
|
|
|
|
// deobfuscateSessionContext has two kinds of consumers: agent-feeding paths
|
|
|
|
|
// (resume, history rewrite, branch switch) whose output is re-obfuscated and
|
|
|
|
|
// sent to the provider, and a display-only transcript (allowLegacyAliases).
|
|
|
|
|
// Legacy index-derived `#XRRS#` aliases are unkeyed and guessable, so a
|
|
|
|
|
// prompt-injected model can plant one in ANY record it influences — its own
|
|
|
|
|
// assistant output OR a tool result (bash stdout). If a feed path restored
|
|
|
|
|
// it, the next provider turn would re-obfuscate it into a usable keyed
|
|
|
|
|
// placeholder the model could weaponize in a tool argument. So feed paths
|
|
|
|
|
// restore keyed placeholders ONLY; legacy is restored solely for the
|
|
|
|
|
// never-re-sent transcript so pre-keyed sessions still render their secrets.
|
|
|
|
|
const obfuscator = new SecretObfuscator([{ type: "plain", content: "legacy-secret" }]);
|
|
|
|
|
const keyedToken = obfuscator.obfuscate("legacy-secret");
|
|
|
|
|
expect(keyedToken).not.toContain("#XRRS#");
|
|
|
|
|
|
|
|
|
|
const assistant: Message = {
|
|
|
|
|
const current = obfuscator.obfuscate("legacy-secret");
|
|
|
|
|
const message: AgentMessage = {
|
|
|
|
|
role: "assistant",
|
|
|
|
|
content: [
|
|
|
|
|
{ type: "text", text: `attacker planted #XRRS# and echoed ${keyedToken}` },
|
|
|
|
|
{
|
|
|
|
|
type: "toolCall",
|
|
|
|
|
id: "call-1",
|
|
|
|
|
name: "read",
|
|
|
|
|
arguments: { note: keyedToken },
|
|
|
|
|
intent: `intent ${keyedToken}`,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
content: [{ type: "text", text: `legacy #XRRS# current ${current}` }],
|
|
|
|
|
api: "anthropic-messages",
|
|
|
|
|
provider: "anthropic",
|
|
|
|
|
model: "test-model",
|
|
|
|
@@ -2441,89 +2387,20 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
stopReason: "stop",
|
|
|
|
|
timestamp: 1,
|
|
|
|
|
};
|
|
|
|
|
const toolResult: Message = {
|
|
|
|
|
role: "toolResult",
|
|
|
|
|
toolCallId: "call-1",
|
|
|
|
|
toolName: "bash",
|
|
|
|
|
content: [{ type: "text", text: "bash stdout #XRRS#" }],
|
|
|
|
|
isError: false,
|
|
|
|
|
timestamp: 2,
|
|
|
|
|
};
|
|
|
|
|
const branchSummary: AgentMessage = {
|
|
|
|
|
role: "branchSummary",
|
|
|
|
|
summary: `branch #XRRS# and echoed ${keyedToken}`,
|
|
|
|
|
fromId: "branch-1",
|
|
|
|
|
timestamp: 3,
|
|
|
|
|
};
|
|
|
|
|
const compactionSummary: AgentMessage = {
|
|
|
|
|
role: "compactionSummary",
|
|
|
|
|
summary: `compaction #XRRS# and echoed ${keyedToken}`,
|
|
|
|
|
tokensBefore: 0,
|
|
|
|
|
timestamp: 4,
|
|
|
|
|
};
|
|
|
|
|
const contextMessages: AgentMessage[] = [
|
|
|
|
|
assistant as AgentMessage,
|
|
|
|
|
toolResult as AgentMessage,
|
|
|
|
|
branchSummary,
|
|
|
|
|
compactionSummary,
|
|
|
|
|
];
|
|
|
|
|
const ctx = {
|
|
|
|
|
messages: contextMessages,
|
|
|
|
|
models: {},
|
|
|
|
|
injectedTtsrRules: [],
|
|
|
|
|
selectedMCPToolNames: [],
|
|
|
|
|
hasPersistedMCPToolSelection: false,
|
|
|
|
|
mode: "none",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Agent-feeding default restores keyed placeholders authored by this
|
|
|
|
|
// obfuscator but leaves a prompt-injected legacy alias inert before the
|
|
|
|
|
// next provider turn.
|
|
|
|
|
const fed = deobfuscateSessionContext(ctx, obfuscator);
|
|
|
|
|
const fedAssistant = (fed.messages[0] as Extract<Message, { role: "assistant" }>).content[0] as { text: string };
|
|
|
|
|
const fedTool = (fed.messages[1] as Extract<Message, { role: "toolResult" }>).content[0] as { text: string };
|
|
|
|
|
expect(fedAssistant.text).toBe("attacker planted #XRRS# and echoed legacy-secret");
|
|
|
|
|
const fedCall = (fed.messages[0] as AssistantMessage).content[1] as {
|
|
|
|
|
arguments: Record<string, unknown>;
|
|
|
|
|
intent?: string;
|
|
|
|
|
};
|
|
|
|
|
expect(fedCall.arguments).toEqual({ note: "legacy-secret" });
|
|
|
|
|
expect(fedCall.intent).toBe("intent legacy-secret");
|
|
|
|
|
expect(fedTool.text).toBe("bash stdout #XRRS#");
|
|
|
|
|
const fedBranch = fed.messages[2] as Extract<AgentMessage, { role: "branchSummary" }>;
|
|
|
|
|
const fedCompaction = fed.messages[3] as Extract<AgentMessage, { role: "compactionSummary" }>;
|
|
|
|
|
expect(fedBranch.summary).toBe("branch #XRRS# and echoed legacy-secret");
|
|
|
|
|
expect(fedCompaction.summary).toBe("compaction #XRRS# and echoed legacy-secret");
|
|
|
|
|
const replayed = obfuscateMessages(obfuscator, [fed.messages[0] as Message]);
|
|
|
|
|
const replayedAssistant = replayed[0] as Extract<Message, { role: "assistant" }>;
|
|
|
|
|
const replayedText = replayedAssistant.content[0] as { text: string };
|
|
|
|
|
expect(replayedText.text).toBe(`attacker planted #XRRS# and echoed ${keyedToken}`);
|
|
|
|
|
const replayedCall = replayedAssistant.content[1] as { arguments: Record<string, unknown>; intent?: string };
|
|
|
|
|
expect(replayedCall.arguments).toEqual({ note: keyedToken });
|
|
|
|
|
expect(replayedCall.intent).toBe(`intent ${keyedToken}`);
|
|
|
|
|
|
|
|
|
|
// Display-only transcript: legacy aliases ARE restored so a genuinely
|
|
|
|
|
// pre-keyed session renders its secrets. This output is never re-obfuscated.
|
|
|
|
|
const shown = deobfuscateSessionContext(ctx, obfuscator, true);
|
|
|
|
|
const shownAssistant = (shown.messages[0] as Extract<Message, { role: "assistant" }>).content[0] as {
|
|
|
|
|
text: string;
|
|
|
|
|
};
|
|
|
|
|
const shownTool = (shown.messages[1] as Extract<Message, { role: "toolResult" }>).content[0] as { text: string };
|
|
|
|
|
expect(shownAssistant.text).toBe("attacker planted legacy-secret and echoed legacy-secret");
|
|
|
|
|
expect(shownTool.text).toBe("bash stdout #XRRS#");
|
|
|
|
|
const shownBranch = shown.messages[2] as Extract<AgentMessage, { role: "branchSummary" }>;
|
|
|
|
|
const shownCompaction = shown.messages[3] as Extract<AgentMessage, { role: "compactionSummary" }>;
|
|
|
|
|
expect(shownBranch.summary).toBe("branch legacy-secret and echoed legacy-secret");
|
|
|
|
|
expect(shownCompaction.summary).toBe("compaction legacy-secret and echoed legacy-secret");
|
|
|
|
|
const restored = deobfuscateAgentMessages(obfuscator, [message])[0];
|
|
|
|
|
if (restored?.role !== "assistant") throw new Error("expected restored assistant message");
|
|
|
|
|
const text = restored.content[0];
|
|
|
|
|
expect(text?.type === "text" && text.text).toBe("legacy #XRRS# current legacy-secret");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("deobfuscates placeholders after friendlyName changes", () => {
|
|
|
|
|
const renamed = new SecretObfuscator([{ type: "plain", content: "renamed-secret", friendlyName: "new name" }]);
|
|
|
|
|
const current = renamed.obfuscate("renamed-secret");
|
|
|
|
|
const oldName = current.replace("#NEWNAME_", "#OLDNAME_");
|
|
|
|
|
const oldName = current.replace("$$NEWNAME_", () => "$$OLDNAME_");
|
|
|
|
|
const removedName = new SecretObfuscator([{ type: "plain", content: "renamed-secret" }]);
|
|
|
|
|
|
|
|
|
|
expect(current).toMatch(/^#NEWNAME_[A-Z0-9]+:L#$/);
|
|
|
|
|
expect(current).toMatch(/^\$\$NEWNAME_[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
expect(renamed.deobfuscate(oldName)).toBe("renamed-secret");
|
|
|
|
|
expect(removedName.deobfuscate(oldName)).toBe("renamed-secret");
|
|
|
|
|
});
|
|
|
|
@@ -2537,13 +2414,12 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const [tokenA, tokenB] = obfuscated.split(" ");
|
|
|
|
|
if (!tokenA || !tokenB) throw new Error("expected two friendly placeholders");
|
|
|
|
|
|
|
|
|
|
expect(tokenA).toMatch(/^#ALPHA_[A-Z0-9]+:M#$/);
|
|
|
|
|
expect(tokenB).toMatch(/^#BRAVO_[A-Z0-9]+:M#$/);
|
|
|
|
|
expect(tokenA).toMatch(/^\$\$ALPHA_[A-Z0-9]+:M\$\$$/);
|
|
|
|
|
expect(tokenB).toMatch(/^\$\$BRAVO_[A-Z0-9]+:M\$\$$/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("SeCretuv SecRetuv");
|
|
|
|
|
|
|
|
|
|
const stripPrefix = (token: string) => token.replace(/^#[A-Z0-9]+_/, "#");
|
|
|
|
|
const aliasA = stripPrefix(tokenA);
|
|
|
|
|
const aliasB = stripPrefix(tokenB);
|
|
|
|
|
const aliasA = tokenA.replace(/^\$\$[A-Z0-9]+_/, () => "$$");
|
|
|
|
|
const aliasB = tokenB.replace(/^\$\$[A-Z0-9]+_/, () => "$$");
|
|
|
|
|
expect(aliasA).not.toBe(aliasB);
|
|
|
|
|
expect(obfuscator.deobfuscate(aliasA)).toBe("SeCretuv");
|
|
|
|
|
expect(obfuscator.deobfuscate(aliasB)).toBe("SecRetuv");
|
|
|
|
@@ -2592,13 +2468,13 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// name-independent bare-alias suffix (`_<hash>:<hint>#`) that
|
|
|
|
|
// lookupFriendlyPlaceholderAlias also resolves on purpose for deobfuscate().
|
|
|
|
|
const bravoPlaceholder = obfuscator.obfuscate(secretB);
|
|
|
|
|
expect(bravoPlaceholder).toMatch(/^#BRAVO_[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
const aliasSuffix = bravoPlaceholder.replace(/^#BRAVO/, "");
|
|
|
|
|
expect(bravoPlaceholder).toMatch(/^\$\$BRAVO_[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
const aliasSuffix = bravoPlaceholder.replace(/^\$\$BRAVO/, "");
|
|
|
|
|
|
|
|
|
|
// Forge a token shaped exactly like a friendly placeholder for secretB, but
|
|
|
|
|
// with secretA's raw literal value standing in for the friendly name.
|
|
|
|
|
const forgedExact = `#${secretA}${aliasSuffix}`;
|
|
|
|
|
expect(forgedExact).toMatch(/^#[A-Z0-9]+_[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
const forgedExact = `$$${secretA}${aliasSuffix}`;
|
|
|
|
|
expect(forgedExact).toMatch(/^\$\$[A-Z0-9]+_[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
expect(obfuscator.obfuscate(forgedExact)).not.toContain(secretA);
|
|
|
|
|
|
|
|
|
|
// A prefix that merely CONTAINS secretA (not just equals it) must also be
|
|
|
|
@@ -2642,13 +2518,13 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// Learn secretB's real placeholder, then derive the friendly-name-
|
|
|
|
|
// independent bare-alias suffix (`_<hash>:<hint>#`).
|
|
|
|
|
const bravoPlaceholder = obfuscator.obfuscate(secretB);
|
|
|
|
|
expect(bravoPlaceholder).toMatch(/^#BRAVO_[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
const aliasSuffix = bravoPlaceholder.replace(/^#BRAVO/, "");
|
|
|
|
|
expect(bravoPlaceholder).toMatch(/^\$\$BRAVO_[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
const aliasSuffix = bravoPlaceholder.replace(/^\$\$BRAVO/, "");
|
|
|
|
|
|
|
|
|
|
// Forge a token wrapping the REGEX secret's raw literal around secretB's
|
|
|
|
|
// real bare-alias suffix.
|
|
|
|
|
const forgedExact = `#${regexSecret}${aliasSuffix}`;
|
|
|
|
|
expect(forgedExact).toMatch(/^#[A-Z0-9]+_[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
const forgedExact = `$$${regexSecret}${aliasSuffix}`;
|
|
|
|
|
expect(forgedExact).toMatch(/^\$\$[A-Z0-9]+_[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
expect(obfuscator.obfuscate(forgedExact)).not.toContain(regexSecret);
|
|
|
|
|
|
|
|
|
|
// Mixed real + forged in one call: the real secretB placeholder must still
|
|
|
|
@@ -2682,14 +2558,14 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
// registering `tokabc123` (not `TOKABC123`) in `#obfuscateMappings`.
|
|
|
|
|
const real = obf.obfuscate("use tokabc123 now");
|
|
|
|
|
expect(real).not.toContain("tokabc123");
|
|
|
|
|
const suffix = /#([A-Z0-9]{4,}(?::[ULCM])?)#/.exec(real)?.[1];
|
|
|
|
|
const suffix = /\$\$([A-Z0-9]{4,}(?::[ULCM])?)\$\$/.exec(real)?.[1];
|
|
|
|
|
expect(suffix).toBeDefined();
|
|
|
|
|
|
|
|
|
|
// Forge a token wrapping an UPPERCASE variant of the secret around the
|
|
|
|
|
// real bare-alias suffix — differently cased from what was actually
|
|
|
|
|
// discovered, so it cannot match either exact-string check, only the
|
|
|
|
|
// regex pattern itself.
|
|
|
|
|
const forged = `see #TOKABC123_${suffix}# here`;
|
|
|
|
|
const forged = `see $$TOKABC123_${suffix}$$ here`;
|
|
|
|
|
const out = obf.obfuscate(forged);
|
|
|
|
|
expect(out).not.toContain("TOKABC123");
|
|
|
|
|
});
|
|
|
|
@@ -2714,15 +2590,15 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
|
|
|
|
|
|
|
|
|
|
const real = obfuscator.obfuscate(secret);
|
|
|
|
|
const suffix = /#([A-Z0-9]{4,}(?::[ULCM])?)#/.exec(real)?.[1];
|
|
|
|
|
const suffix = /\$\$([A-Z0-9]{4,}(?::[ULCM])?)\$\$/.exec(real)?.[1];
|
|
|
|
|
expect(suffix).toBeDefined();
|
|
|
|
|
|
|
|
|
|
// Forge a prefix that is the sanitized rendering of the SAME configured
|
|
|
|
|
// secret's own value, wrapped around the real bare-alias suffix.
|
|
|
|
|
const forged = `run tool with #GITHUBPATABC123_${suffix}# now`;
|
|
|
|
|
const forged = `run tool with $$GITHUBPATABC123_${suffix}$$ now`;
|
|
|
|
|
const restored = obfuscator.deobfuscate(forged);
|
|
|
|
|
expect(restored).not.toContain(secret);
|
|
|
|
|
expect(restored).toContain("#GITHUBPATABC123_");
|
|
|
|
|
expect(restored).toContain("$$GITHUBPATABC123_");
|
|
|
|
|
|
|
|
|
|
// A genuine rename is unaffected: the OLD friendly-name prefix is not
|
|
|
|
|
// itself secret-shaped, so the bare-alias fallback still resolves it to
|
|
|
|
@@ -2731,9 +2607,9 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "some-other-secret-value", friendlyName: "OldName" },
|
|
|
|
|
]);
|
|
|
|
|
const currentToken = renameObfuscator.obfuscate("some-other-secret-value");
|
|
|
|
|
expect(currentToken).toMatch(/^#OLDNAME_[A-Z0-9]+:L#$/);
|
|
|
|
|
const renameSuffix = currentToken.replace(/^#OLDNAME/, "");
|
|
|
|
|
expect(renameObfuscator.deobfuscate(`#NEWNAME${renameSuffix}`)).toBe("some-other-secret-value");
|
|
|
|
|
expect(currentToken).toMatch(/^\$\$OLDNAME_[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
const renameSuffix = currentToken.replace(/^\$\$OLDNAME/, "");
|
|
|
|
|
expect(renameObfuscator.deobfuscate(`$$NEWNAME${renameSuffix}`)).toBe("some-other-secret-value");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops a friendly name that contains another configured secret's literal value", () => {
|
|
|
|
@@ -2758,11 +2634,11 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("ABCDEFGH");
|
|
|
|
|
expect(obfuscated).not.toContain("LEAKTOKEN");
|
|
|
|
|
// Friendly name dropped for this mint: bare, unprefixed placeholder shape.
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
|
|
|
|
|
// A friendly name that does NOT collide with any live secret is unaffected.
|
|
|
|
|
const safeObfuscated = obfuscator.obfuscate("OTHERSECRETXY");
|
|
|
|
|
expect(safeObfuscated).toMatch(/^#SAFE_[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
expect(safeObfuscated).toMatch(/^\$\$SAFE_[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops a friendly name matched by a later-declared regex secret, regardless of entries[] order", () => {
|
|
|
|
@@ -2783,14 +2659,14 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("ABCDEFGH");
|
|
|
|
|
expect(obfuscated).not.toContain("LEAKTOKEN");
|
|
|
|
|
expect(obfuscated).toMatch(/^#[A-Z0-9]{4,}(?::[ULCM])?#$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^\$\$[A-Z0-9]{4,}(?::[ULCM])?\$\$$/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps a mixed-case placeholder stable when a same-normalized secret is added earlier", () => {
|
|
|
|
|
// Session 1: only SecRet is configured; persist its mixed-case token.
|
|
|
|
|
const before = new SecretObfuscator([{ type: "plain", content: "SecRetuv" }]);
|
|
|
|
|
const persisted = before.obfuscate("SecRetuv");
|
|
|
|
|
expect(persisted).toMatch(/^#[A-Z0-9]+:M#$/);
|
|
|
|
|
expect(persisted).toMatch(/^\$\$[A-Z0-9]+:M\$\$$/);
|
|
|
|
|
|
|
|
|
|
// Session 2: SeCret (same normalized value, also :M) is added EARLIER.
|
|
|
|
|
const after = new SecretObfuscator([
|
|
|
|
@@ -2880,18 +2756,23 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("withholds pending placeholders while streaming provider text", () => {
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #AB12:")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN_")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN_AB12:")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN_AB12:U")).toBe("before ");
|
|
|
|
|
// A lone trailing `#` is buffered even after an alnum/`:` because it can
|
|
|
|
|
// open a new placeholder; emitting it would corrupt the length-sliced draft.
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN_AB12:U#")).toBe("before #TOKEN_AB12:U");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("prefix ID#")).toBe("prefix ID");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("count 42#")).toBe("count 42");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before #TOKEN ")).toBe("before #TOKEN ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$AB12:")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN_")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN_AB12:")).toBe("before ");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN_AB12:U")).toBe("before ");
|
|
|
|
|
// A trailing delimiter character is buffered because it can open an adjacent placeholder.
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN_AB12:U$$")).toBe("before $$TOKEN_AB12:U");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("prefix ID$")).toBe("prefix ID");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("count 42$")).toBe("count 42");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("before $$TOKEN ")).toBe("before $$TOKEN ");
|
|
|
|
|
// A single `$` followed by non-`$` can never open a `$$…$$` placeholder, so it
|
|
|
|
|
// must stream through instead of being withheld until the next boundary.
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("run $HOME")).toBe("run $HOME");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("pay $100")).toBe("pay $100");
|
|
|
|
|
expect(stripPendingSecretPlaceholderSuffix("cost $$100")).toBe("cost ");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("uses independent bases across casing variants with distinct hints", () => {
|
|
|
|
@@ -2901,17 +2782,17 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "Secretuv", friendlyName: "token" },
|
|
|
|
|
]);
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("secretuv SECRETUV Secretuv");
|
|
|
|
|
const tokens = obfuscated.match(/#TOKEN_[A-Z0-9]+:[ULCM]#/g);
|
|
|
|
|
const tokens = obfuscated.match(/\$\$TOKEN_[A-Z0-9]+:[ULCM]\$\$/g);
|
|
|
|
|
if (!tokens) throw new Error("Expected case-hinted placeholders");
|
|
|
|
|
const bases = tokens.map(token => /^#TOKEN_([A-Z0-9]+):/.exec(token)?.[1]);
|
|
|
|
|
const bases = tokens.map(token => /^\$\$TOKEN_([A-Z0-9]+):/.exec(token)?.[1]);
|
|
|
|
|
|
|
|
|
|
expect(tokens).toHaveLength(3);
|
|
|
|
|
// Distinct ASCII-case variants must NOT share a base: a shared case-folded
|
|
|
|
|
// base would let a provider synthesize a sibling token by swapping the hint.
|
|
|
|
|
expect(new Set(bases).size).toBe(3);
|
|
|
|
|
expect(tokens[0]?.endsWith(":L#")).toBe(true);
|
|
|
|
|
expect(tokens[1]?.endsWith(":U#")).toBe(true);
|
|
|
|
|
expect(tokens[2]?.endsWith(":C#")).toBe(true);
|
|
|
|
|
expect(tokens[0]?.endsWith(":L$$")).toBe(true);
|
|
|
|
|
expect(tokens[1]?.endsWith(":U$$")).toBe(true);
|
|
|
|
|
expect(tokens[2]?.endsWith(":C$$")).toBe(true);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("secretuv SECRETUV Secretuv");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -2931,18 +2812,18 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
// Provider sees only the lowercase placeholder.
|
|
|
|
|
const visible = obfuscator.obfuscate("abc12345");
|
|
|
|
|
expect(visible).toMatch(/^#[A-Z0-9]+:L#$/);
|
|
|
|
|
const base = /^#([A-Z0-9]+):L#$/.exec(visible)?.[1];
|
|
|
|
|
expect(visible).toMatch(/^\$\$[A-Z0-9]+:L\$\$$/);
|
|
|
|
|
const base = /^\$\$([A-Z0-9]+):L\$\$$/.exec(visible)?.[1];
|
|
|
|
|
if (!base) throw new Error("expected a lowercase placeholder base");
|
|
|
|
|
|
|
|
|
|
// The uppercase secret's real token uses an independent base.
|
|
|
|
|
const upperReal = obfuscator.obfuscate("ABC12345");
|
|
|
|
|
expect(upperReal).toMatch(/^#[A-Z0-9]+:U#$/);
|
|
|
|
|
expect(upperReal).not.toBe(`#${base}:U#`);
|
|
|
|
|
expect(upperReal).toMatch(/^\$\$[A-Z0-9]+:U\$\$$/);
|
|
|
|
|
expect(upperReal).not.toBe(`$$${base}:U$$`);
|
|
|
|
|
|
|
|
|
|
// Live deobfuscation of the synthesized sibling token leaves it literal
|
|
|
|
|
// instead of restoring the never-provider-visible uppercase secret.
|
|
|
|
|
const synthesized = `#${base}:U#`;
|
|
|
|
|
const synthesized = `$$${base}:U$$`;
|
|
|
|
|
expect(obfuscator.deobfuscate(synthesized)).toBe(synthesized);
|
|
|
|
|
expect(obfuscator.deobfuscateObject({ cmd: synthesized })).toEqual({ cmd: synthesized });
|
|
|
|
|
// The legitimate visible token still round-trips.
|
|
|
|
@@ -2960,7 +2841,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
]);
|
|
|
|
|
const input = "SeCretuv SecRetuv";
|
|
|
|
|
const obfuscated = obfuscator.obfuscate(input);
|
|
|
|
|
const tokens = obfuscated.match(/#TOKEN_[A-Z0-9]+:M#/g);
|
|
|
|
|
const tokens = obfuscated.match(/\$\$TOKEN_[A-Z0-9]+:M\$\$/g);
|
|
|
|
|
if (!tokens) throw new Error("Expected mixed-case placeholders");
|
|
|
|
|
|
|
|
|
|
expect(tokens).toHaveLength(2);
|
|
|
|
@@ -2975,7 +2856,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
{ type: "plain", content: "second-token", friendlyName: "api" },
|
|
|
|
|
]);
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("first-token second-token");
|
|
|
|
|
const tokens = obfuscated.match(/#API_[A-Z0-9]+:L#/g);
|
|
|
|
|
const tokens = obfuscated.match(/\$\$API_[A-Z0-9]+:L\$\$/g);
|
|
|
|
|
if (!tokens) throw new Error("Expected friendly-name placeholders");
|
|
|
|
|
|
|
|
|
|
expect(tokens).toHaveLength(2);
|
|
|
|
@@ -3009,7 +2890,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
expect(entries).toHaveLength(1);
|
|
|
|
|
expect(entries[0]?.friendlyName).toBeUndefined();
|
|
|
|
|
expect(obfuscated).toMatch(/#[A-Z0-9]+:L#/);
|
|
|
|
|
expect(obfuscated).toMatch(/\$\$[A-Z0-9]+:L\$\$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/_[A-Z0-9]+/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("invalid-friendly-secret");
|
|
|
|
|
} finally {
|
|
|
|
@@ -3035,7 +2916,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
|
|
|
|
|
expect(entries).toHaveLength(1);
|
|
|
|
|
expect(entries[0]?.friendlyName).toBeUndefined();
|
|
|
|
|
expect(obfuscated).toMatch(/#[A-Z0-9]+:L#/);
|
|
|
|
|
expect(obfuscated).toMatch(/\$\$[A-Z0-9]+:L\$\$/);
|
|
|
|
|
expect(obfuscated).not.toMatch(/_[A-Z0-9]+/);
|
|
|
|
|
expect(obfuscator.deobfuscate(obfuscated)).toBe("non-string-friendly-secret");
|
|
|
|
|
} finally {
|
|
|
|
@@ -3091,7 +2972,7 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
|
|
|
|
const obfuscated = obfuscator.obfuscate("use tok_abc123 now");
|
|
|
|
|
|
|
|
|
|
expect(obfuscated).not.toMatch(/TOKABC123_/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use #[A-Z0-9]+:L# now$/);
|
|
|
|
|
expect(obfuscated).toMatch(/^use \$\$[A-Z0-9]+:L\$\$ now$/);
|
|
|
|
|
} finally {
|
|
|
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
@@ -3289,10 +3170,10 @@ describe("SecretObfuscator cross-turn cache stability", () => {
|
|
|
|
|
const minted = obfuscateMessages(obfuscator, [
|
|
|
|
|
{ role: "user", content: "remember OTHERSECRET for later", timestamp: 1 },
|
|
|
|
|
]);
|
|
|
|
|
const mintedMatch = /#TOKABC123_[A-Z0-9]+(?::[ULCM])?#/.exec(userText(minted[0]));
|
|
|
|
|
const mintedMatch = /\$\$TOKABC123_[A-Z0-9]+(?::[ULCM])?\$\$/.exec(userText(minted[0]));
|
|
|
|
|
if (mintedMatch === null) throw new Error("expected turn 1 to mint a friendly-prefixed placeholder");
|
|
|
|
|
const mintedPlaceholder = mintedMatch[0];
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^#TOKABC123_/, "#");
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^\$\$TOKABC123_/, "$$");
|
|
|
|
|
|
|
|
|
|
// Turn 2: assistant history persisted that exact prefixed placeholder
|
|
|
|
|
// verbatim, wrapped in ordinary prose that carries no secret material
|
|
|
|
@@ -3390,10 +3271,10 @@ describe("SecretObfuscator cross-turn cache stability", () => {
|
|
|
|
|
const minted = obfuscateMessages(obfuscator, [
|
|
|
|
|
{ role: "user", content: "remember OTHERSECRET for later", timestamp: 1 },
|
|
|
|
|
]);
|
|
|
|
|
const mintedMatch = /#TOKABC123_[A-Z0-9]+(?::[ULCM])?#/.exec(userText(minted[0]));
|
|
|
|
|
const mintedMatch = /\$\$TOKABC123_[A-Z0-9]+(?::[ULCM])?\$\$/.exec(userText(minted[0]));
|
|
|
|
|
if (mintedMatch === null) throw new Error("expected turn 1 to mint a friendly-prefixed placeholder");
|
|
|
|
|
const mintedPlaceholder = mintedMatch[0];
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^#TOKABC123_/, "#");
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^\$\$TOKABC123_/, "$$");
|
|
|
|
|
|
|
|
|
|
// Turn 2: assistant history persisted that exact prefixed placeholder
|
|
|
|
|
// verbatim inside a toolCall block's arguments, intent, and rawBlock —
|
|
|
|
@@ -3515,10 +3396,10 @@ describe("SecretObfuscator cross-turn cache stability", () => {
|
|
|
|
|
const minted = obfuscateMessages(obfuscator, [
|
|
|
|
|
{ role: "user", content: "remember OTHERSECRET for later", timestamp: 1 },
|
|
|
|
|
]);
|
|
|
|
|
const mintedMatch = /#TOKABC123_[A-Z0-9]+(?::[ULCM])?#/.exec(userText(minted[0]));
|
|
|
|
|
const mintedMatch = /\$\$TOKABC123_[A-Z0-9]+(?::[ULCM])?\$\$/.exec(userText(minted[0]));
|
|
|
|
|
if (mintedMatch === null) throw new Error("expected turn 1 to mint a friendly-prefixed placeholder");
|
|
|
|
|
const mintedPlaceholder = mintedMatch[0];
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^#TOKABC123_/, "#");
|
|
|
|
|
const bareAlias = mintedPlaceholder.replace(/^\$\$TOKABC123_/, "$$");
|
|
|
|
|
|
|
|
|
|
// Turn 2: assistant history persisted that exact prefixed placeholder
|
|
|
|
|
// verbatim inside a thinking block, wrapped in ordinary raw reasoning
|
|
|
|
@@ -3673,7 +3554,8 @@ describe("deobfuscateAgentMessages (display restore)", () => {
|
|
|
|
|
timestamp: 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [restored] = deobfuscateAgentMessages(obfuscator, [message], true) as [typeof message];
|
|
|
|
|
const restored = deobfuscateAgentMessages(obfuscator, [message])[0];
|
|
|
|
|
if (restored?.role !== "compactionSummary") throw new Error("expected restored compaction summary");
|
|
|
|
|
const blocks = restored.blocks ?? [];
|
|
|
|
|
const text = blocks[0];
|
|
|
|
|
const image = blocks[1];
|
|
|
|
|