7834ef7ed3cf

fix: Make explicit login-form icon clicks open Peach (v3.6.146)

- Treat login-field peach clicks as explicit summons - Bypass the brittle passive form/ML gate on that path - Add a regression test for the explicit target resolution

time
2026-05-31 03:46 UTC · 4 months ago
machine
koh2
parent
5ede1338abb9
7 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/content/index.ts+13 −10src/content/interaction.test.ts+62 −0src/content/interaction.ts+30 −0
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 Password Manager",5-  "version": "3.6.145",5+  "version": "3.6.146",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 Password Manager",4-  "version": "3.6.145",4+  "version": "3.6.146",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.104",3+  "version": "3.6.146",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.104",9+      "version": "3.6.146",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.145",3+  "version": "3.6.146",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/content/index.ts+13 −10
@@ -26,6 +26,7 @@2626 import { detectBrowser } from "../shared/browser";2727 import { FIREFOX_USE_CONTENT_SCRIPT_PROXY } from "../shared/passkey-flags";2828 import { fillCardForm, fillFields, fillOTPDigitBoxes, fillTOTPCode, setNativeValue } from "./fill";29+import { resolveAutofillInteractionTargets } from "./interaction";2930 import { setupSaveDetection } from "./save";3031 import { contentMLDetector } from "./content-ml-detector";3132 import { detectPasswordPolicy } from "./policy";@@ -573,7 +574,10 @@573574 }574575 575576 576-async function handleFieldInteraction(anchorField?: HTMLInputElement | null): Promise<void> {577+async function handleFieldInteraction(578+  anchorField?: HTMLInputElement | null,579+  explicitSummon = false580+): Promise<void> {577581   const field = anchorField ?? getDeepActiveInput();578582   if (!field) return;579583   if (!isVisible(field)) return;@@ -639,7 +643,7 @@639643 640644     if (passkeyToastActive) return;641645 642-    if (fieldType !== "password" && mlPrediction && mlPrediction.type !== "username" && mlPrediction.type !== "email") {646+    if (!explicitSummon && fieldType !== "password" && mlPrediction && mlPrediction.type !== "username" && mlPrediction.type !== "email") {643647       return;644648     }645649 @@ -649,7 +653,8 @@649653     }650654 651655     let fields = detectLoginFields();652-    if (!isAnyCredentialForm(fields)) {656+    const targets = resolveAutofillInteractionTargets(field, fields, explicitSummon);657+    if (!targets.shouldOpen) {653658       return;654659     }655660 @@ -681,13 +686,11 @@681686 682687     if (passkeyToastActive) return;683688 684-    let usernameEl: HTMLInputElement | null = null;685-    const userField = fields.find((f) => f.type === "username" || f.type === "email");686-    if (userField) usernameEl = userField.el;687689     const pwField = fields.find((f) => f.type === "password");688690     // For email-first flows there is no password field on this step — that is expected.689-    if (!pwField && !isEmailFirstForm(fields)) return;690-    const passwordEl = pwField?.el ?? null;691+    if (!explicitSummon && !pwField && !isEmailFirstForm(fields)) return;692+    const usernameEl = targets.usernameEl;693+    const passwordEl = targets.passwordEl;691694 692695     await openAutofillSelector(field, usernameEl, passwordEl, entries, allEntries, domain);693696   } finally {@@ -724,7 +727,7 @@724727     if (dismissLockTimer) { clearTimeout(dismissLockTimer); dismissLockTimer = null; }725728     clearFillStateForExplicitSummon(el);726729     el.focus({ preventScroll: true });727-    void handleFieldInteraction(el);730+    void handleFieldInteraction(el, true);728731   });729732   processedInputs.set(el, cleanup);730733 }@@ -781,7 +784,7 @@781784           if (dismissLockTimer) { clearTimeout(dismissLockTimer); dismissLockTimer = null; }782785           clearFillStateForExplicitSummon(el);783786           el.focus({ preventScroll: true });784-          void handleFieldInteraction(el);787+          void handleFieldInteraction(el, true);785788         });786789         processedInputs.set(el, cleanup);787790       }

view file

src/content/interaction.test.ts+62 −0
@@ -0,0 +1,62 @@1+// @vitest-environment jsdom2+3+import { beforeEach, describe, expect, it } from "vitest";4+5+import { resolveAutofillInteractionTargets } from "./interaction";6+7+describe("resolveAutofillInteractionTargets", () => {8+  beforeEach(() => {9+    document.body.innerHTML = "";10+  });11+12+  it("keeps passive focus gated when no credential form is present", () => {13+    const anchor = document.createElement("input");14+    anchor.type = "email";15+16+    const targets = resolveAutofillInteractionTargets(anchor, []);17+18+    expect(targets.shouldOpen).toBe(false);19+    expect(targets.usernameEl).toBeNull();20+    expect(targets.passwordEl).toBeNull();21+  });22+23+  it("prefers detected username and password fields when they exist", () => {24+    const anchor = document.createElement("input");25+    anchor.type = "email";26+    const username = document.createElement("input");27+    username.type = "email";28+    const password = document.createElement("input");29+    password.type = "password";30+31+    const targets = resolveAutofillInteractionTargets(anchor, [32+      { type: "username", el: username },33+      { type: "password", el: password },34+    ]);35+36+    expect(targets.shouldOpen).toBe(true);37+    expect(targets.usernameEl).toBe(username);38+    expect(targets.passwordEl).toBe(password);39+  });40+41+  it("lets an explicit email-field click open Peach even if detection is partial", () => {42+    const anchor = document.createElement("input");43+    anchor.type = "email";44+45+    const targets = resolveAutofillInteractionTargets(anchor, [], true);46+47+    expect(targets.shouldOpen).toBe(true);48+    expect(targets.usernameEl).toBe(anchor);49+    expect(targets.passwordEl).toBeNull();50+  });51+52+  it("lets an explicit password-field click open Peach even if detection is partial", () => {53+    const anchor = document.createElement("input");54+    anchor.type = "password";55+56+    const targets = resolveAutofillInteractionTargets(anchor, [], true);57+58+    expect(targets.shouldOpen).toBe(true);59+    expect(targets.usernameEl).toBeNull();60+    expect(targets.passwordEl).toBe(anchor);61+  });62+});

view file

src/content/interaction.ts+30 −0
@@ -0,0 +1,30 @@1+import type { DetectedField } from "./detect";2+import { isAnyCredentialForm } from "./detect";3+4+export interface AutofillInteractionTargets {5+  usernameEl: HTMLInputElement | null;6+  passwordEl: HTMLInputElement | null;7+  shouldOpen: boolean;8+}9+10+/**11+ * Explicit icon clicks should still work when passive heuristics are too12+ * cautious. Prefer the real detected fields when present, but allow the click13+ * path to open Peach even if the current page only exposes a partial form.14+ */15+export function resolveAutofillInteractionTargets(16+  anchorField: HTMLInputElement,17+  fields: DetectedField[],18+  explicitSummon = false19+): AutofillInteractionTargets {20+  const userField = fields.find((f) => f.type === "username" || f.type === "email");21+  const pwField = fields.find((f) => f.type === "password");22+  const anchorType = anchorField.type.toLowerCase();23+  const fallbackType = anchorType === "password" ? "password" : "username";24+25+  return {26+    usernameEl: userField?.el ?? (explicitSummon && fallbackType === "username" ? anchorField : null),27+    passwordEl: pwField?.el ?? (explicitSummon && fallbackType === "password" ? anchorField : null),28+    shouldOpen: explicitSummon || isAnyCredentialForm(fields),29+  };30+}

view file