7a0792a9de52
fix: stop echoing raw background errors in Codex and Adopt views (audit PB-26-607, v5.1.16)
- time
- 2026-09-06 15:27 UTC · 1 month ago
- machine
- koh2
- agent
- openrouter/tencent/hy4-preview
- session
- PB-26-601..611 audit closeout
- parent
- b68bdc98f5f1
↓ peach-browser-7a0792a9.tar.gzatlas at this save
files changed9 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/AdoptVaultView.test.tsx+51 −0src/popup/views/AdoptVaultView.tsx+21 −2src/popup/views/CodexGenerateView.test.tsx+24 −0src/popup/views/CodexGenerateView.tsx+6 −4src/popup/views/CodexRestoreView.test.tsx+26 −0src/popup/views/CodexRestoreView.tsx+2 −1manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.1.15",4+ "version": "5.1.16",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.1.15",5+ "version": "5.1.16",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.1.15",3+ "version": "5.1.16",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/popup/views/AdoptVaultView.test.tsx+51 −0
@@ -247,6 +247,57 @@247247 expect(view.container.querySelector('a[href*="buy.polar.sh"]')).toBeTruthy();248248 });249249 250+ it("PB-26-607: never renders a raw background error string", async () => {251+ type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;252+ const listeners: RuntimeListener[] = [];253+ const RAW = "TypeError: cannot read property \"salt\" of undefined at deriveSlotKey (vault.ts:2841)";254+ Object.defineProperty(globalThis, "chrome", {255+ configurable: true,256+ value: {257+ runtime: {258+ lastError: undefined,259+ sendMessage(260+ message: { type?: string },261+ callback: (response: unknown) => void,262+ ) {263+ if (message.type === "START_ADOPTION_RECEIVER") {264+ callback({ ok: false, error: RAW });265+ } else {266+ callback({ ok: true });267+ }268+ },269+ onMessage: {270+ addListener(fn: RuntimeListener) { listeners.push(fn); },271+ removeListener(fn: RuntimeListener) {272+ const index = listeners.indexOf(fn);273+ if (index >= 0) listeners.splice(index, 1);274+ },275+ },276+ },277+ },278+ });279+280+ const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} />);281+ await enterStrongPassword(view.container);282+ button(view.container, "Continue").click();283+ await flushView();284+285+ expect(view.container.textContent).toContain("could not start joining this vault");286+ expect(view.container.textContent).not.toContain(RAW);287+ expect(view.container.textContent).not.toContain("vault.ts");288+289+ // Same guarantee on the result-push path, which switches to the error phase.290+ for (const listener of listeners) {291+ listener({ type: "ADOPTION_SOURCE_RESULT", payload: { status: "error", error: RAW } });292+ }293+ await flushView();294+295+ expect(view.container.textContent).toContain("Could not join the vault.");296+ expect(view.container.textContent).toContain("the other device could not finish the transfer");297+ expect(view.container.textContent).not.toContain(RAW);298+ expect(view.container.textContent).not.toContain("vault.ts");299+ });300+250301 it("continues straight to initial sync when receiver pairing completes", async () => {251302 type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;252303 const listeners: RuntimeListener[] = [];src/popup/views/AdoptVaultView.tsx+21 −2
@@ -21,6 +21,15 @@2121 onDone(): void | Promise<void>;2222 }2323 24+/**25+ * PB-26-607: never echo a raw background error string into the UI.26+ * ADOPT_PRO_REQUIRED survives because the error phase branches on it to show27+ * the Pro upsell instead of the generic failure copy.28+ */29+function describeAdoptionError(error: string | undefined, fallback: string): string {30+ return error === "ADOPT_PRO_REQUIRED" ? error : fallback;31+}32+2433 const MIN_LENGTH = 12;2534 const MIN_ZXCVBN_SCORE = 2;2635 const PAIRING_CELEBRATION_MS = 5_000;@@ -75,7 +84,12 @@7584 if (response.ok && response.data?.invitation) {7685 setReceiverInvitation(response.data.invitation);7786 } else {78- setReceiverError(response.error ?? "adoption_receiver_failed");87+ setReceiverError(88+ describeAdoptionError(89+ response.error,90+ "could not start joining this vault. please try again.",91+ ),92+ );7993 }8094 })();8195 });@@ -116,7 +130,12 @@116130 void props.onDone();117131 }, PAIRING_CELEBRATION_MS);118132 } else if (p?.status === "error") {119- setReceiverError(p.error ?? "adoption_failed");133+ setReceiverError(134+ describeAdoptionError(135+ p.error,136+ "the other device could not finish the transfer. please try again.",137+ ),138+ );120139 setPhase("error");121140 }122141 }src/popup/views/CodexGenerateView.test.tsx+24 −0
@@ -79,4 +79,28 @@7979 expect(mounted.container.textContent).toContain("abandon");8080 expect(mounted.container.textContent).toContain("Failed to load Codex library");8181 });82+83+ it("PB-26-607: never renders a raw background error string", async () => {84+ const RAW = "Error: ENOENT /vaults/slot-b.json at readSlot (vault.ts:884)";85+ sendMessageMock.mockResolvedValue({ ok: false, error: RAW });86+87+ const { CodexGenerateView } = await import("./CodexGenerateView");88+ const mounted = renderView(89+ <CodexGenerateView90+ isPro={true}91+ onBack={vi.fn()}92+ onUpgradePro={vi.fn()}93+ onOpenSecurity={vi.fn()}94+ onVaultLocked={vi.fn()}95+ />,96+ );97+ dispose = mounted.dispose;98+99+ await flushView();100+ await flushView();101+102+ expect(mounted.container.textContent).toContain("could not load vault data");103+ expect(mounted.container.textContent).not.toContain(RAW);104+ expect(mounted.container.textContent).not.toContain("vault.ts");105+ });82106 });src/popup/views/CodexGenerateView.tsx+6 −4
@@ -127,7 +127,8 @@127127 setPasswordError("wrong password. try again.");128128 return;129129 }130- setLoadError(res.error ?? "could not load vault data");130+ // PB-26-607: never echo the raw background error into the UI.131+ setLoadError("could not load vault data");131132 setLoading(false);132133 return;133134 }@@ -255,7 +256,7 @@255256 a.remove();256257 }257258 258- function explainCodexError(code: string, message: string): string {259+ function explainCodexError(code: string): string {259260 switch (code) {260261 case "VAULT_LOCKED":261262 return "your vault is locked. unlock and try again.";@@ -277,7 +278,8 @@277278 case "RECONSTRUCTION_COUNT_MISMATCH":278279 return `a self-check failed while building the Codex (${code}). nothing was saved. please try again.`;279280 default:280- return message || "Codex generation failed.";281+ // PB-26-607: never echo the raw error text into the UI.282+ return "Codex generation failed.";281283 }282284 }283285 @@ -329,7 +331,7 @@329331 const message =330332 err instanceof Error ? err.message : "Codex generation failed.";331333 console.error("[Codex] generation failed:", code, message);332- setGenError(explainCodexError(code, message));334+ setGenError(explainCodexError(code));333335 setStep("words");334336 }335337 }src/popup/views/CodexRestoreView.test.tsx+26 −0
@@ -153,6 +153,32 @@153153 expect(payload.masterPassphrase).toBe(" hunter2 ");154154 });155155 156+ it("PB-26-607: never renders a raw background error string", async () => {157+ const RAW = "TypeError: argon2id derive failed at unlockVault (vault.ts:1712)";158+ sendMessageMock.mockImplementation(async (msg: unknown) => {159+ if ((msg as { type?: string }).type === "RESTORE_FROM_CODEX") {160+ return { ok: false, error: RAW };161+ }162+ return { ok: true, data: { entryCount: 7, verifyCode: VERIFY_CODE } };163+ });164+165+ const view = await mountAndScan();166+ const pwInput = view.container.querySelector<HTMLInputElement>('input[type="password"]')!;167+ pwInput.value = "hunter2";168+ pwInput.dispatchEvent(new Event("input", { bubbles: true }));169+ await flushView();170+171+ const restoreBtn = Array.from(view.container.querySelectorAll("button")).find((b) =>172+ b.textContent?.includes("restore vault"),173+ )!;174+ restoreBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));175+ await waitFor(() => view.container.textContent?.includes("restoration failed") === true);176+177+ expect(view.container.textContent).toContain("restoration failed. please try again.");178+ expect(view.container.textContent).not.toContain(RAW);179+ expect(view.container.textContent).not.toContain("vault.ts");180+ });181+156182 it("blocks an all-whitespace passphrase without dispatching RESTORE_FROM_CODEX", async () => {157183 const view = await mountAndScan();158184 src/popup/views/CodexRestoreView.tsx+2 −1
@@ -330,7 +330,8 @@330330 return "the reconstructed vault is unreadable. please re-scan the Codex.";331331 if (error.includes("FINGERPRINT_MISMATCH"))332332 return "vault integrity check failed. please re-scan the Codex.";333- return error;333+ // PB-26-607: never echo the raw background error into the UI.334+ return "restoration failed. please try again.";334335 }335336 336337 async function handleRestore() {