d0a3b01669b4

route shortcut and swipe password copies through canary reporting and bump v5.0.76

time
2026-09-03 22:43 UTC · 1 month ago
machine
koh2
agent
claude-sonnet-4-6
session
PB-26-308 canary copy paths
parent
637ebc6baf50
8 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/App.tsx+10 −2src/popup/copy.ts+19 −2src/popup/views/Entry.tsx+6 −16src/popup/views/Vault.tsx+5 −1src/popup/views/contracts.test.tsx+66 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.75",4+  "version": "5.0.76",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.0.75",5+  "version": "5.0.76",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.0.75",3+  "version": "5.0.76",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/popup/App.tsx+10 −2
@@ -791,11 +791,19 @@791791     const SHORTCUT_HANDLERS: Partial<Record<ShortcutAction, () => void>> = {792792       copy_password: () => {793793         const entry = selectedEntry();794-        if (entry?.password) void popupCopy(entry.password, "Password").then((ok) => { if (ok) triggerRing(); });794+        if (entry?.password) void popupCopy(795+          entry.password,796+          "Password",797+          entry.canary?.webhookUrl ? { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title } : undefined,798+        ).then((ok) => { if (ok) triggerRing(); });795799       },796800       copy_username: () => {797801         const entry = selectedEntry();798-        if (entry?.username) void popupCopy(entry.username, "Username").then((ok) => { if (ok) triggerRing(); });802+        if (entry?.username) void popupCopy(803+          entry.username,804+          "Username",805+          entry.canary?.webhookUrl ? { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title } : undefined,806+        ).then((ok) => { if (ok) triggerRing(); });799807       },800808       copy_totp: () => {801809         const entry = selectedEntry();

view file

src/popup/copy.ts+19 −2
@@ -11,9 +11,14 @@1111  * auto-clear timer.1212  */1313 14+import type { Canary } from "../shared/types";1415 import { sendMessage } from "./App";1516 16-export async function popupCopy(text: string, label: string): Promise<boolean> {17+export async function popupCopy(18+  text: string,19+  label: string,20+  canary?: { webhookUrl: Canary["webhookUrl"]; entryTitle: string },21+): Promise<boolean> {1722   let alreadyWritten = false;1823   try {1924     await navigator.clipboard.writeText(text);@@ -27,5 +32,17 @@2732     type: "COPY",2833     payload: { text, label, alreadyWritten },2934   });30-  return alreadyWritten || Boolean((response as { ok?: boolean })?.ok);35+  const ok = alreadyWritten || Boolean((response as { ok?: boolean })?.ok);36+  const canaryLabel = label.toLowerCase();37+  if (ok && canary && (canaryLabel === "password" || canaryLabel === "username")) {38+    void sendMessage({39+      type: "FIRE_CANARY_PING",40+      payload: {41+        webhookUrl: canary.webhookUrl,42+        entryTitle: canary.entryTitle,43+        trigger: canaryLabel === "password" ? "password_copied" : "username_copied",44+      },45+    });46+  }47+  return ok;3148 }

view file

src/popup/views/Entry.tsx+6 −16
@@ -499,23 +499,13 @@499499   // ── Copy ─────────────────────────────────────────────────────────500500 501501   async function copyField(text: string, label: string, button?: HTMLButtonElement) {502-    const ok = await popupCopy(text, label);502+    const entry = props.entry;503+    const ok = await popupCopy(504+      text,505+      label,506+      entry?.canary?.webhookUrl ? { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title } : undefined,507+    );503508     if (ok) {504-      const entry = props.entry;505-      if (entry?.canary?.webhookUrl) {506-        const trigger =507-          label === "password"508-            ? "password_copied"509-            : label === "username"510-              ? "username_copied"511-              : undefined;512-        if (trigger) {513-          void sendMessage({514-            type: "FIRE_CANARY_PING",515-            payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger },516-          });517-        }518-      }519509       triggerRing();520510       triggerPipPulse();521511       setCopied(null);

view file

src/popup/views/Vault.tsx+5 −1
@@ -176,7 +176,11 @@176176       vibrate(15);177177       setFlashId(entry.id);178178       window.setTimeout(() => setFlashId((id) => id === entry.id ? null : id), 600);179-      await popupCopy(entry.password, "password");179+      await popupCopy(180+        entry.password,181+        "password",182+        entry.canary?.webhookUrl ? { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title } : undefined,183+      );180184     }181185   }182186 

view file

src/popup/views/contracts.test.tsx+66 −0
@@ -1235,6 +1235,72 @@12351235     });12361236   });12371237 1238+  it("Entry keyboard password copy reports its canary after COPY", async () => {1239+    sendMessageMock.mockResolvedValue({ ok: true });1240+    mount(1241+      <EntryView1242+        entry={sampleEntry()}1243+        onDelete={vi.fn()}1244+        onFieldSaved={vi.fn(async () => undefined)}1245+      />1246+    );1247+1248+    window.dispatchEvent(new KeyboardEvent("keydown", { key: "c", bubbles: true }));1249+    await flushView();1250+1251+    expect(sendMessageMock).toHaveBeenNthCalledWith(1, {1252+      type: "COPY",1253+      payload: { text: "secret123", label: "password", alreadyWritten: false },1254+    });1255+    await flushView();1256+    expect(sendMessageMock.mock.calls).toContainEqual([{1257+      type: "FIRE_CANARY_PING",1258+      payload: {1259+        webhookUrl: "https://example.com/hook",1260+        entryTitle: "GitHub",1261+        trigger: "password_copied",1262+      },1263+    }]);1264+  });1265+1266+  it("Vault password swipe copy reports its canary after COPY", async () => {1267+    sendMessageMock.mockResolvedValue({ ok: true });1268+    const container = mount(1269+      <VaultView1270+        entries={[sampleEntry()]}1271+        sortOrder="url-first"1272+        onSelectEntry={vi.fn()}1273+        onSortOrderChange={vi.fn()}1274+        onAdd={vi.fn()}1275+        onClose={vi.fn()}1276+      />1277+    );1278+    const row = container.querySelector(".peach-entry-row") as HTMLButtonElement;1279+    row.dispatchEvent(new TouchEvent("touchstart", {1280+      bubbles: true,1281+      touches: [{ clientX: 0, clientY: 0 } as Touch],1282+    }));1283+    row.dispatchEvent(new TouchEvent("touchend", {1284+      bubbles: true,1285+      changedTouches: [{ clientX: 100, clientY: 0 } as Touch],1286+    }));1287+    await flushView();1288+1289+    expect(sendMessageMock).toHaveBeenNthCalledWith(1, {1290+      type: "COPY",1291+      payload: { text: "secret123", label: "password", alreadyWritten: false },1292+    });1293+    await flushView();1294+    expect(sendMessageMock.mock.calls).toContainEqual([{1295+      type: "FIRE_CANARY_PING",1296+      payload: {1297+        webhookUrl: "https://example.com/hook",1298+        entryTitle: "GitHub",1299+        trigger: "password_copied",1300+      },1301+    }]);1302+  });1303+12381304   it("Entry shows Advanced for passkey-bearing credential entries", async () => {12391305     const container = mount(12401306       <EntryView

view file