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
↓ peach-browser-5c84f57c.tar.gzatlas at this save
files changed4 files
src/background/messages.test.ts+48 −0src/background/messages.ts+4 −0src/background/vault.ts+35 −0src/shared/types.ts+9 −0src/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");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" };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[]> {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;