1eb81220588e

fix: inline popup search reaches full vault + back-arrow returns to silver platter (v3.6.100)

- content/index.ts: openAutofillSelector always fetches the full vault list (includeAll=true), not just domain matches, so search can reach an entry stored under a different URL — the original bug behind the search affordance: it was locked to the same entries the platter showed - background/messages.ts: AUTOFILL_QUERY includeAll response now carries username/email/url alongside id/title/hasPasskey so the inline popup filterEntries() can match more than just titles. Still no secrets cross the boundary - messages.test: update the includeAll case to assert the search-ready summary shape and that password/totp don't appear - content/inject.ts: track silverPlatterOrigin in the showAutofillSelector closure; when the user pivots from a silver platter into the list view, the header now shows a back arrow (where the search icon was) that returns to renderSilverPlatter(silverPlatterOrigin) - inject.ts: add backSvgStr() and reuse the .domain-search-btn shell so the back affordance matches the search button it replaces - Bump package/lockfile/Chrome+Firefox manifests to 3.6.100

time
2026-05-27 16:57 UTC · 5 months ago
machine
koh2
parent
d9877efa8475
8 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/background/messages.test.ts+15 −3src/background/messages.ts+8 −0src/content/index.ts+5 −3src/content/inject.ts+32 −1
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach",5-  "version": "3.6.99",5+  "version": "3.6.100",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.6.99",4+  "version": "3.6.100",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package-lock.json+2 −2
@@ -1,12 +1,12 @@11 {22   "name": "peach",3-  "version": "3.6.99",3+  "version": "3.6.100",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.99",9+      "version": "3.6.100",1010       "dependencies": {1111         "@bokuweb/zstd-wasm": "^0.0.27",1212         "@stablelib/blake2s": "^2.0.1",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.99",3+  "version": "3.6.100",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/messages.test.ts+15 −3
@@ -401,7 +401,7 @@401401     expect((response.data as Array<Record<string, unknown>>)[0].totp).toBeUndefined();402402   });403403 404-  it("AUTOFILL_QUERY includeAll returns only id and title for non-note, non-passkey entries", async () => {404+  it("AUTOFILL_QUERY includeAll returns search-ready summaries for non-note, non-passkey entries", async () => {405405     const vault = await import("./vault");406406     await vault.createVault("correct-pass-12345");407407     await vault.addEntry({@@ -423,9 +423,21 @@423423 424424     expect(response).toMatchObject({425425       ok: true,426-      data: [{ id: expect.any(String), title: "GitHub" }],426+      data: [427+        {428+          id: expect.any(String),429+          title: "GitHub",430+          username: "june",431+          url: "https://github.com",432+          hasPasskey: false,433+        },434+      ],427435     });428-    expect(response.data).toHaveLength(1);436+    const data = response.data as Array<Record<string, unknown>>;437+    expect(data).toHaveLength(1);438+    // No secrets cross the boundary.439+    expect(data[0]).not.toHaveProperty("password");440+    expect(data[0]).not.toHaveProperty("totp");429441   });430442 431443   it("AUTOFILL_QUERY kind=totp returns only matching entries with 2FA summaries", async () => {

view file

src/background/messages.ts+8 −0
@@ -545,6 +545,14 @@545545               ? {546546                   id: e.id,547547                   title: e.title,548+                  // username/email/url shipped so the inline popup's search549+                  // affordance can match on them, not just titles. No secrets550+                  // cross the boundary — same shape as the domain-matched551+                  // response below minus hasTotp (which the search list552+                  // doesn't surface).553+                  username: e.username,554+                  email: e.email,555+                  url: e.url,548556                   hasPasskey: e.passkey !== undefined,549557                 }550558               : {

view file

src/content/index.ts+5 −3
@@ -655,9 +655,11 @@655655     }656656 657657     const entries = firstResult.entries;658-    const allEntries = entries.length === 0659-      ? (await fetchAutofillEntries(domain, true)).entries660-      : entries;658+    // Always fetch the vault-wide list so the inline popup's search affordance659+    // can reach entries that don't match this domain — the user may have a660+    // valid login filed under a different URL (e.g. corp account on a vanity661+    // domain) and still want to fill it here.662+    const allEntries = (await fetchAutofillEntries(domain, true)).entries;661663 662664     if (passkeyToastActive) return;663665 

view file

src/content/inject.ts+32 −1
@@ -95,6 +95,10 @@9595   return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><circle cx="5.25" cy="5.25" r="3.5"/><path d="M7.9 7.9l2.35 2.35"/></svg>`;9696 }9797 98+function backSvgStr(): string {99+  return `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 2.5L3.5 6l4 3.5"/></svg>`;100+}101+98102 function peachIconSvg(): string {99103   // Peach body stays brand-peach regardless of `accentHex` — a sage or100104   // sky accent must not turn the autofill icon green or blue. Accent@@ -1280,6 +1284,10 @@12801284   let focusedIndex = -1;12811285   let currentMode: "list" | "create" | "success" | "silver-platter" = "list";12821286   let searchInput: HTMLInputElement | null = null;1287+  // Tracks the silver-platter entry the user pivoted away from, so the1288+  // search/list header can offer a one-click way back to it. null when the1289+  // popup never showed a single-match platter (e.g. multi-match list view).1290+  let silverPlatterOrigin: SelectorEntry | null = null;12831291   // Hooks any sub-state owns for cleanup at dismiss time. Today: TOTP tray12841292   // setInterval. Add to this list — don't fan out the dismiss function.12851293   const cleanupHooks: Array<() => void> = [];@@ -1582,6 +1590,26 @@15821590     domHdr.appendChild(headText);15831591 15841592     if (!options.isSignup) {1593+      const actions = document.createElement("div");1594+      actions.className = "domain-header-actions";1595+1596+      // Back arrow takes the place of the silver-platter's search icon when1597+      // the user has pivoted away from a single-match platter — gives them1598+      // a one-click way back to the original recommendation.1599+      if (silverPlatterOrigin) {1600+        const backBtn = document.createElement("button");1601+        backBtn.className = "domain-search-btn";1602+        backBtn.type = "button";1603+        backBtn.setAttribute("aria-label", "Back to suggested entry");1604+        backBtn.title = "Back to suggested entry";1605+        backBtn.innerHTML = backSvgStr();1606+        backBtn.addEventListener("click", (e) => {1607+          e.stopPropagation();1608+          if (silverPlatterOrigin) renderSilverPlatter(silverPlatterOrigin);1609+        });1610+        actions.appendChild(backBtn);1611+      }1612+15851613       const addBtn = document.createElement("button");15861614       addBtn.className = "domain-add-btn";15871615       addBtn.type = "button";@@ -1589,7 +1617,9 @@15891617       addBtn.title = "Create new entry";15901618       addBtn.textContent = "+";15911619       addBtn.addEventListener("click", (e) => { e.stopPropagation(); void showCreateForm(); });1592-      domHdr.appendChild(addBtn);1620+      actions.appendChild(addBtn);1621+1622+      domHdr.appendChild(actions);15931623     }15941624 15951625     container.appendChild(domHdr);@@ -1974,6 +2004,7 @@19742004     searchInput = null;19752005     focusedIndex = -1;19762006     currentMode = "silver-platter";2007+    silverPlatterOrigin = entry;19772008 19782009     const domHdr = document.createElement("div");19792010     domHdr.className = "domain-header";

view file