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
9 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 −1
manifest.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": {

view file

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",

view file

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",

view file

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[] = [];

view file

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       }

view file

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 });

view file

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   }

view file

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 

view file

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() {

view file