diff --git a/src/config/redact-snapshot.test.ts b/src/config/redact-snapshot.test.ts index 88bbd35be5d..7972489c6b5 100644 --- a/src/config/redact-snapshot.test.ts +++ b/src/config/redact-snapshot.test.ts @@ -1163,59 +1163,38 @@ describe("redactConfigSnapshot", () => { expect(channels.slack.accounts[1].botToken).toBe(REDACTED_SENTINEL); }); - it("redacts credentials embedded in browser.cdpUrl (query token and userinfo)", () => { + it("redacts browser cdpUrl secrets while preserving bare endpoints", () => { const hints = buildConfigSchema().uiHints; const raw = `{ browser: { cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123", - }, -}`; - const snapshot = makeSnapshot( - { - browser: { - cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123", - }, - }, - raw, - ); - - const result = redactConfigSnapshot(snapshot, hints); - const cfg = result.config as typeof snapshot.config; - expect(cfg.browser.cdpUrl).toBe(REDACTED_SENTINEL); - expect(result.raw).toContain(REDACTED_SENTINEL); - expect(result.raw).not.toContain("user:pass@"); - expect(result.raw).not.toContain("supersecret123"); - - const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.browser.cdpUrl).toBe( - "https://user:pass@chrome.browserless.io?token=supersecret123", - ); - }); - - it("redacts credentials embedded in browser.profiles.*.cdpUrl", () => { - const hints = buildConfigSchema().uiHints; - const raw = `{ - browser: { profiles: { - staging: { + remote: { cdpUrl: "https://chrome.staging.example.com?token=staging-secret", }, prod: { cdpUrl: "https://alice:secret@chrome.prod.example.com", }, + local: { + cdpUrl: "ws://localhost:9222", + }, }, }, }`; const snapshot = makeSnapshot( { browser: { + cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123", profiles: { - staging: { + remote: { cdpUrl: "https://chrome.staging.example.com?token=staging-secret", }, prod: { cdpUrl: "https://alice:secret@chrome.prod.example.com", }, + local: { + cdpUrl: "ws://localhost:9222", + }, }, }, }, @@ -1224,34 +1203,26 @@ describe("redactConfigSnapshot", () => { const result = redactConfigSnapshot(snapshot, hints); const cfg = result.config as typeof snapshot.config; - expect(cfg.browser.profiles.staging.cdpUrl).toBe(REDACTED_SENTINEL); + expect(cfg.browser.cdpUrl).toBe(REDACTED_SENTINEL); + expect(cfg.browser.profiles.remote.cdpUrl).toBe(REDACTED_SENTINEL); expect(cfg.browser.profiles.prod.cdpUrl).toBe(REDACTED_SENTINEL); + expect(cfg.browser.profiles.local.cdpUrl).toBe("ws://localhost:9222"); + expect(result.raw).toContain(REDACTED_SENTINEL); + expect(result.raw).not.toContain("user:pass@"); + expect(result.raw).not.toContain("supersecret123"); expect(result.raw).not.toContain("staging-secret"); expect(result.raw).not.toContain("alice:secret@"); const restored = restoreRedactedValues(result.config, snapshot.config, hints); - expect(restored.browser.profiles.staging.cdpUrl).toBe( + expect(restored.browser.cdpUrl).toBe( + "https://user:pass@chrome.browserless.io?token=supersecret123", + ); + expect(restored.browser.profiles.remote.cdpUrl).toBe( "https://chrome.staging.example.com?token=staging-secret", ); expect(restored.browser.profiles.prod.cdpUrl).toBe( "https://alice:secret@chrome.prod.example.com", ); - }); - - it("does not redact bare cdpUrl addresses without credentials", () => { - const hints = buildConfigSchema().uiHints; - const snapshot = makeSnapshot({ - browser: { - cdpUrl: "http://10.0.0.42:9222", - profiles: { - local: { cdpUrl: "ws://localhost:9222" }, - }, - }, - }); - - const result = redactConfigSnapshot(snapshot, hints); - const cfg = result.config as typeof snapshot.config; - expect(cfg.browser.cdpUrl).toBe("http://10.0.0.42:9222"); - expect(cfg.browser.profiles.local.cdpUrl).toBe("ws://localhost:9222"); + expect(restored.browser.profiles.local.cdpUrl).toBe("ws://localhost:9222"); }); }); diff --git a/src/gateway/server.config-patch.test.ts b/src/gateway/server.config-patch.test.ts index 4be2cc9e8c4..fc87da639b3 100644 --- a/src/gateway/server.config-patch.test.ts +++ b/src/gateway/server.config-patch.test.ts @@ -157,6 +157,62 @@ describe("gateway config methods", () => { expect(res.payload?.config).toBeTruthy(); }); + it("redacts browser cdpUrl credentials from config.get responses", async () => { + const { createConfigIO, resetConfigRuntimeState } = await import("../config/config.js"); + const configPath = createConfigIO().configPath; + await fs.mkdir(path.dirname(configPath), { recursive: true }); + try { + await fs.writeFile( + configPath, + `${JSON.stringify( + { + browser: { + cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123", + profiles: { + remote: { + cdpUrl: "https://alice:secret@chrome.remote.example.com?token=profile-secret", + }, + local: { + cdpUrl: "ws://127.0.0.1:9222", + }, + }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + resetConfigRuntimeState(); + + const after = await rpcReq<{ + raw?: string | null; + config?: { + browser?: { + cdpUrl?: string; + profiles?: Record; + }; + }; + }>(requireWs(), "config.get", {}); + expect(after.ok).toBe(true); + expect(after.payload?.config?.browser?.cdpUrl).toBe("__OPENCLAW_REDACTED__"); + expect(after.payload?.config?.browser?.profiles?.remote?.cdpUrl).toBe( + "__OPENCLAW_REDACTED__", + ); + expect(after.payload?.config?.browser?.profiles?.local?.cdpUrl).toBe("ws://127.0.0.1:9222"); + if (typeof after.payload?.raw === "string") { + expect(after.payload.raw).toContain("__OPENCLAW_REDACTED__"); + expect(after.payload.raw).not.toContain("supersecret123"); + expect(after.payload.raw).not.toContain("user:pass@"); + expect(after.payload.raw).not.toContain("profile-secret"); + expect(after.payload.raw).not.toContain("alice:secret@"); + } + } finally { + await fs.rm(configPath, { force: true }); + resetConfigRuntimeState(); + } + }); + it("does not reject config.set for unresolved auth-profile refs outside submitted config", async () => { const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_REF_${Date.now()}`; await writeUnresolvedAuthProfileTokenRef(missingEnvVar);