5c84f57c81c7

SavedIdentity type, GET_IDENTITIES in MessageType, getIdentities() in vault.ts, handler and tests in messages.ts

time
2026-04-27 13:29 UTC · 6 months ago
machine
koh2
parent
1697ce363850
4 files src/background/messages.test.ts+48 −0src/background/messages.ts+4 −0src/background/vault.ts+35 −0src/shared/types.ts+9 −0
src/background/messages.test.ts+48 −0
@@ -620,6 +620,54 @@620620     expect(fetchMock).not.toHaveBeenCalled();621621   });622622 623+  it("GET_IDENTITIES when locked returns vault locked error", async () => {624+    const vault = await import("./vault");625+    await vault.createVault("correct-pass-12345");626+    vault.lock();627+    await expect(dispatch({ type: "GET_IDENTITIES" })).resolves.toMatchObject({628+      ok: false,629+      error: "vault locked",630+    });631+  });632+633+  it("GET_IDENTITIES returns emails and usernames derived from login entries", async () => {634+    const vault = await import("./vault");635+    await vault.createVault("correct-pass-12345");636+    await vault.addEntry({ title: "Gmail", username: "[email protected]", password: "pw1", url: "https://gmail.com" });637+    await vault.addEntry({ title: "GitHub", username: "[email protected]", password: "pw2", url: "https://github.com" });638+    await vault.addEntry({ title: "Slack", username: "juni_patel", password: "pw3", url: "https://slack.com" });639+640+    const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };641+    expect(resp.ok).toBe(true);642+    const data = resp.data as Array<{ value: string; type: string; useCount: number }>;643+    expect(data.find((d) => d.value === "[email protected]")).toMatchObject({644+      type: "email",645+      useCount: 2,646+    });647+    expect(data.find((d) => d.value === "juni_patel")).toMatchObject({648+      type: "username",649+      useCount: 1,650+    });651+    // emails appear before usernames652+    const emailIdx = data.findIndex((d) => d.value === "[email protected]");653+    const userIdx = data.findIndex((d) => d.value === "juni_patel");654+    expect(emailIdx).toBeLessThan(userIdx);655+  });656+657+  it("GET_IDENTITIES deduplicates correctly — same email across 3 entries appears once with useCount 3", async () => {658+    const vault = await import("./vault");659+    await vault.createVault("correct-pass-12345");660+    for (let i = 0; i < 3; i++) {661+      await vault.addEntry({ title: `Site${i}`, username: "[email protected]", password: "pw", url: `https://site${i}.com` });662+    }663+664+    const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };665+    const data = resp.data as Array<{ value: string; useCount: number }>;666+    const hits = data.filter((d) => d.value === "[email protected]");667+    expect(hits).toHaveLength(1);668+    expect(hits[0]!.useCount).toBe(3);669+  });670+623671   it("SUBMIT_FEEDBACK does not refresh the auto-lock timer", async () => {624672     const vault = await import("./vault");625673     await vault.createVault("correct-pass-12345");

view file

src/background/messages.ts+4 −0
@@ -405,6 +405,10 @@405405         await deactivateLicense();406406         return { ok: true };407407       }408+      case "GET_IDENTITIES": {409+        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };410+        return { ok: true, data: vault.getIdentities() };411+      }408412       case "GET_ADVANCED_HEALTH_REPORT": {409413         const gate = await requirePro("advanced_health");410414         if (!gate.allowed) return { ok: false, error: "pro_required" };

view file

src/background/vault.ts+35 −0
@@ -15,6 +15,7 @@1515   AccentColorName,1616   LegacyVaultEntry,1717   ManualSummonRecord,18+  SavedIdentity,1819   StoredVault,1920   VaultEntry,2021 } from "../shared/types";@@ -556,6 +557,40 @@556557 export function getEntries(): VaultEntry[] {557558   if (!derivedKey) throw new Error("vault locked");558559   return entries.filter((entry) => !entry.deletedAt);560+}561+562+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;563+564+export function getIdentities(): SavedIdentity[] {565+  if (!derivedKey) throw new Error("vault locked");566+  const active = entries.filter((e) => !e.deletedAt && e.type === "login");567+568+  const emailMap = new Map<string, { lastUsed: number; count: number; domain: string }>();569+  const usernameMap = new Map<string, { lastUsed: number; count: number; domain: string }>();570+571+  for (const entry of active) {572+    const value = entry.username?.trim();573+    if (!value) continue;574+    const map = EMAIL_RE.test(value) ? emailMap : usernameMap;575+    const existing = map.get(value);576+    const domain = extractDomain(entry.url ?? "") ?? "";577+    const modifiedAt = entry.modifiedAt ?? entry.createdAt ?? 0;578+    if (!existing || modifiedAt > existing.lastUsed) {579+      map.set(value, { lastUsed: modifiedAt, count: (existing?.count ?? 0) + 1, domain });580+    } else {581+      map.set(value, { ...existing, count: existing.count + 1 });582+    }583+  }584+585+  const toIdentity = (type: "email" | "username") =>586+    ([value, d]: [string, { lastUsed: number; count: number; domain: string }]): SavedIdentity => ({587+      value, type, lastUsed: d.lastUsed, useCount: d.count, exampleDomain: d.domain || undefined,588+    });589+590+  return [591+    ...[...emailMap.entries()].map(toIdentity("email")).sort((a, b) => b.useCount - a.useCount),592+    ...[...usernameMap.entries()].map(toIdentity("username")).sort((a, b) => b.useCount - a.useCount),593+  ];559594 }560595 561596 export async function batchAddEntries(drafts: EntryDraft[]): Promise<VaultEntry[]> {

view file

src/shared/types.ts+9 −0
@@ -208,6 +208,7 @@208208   | "GET_ADVANCED_HEALTH_REPORT"209209   | "INITIATE_PASSWORD_CHANGE"210210   | "DEACTIVATE_LICENSE"211+  | "GET_IDENTITIES"211212   // Content-script-bound message types — sent BY the background TO content212213   // scripts. They never reach messages.ts's switch. Kept in this union so213214   // existing call sites type-check; do not add cases for them in the@@ -268,6 +269,14 @@268269 ] as const;269270 270271 export type AccentColorName = typeof ACCENT_COLORS[number]["name"];272+273+export interface SavedIdentity {274+  value: string;275+  type: "email" | "username";276+  lastUsed: number;277+  useCount: number;278+  exampleDomain?: string;279+}271280 272281 export interface PasswordPolicy {273282   minLength?: number;

view file