7834ef7ed3cf
fix: Make explicit login-form icon clicks open Peach (v3.6.146)
- time
- 2026-05-31 03:46 UTC · 4 months ago
- machine
- koh2
- parent
- 5ede1338abb9
↓ peach-browser-7834ef7e.tar.gzatlas at this save
files changed7 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 −0manifest.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": {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",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",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": {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 }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+});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+}