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
↓ peach-browser-d0a3b016.tar.gzatlas at this save
files changed8 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 −0manifest.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": {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",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",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();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 }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);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 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