246c4b48a712

remove: smart password change — feature never worked, cut entirely

time
2026-05-19 20:50 UTC · 5 months ago
machine
koh2
parent
004d1d32f877
13 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/background/health.test.ts+1 −22src/background/health.ts+0 −47src/background/messages.ts+1 −20src/popup/styles/popup.css+0 −6src/popup/views/Entry.tsx+0 −6src/popup/views/Health.test.tsx+86 −0src/popup/views/Health.tsx+0 −26src/popup/views/Settings.tsx+0 −1src/shared/types.ts+0 −4
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.43",5+  "version": "3.6.44",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.43",4+  "version": "3.6.44",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.43",3+  "version": "3.6.44",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.43",9+      "version": "3.6.44",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.43",3+  "version": "3.6.44",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/health.test.ts+1 −22
@@ -1,5 +1,5 @@11 import { describe, expect, it } from "vitest";2-import { buildAdvancedHealthReport, resolveResetUrl } from "./health";2+import { buildAdvancedHealthReport } from "./health";33 import type { VaultEntry } from "../shared/types";44 55 const NINETY_ONE_DAYS = 91 * 86_400_000;@@ -116,26 +116,5 @@116116       .map((b) => b.toString(16).padStart(2, "0"))117117       .join("");118118     expect(json.includes(hex)).toBe(false);119-  });120-});121-122-describe("resolveResetUrl", () => {123-  it("maps known domains to their security pages", () => {124-    expect(resolveResetUrl(entry({ url: "https://github.com/login" }))).toBe(125-      "https://github.com/settings/security"126-    );127-    expect(resolveResetUrl(entry({ url: "https://mail.google.com/u/0" }))).toBe(128-      "https://myaccount.google.com/security"129-    );130-  });131-132-  it("falls back to /account/security on the entry's origin", () => {133-    expect(resolveResetUrl(entry({ url: "https://app.example.com/login" }))).toBe(134-      "https://app.example.com/account/security"135-    );136-  });137-138-  it("returns null when no URL is set", () => {139-    expect(resolveResetUrl(entry({ url: undefined }))).toBeNull();140119   });141120 });

view file

src/background/health.ts+0 −47
@@ -168,50 +168,3 @@168168     remediation: reports,169169   };170170 }171-172-/**173- * Resolve a best-effort password reset URL for the entry's domain. Returns174- * the entry's own URL as a fallback so Pro users still land somewhere175- * sensible — a banner inside the in-page popup later picks up the form176- * pre-fill. Form auto-fill is intentionally not done here; that's the177- * content-script half of smart password change and stays event-driven.178- */179-export function resolveResetUrl(entry: VaultEntry): string | null {180-  const url = entry.url ?? "";181-  const domain = extractDomain(url);182-  if (!domain) return null;183-184-  const knownResetPaths: Record<string, string> = {185-    "google.com": "https://myaccount.google.com/security",186-    "gmail.com": "https://myaccount.google.com/security",187-    "github.com": "https://github.com/settings/security",188-    "twitter.com": "https://twitter.com/settings/password",189-    "x.com": "https://x.com/settings/password",190-    "facebook.com": "https://www.facebook.com/settings?tab=security",191-    "instagram.com": "https://www.instagram.com/accounts/password/change/",192-    "linkedin.com":193-      "https://www.linkedin.com/psettings/change-password",194-    "amazon.com": "https://www.amazon.com/ap/changepassword",195-    "apple.com": "https://appleid.apple.com",196-    "icloud.com": "https://appleid.apple.com",197-    "microsoft.com": "https://account.microsoft.com/security",198-    "outlook.com": "https://account.microsoft.com/security",199-    "live.com": "https://account.microsoft.com/security",200-    "dropbox.com": "https://www.dropbox.com/account/security",201-    "netflix.com": "https://www.netflix.com/password",202-    "spotify.com": "https://www.spotify.com/account/change-password/",203-    "reddit.com": "https://www.reddit.com/settings/account",204-  };205-206-  for (const [match, target] of Object.entries(knownResetPaths)) {207-    if (domain === match || domain.endsWith("." + match)) return target;208-  }209-210-  try {211-    const normalized = url.startsWith("http") ? url : `https://${url}`;212-    const u = new URL(normalized);213-    return `${u.origin}/account/security`;214-  } catch {215-    return null;216-  }217-}

view file

src/background/messages.ts+1 −20
@@ -42,7 +42,7 @@4242   resolvePendingPasskeyRequest,4343   setPasskeyProviderEnabled,4444 } from "./passkey";45-import { buildAdvancedHealthReport, resolveResetUrl } from "./health";45+import { buildAdvancedHealthReport } from "./health";4646 import { storageClear } from "../shared/storage";4747 import { findInvalidWords } from "../shared/recovery-words";4848 import type {@@ -729,25 +729,6 @@729729         if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };730730         const report = await buildAdvancedHealthReport(vault.getEntries());731731         return { ok: true, data: report };732-      }733-      case "INITIATE_PASSWORD_CHANGE": {734-        const gate = await requirePro("smart_password_change");735-        if (!gate.allowed) return { ok: false, error: "pro_required" };736-        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };737-        const { id } = msg.payload as { id: string };738-        const entry = vault.getEntryById(id);739-        if (!entry) return { ok: false, error: "entry_not_found" };740-        const url = resolveResetUrl(entry);741-        if (!url) return { ok: false, error: "no_reset_url" };742-        try {743-          await chrome.tabs.create({ url });744-          return { ok: true, data: { url } };745-        } catch (err) {746-          return {747-            ok: false,748-            error: err instanceof Error ? err.message : "failed_to_open_tab",749-          };750-        }751732       }752733       case "IS_DURESS_ENABLED": {753734         const enabled = await vault.isDuressEnabled();

view file

src/popup/styles/popup.css+0 −6
@@ -1007,12 +1007,6 @@10071007   letter-spacing: 0.04em;10081008 }10091009 1010-.peach-breach-action {1011-  margin-left: auto;1012-  font-size: 0.625rem;1013-  padding: 3px 8px;1014-}1015-10161010 .peach-password-value.is-breached {10171011   color: rgba(220, 80, 60, 0.75);10181012   border-bottom: 1px solid rgba(220, 80, 60, 0.22);

view file

src/popup/views/Entry.tsx+0 −6
@@ -537,12 +537,6 @@537537                     <circle cx="6" cy="9" r="0.5" fill="var(--breach)" stroke="none" />538538                   </svg>539539                   <span style="flex:1">found in a known data breach</span>540-                  <button541-                    class="peach-btn peach-btn--sm peach-btn--accent peach-breach-action"542-                    onClick={() => { startEdit("password", entry().password ?? ""); setShowGenerator(true); }}543-                  >544-                    Change password545-                  </button>546540                 </div>547541               </Show>548542 

view file

src/popup/views/Health.test.tsx+86 −0
@@ -0,0 +1,86 @@1+// @vitest-environment jsdom2+3+import { afterEach, describe, expect, it, vi } from "vitest";4+5+import { flushView, renderView } from "./view-test-utils";6+import type { AdvancedHealthReport, VaultEntry } from "../../shared/types";7+8+const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();9+10+vi.mock("../App", () => ({11+  sendMessage: (msg: unknown) => sendMessageMock(msg),12+}));13+14+function entry(overrides: Partial<VaultEntry> = {}): VaultEntry {15+  return {16+    id: "entry-1",17+    type: "login",18+    title: "Example",19+    username: "[email protected]",20+    password: "correct-horse-battery-staple",21+    url: "https://example.com/login",22+    createdAt: Date.now() - 10_000,23+    modifiedAt: Date.now() - 10_000,24+    ...overrides,25+  };26+}27+28+describe("HealthView", () => {29+  let dispose: (() => void) | undefined;30+31+  afterEach(() => {32+    dispose?.();33+    dispose = undefined;34+    sendMessageMock.mockReset();35+    document.body.innerHTML = "";36+  });37+38+  it("renders the Pro remediation queue without the retired password-change button", async () => {39+    const report: AdvancedHealthReport = {40+      vaultScore: 65,41+      generatedAt: Date.now(),42+      entryCount: 1,43+      reusedGroups: 0,44+      agedCount: 1,45+      remediation: [46+        {47+          id: "entry-1",48+          title: "Example",49+          domain: "example.com",50+          url: "https://example.com/login",51+          score: 65,52+          ageDays: 200,53+          reasons: ["200 days old"],54+          factors: {55+            breached: false,56+            breachCount: 0,57+            reused: false,58+            aged: true,59+            weak: false,60+            missingTotpOnSensitive: false,61+          },62+        },63+      ],64+    };65+    sendMessageMock.mockResolvedValue({ ok: true, data: report });66+67+    const { HealthView } = await import("./Health");68+    const mounted = renderView(69+      <HealthView70+        entries={[entry()]}71+        isPro={true}72+        onFixEntry={vi.fn()}73+        onBack={vi.fn()}74+        onRefresh={vi.fn()}75+      />,76+    );77+    dispose = mounted.dispose;78+79+    await flushView();80+    await flushView();81+82+    expect(mounted.container.textContent).toContain("Remediation queue");83+    expect(mounted.container.textContent).toContain("Example");84+    expect(mounted.container.querySelectorAll(".peach-health-pro-row button")).toHaveLength(0);85+  });86+});

view file

src/popup/views/Health.tsx+0 −26
@@ -69,7 +69,6 @@6969   // Start true for Pro users so the loading screen is visible on the very first render —7070   // previously initialized to false and flipped in onMount, which left a blank-body flash.7171   const [advancedLoading, setAdvancedLoading] = createSignal<boolean>(!!props.isPro);72-  const [smartChangingId, setSmartChangingId] = createSignal<string | null>(null);7372 7473   onMount(async () => {7574     if (!props.isPro) return;@@ -83,24 +82,6 @@8382     }8483   });8584 86-  async function handleSmartChange(entryId: string) {87-    setSmartChangingId(entryId);88-    const res = await sendMessage({89-      type: "INITIATE_PASSWORD_CHANGE",90-      payload: { id: entryId },91-    }) as { ok: boolean; error?: string };92-    setSmartChangingId(null);93-    if (res.ok) {94-      // The reset URL opened in a new tab — close the popup so the user can95-      // act on the page. Refresh the report on next mount so the remediation96-      // queue reflects any updates the user makes.97-      window.close();98-    } else if (res.error === "no_reset_url") {99-      // Entry has no usable URL — open the entry directly for manual editing.100-      props.onFixEntry(props.entries.find((e) => e.id === entryId)!);101-    }102-  }103-10485   const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;10586 10687   const breachedEntries = createMemo(() =>@@ -256,13 +237,6 @@256237                         ></div>257238                       </div>258239                     </div>259-                    <button260-                      class="peach-btn peach-btn--accent peach-btn--sm"261-                      onClick={() => void handleSmartChange(item.id)}262-                      disabled={smartChangingId() === item.id}263-                    >264-                      {smartChangingId() === item.id ? "opening…" : "Change this password"}265-                    </button>266240                   </div>267241                 )}268242               </For>

view file

src/popup/views/Settings.tsx+0 −1
@@ -1459,7 +1459,6 @@14591459     { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" },14601460     { feature: "ios_app", name: "Mobile apps", desc: "Android · iOS soon" },14611461     { feature: "advanced_health", name: "Vault health", desc: "Reuse · age · exposure" },1462-    { feature: "smart_password_change", name: "Smart reset", desc: "Guided password change" },14631462   ];14641463 14651464   const PRO_SECURITY_FEATURES: Array<{

view file

src/shared/types.ts+0 −4
@@ -275,7 +275,6 @@275275   | "SUBMIT_FEEDBACK"276276   | "GET_VAULT_STATUS"277277   | "GET_ADVANCED_HEALTH_REPORT"278-  | "INITIATE_PASSWORD_CHANGE"279278   | "DEACTIVATE_LICENSE"280279   | "GET_IDENTITIES"281280   | "DESTROY_VAULT"@@ -333,7 +332,6 @@333332   | "sync"334333   | "passkeys"335334   | "advanced_health"336-  | "smart_password_change"337335   | "ios_app"338336   | "codex"339337   | "canary_entry"@@ -478,8 +476,6 @@478476   | "invalid_feedback_endpoint"479477   | "message_empty"480478   | "network_error"481-  | "no_reset_url"482-  | "failed_to_open_tab"483479   | "operation_failed"484480   | "unknown_message_type";485481 

view file