diff --git a/extension/src/popup/components/entry-list.ts b/extension/src/popup/components/entry-list.ts index 9bc6ee0..2fa7aa2 100644 --- a/extension/src/popup/components/entry-list.ts +++ b/extension/src/popup/components/entry-list.ts @@ -25,26 +25,7 @@ function getGroups(entries: Array<[string, ManifestEntry]>): string[] { export function renderEntryList(app: HTMLElement): void { const state = getState(); const groups = getGroups(state.entries); - - // Filter entries by active group (already filtered if group was sent to service worker, - // but we also support client-side filtering for instant response). - let filtered = state.entries; - if (state.activeGroup) { - const g = state.activeGroup.toLowerCase(); - filtered = filtered.filter(([, e]) => e.group?.toLowerCase() === g); - } - if (state.searchQuery) { - const q = state.searchQuery.toLowerCase(); - filtered = filtered.filter(([, e]) => { - if (e.name.toLowerCase().includes(q)) return true; - if (e.url?.toLowerCase().includes(q)) return true; - if (e.username?.toLowerCase().includes(q)) return true; - return false; - }); - } - - // Sort by name. - filtered.sort((a, b) => a[1].name.localeCompare(b[1].name)); + const filtered = getFilteredEntries(); const groupTabsHtml = groups.length > 0 ? `
@@ -126,6 +107,27 @@ async function openEntry(id: string): Promise { } } +/// Compute the visible (filtered) entry list from current state. +function getFilteredEntries(): Array<[string, ManifestEntry]> { + const state = getState(); + let filtered = state.entries; + if (state.activeGroup) { + const g = state.activeGroup.toLowerCase(); + filtered = filtered.filter(([, e]) => e.group?.toLowerCase() === g); + } + if (state.searchQuery) { + const q = state.searchQuery.toLowerCase(); + filtered = filtered.filter(([, e]) => { + if (e.name.toLowerCase().includes(q)) return true; + if (e.url?.toLowerCase().includes(q)) return true; + if (e.username?.toLowerCase().includes(q)) return true; + return false; + }); + } + filtered.sort((a, b) => a[1].name.localeCompare(b[1].name)); + return filtered; +} + function handleListKeydown(e: KeyboardEvent): void { const state = getState(); const target = e.target as HTMLElement; @@ -143,9 +145,11 @@ function handleListKeydown(e: KeyboardEvent): void { return; } + const filtered = getFilteredEntries(); + if (e.key === 'ArrowDown') { e.preventDefault(); - const max = state.entries.length - 1; + const max = Math.max(filtered.length - 1, 0); setState({ selectedIndex: Math.min(state.selectedIndex + 1, max) }); return; } @@ -158,7 +162,6 @@ function handleListKeydown(e: KeyboardEvent): void { if (e.key === 'Enter' && !isSearch) { e.preventDefault(); - const filtered = state.entries; if (filtered[state.selectedIndex]) { openEntry(filtered[state.selectedIndex][0]); } @@ -166,8 +169,8 @@ function handleListKeydown(e: KeyboardEvent): void { } if (e.key === 'Escape') { - // Remove listener to avoid stacking. document.removeEventListener('keydown', handleListKeydown); + window.close(); return; } } diff --git a/extension/src/service-worker/index.ts b/extension/src/service-worker/index.ts index 0d7e43d..01e98f2 100644 --- a/extension/src/service-worker/index.ts +++ b/extension/src/service-worker/index.ts @@ -16,6 +16,8 @@ let masterKey: Uint8Array | null = null; let manifest: Manifest | null = null; let gitHost: GitHost | null = null; let wasmReady = false; +// Cache TOTP secrets by entry ID to avoid re-fetching the entry every second +const totpSecretCache: Map = new Map(); // --- WASM initialization --- @@ -121,6 +123,7 @@ async function handleMessage(req: Request): Promise { case 'lock': masterKey = null; manifest = null; + totpSecretCache.clear(); return { ok: true }; // --- Entries --- @@ -220,11 +223,18 @@ async function handleMessage(req: Request): Promise { case 'get_totp': { if (!masterKey || !gitHost) return { ok: false, error: 'Vault is locked' }; const w = await initWasm(); - const entry = await vault.fetchAndDecryptEntry(gitHost, masterKey, req.id); - if (!entry.totp_secret) return { ok: false, error: 'No TOTP secret for this entry' }; + + // Use cached TOTP secret to avoid re-fetching the entry every second + let totpSecret = totpSecretCache.get(req.id); + if (!totpSecret) { + const entry = await vault.fetchAndDecryptEntry(gitHost, masterKey, req.id); + if (!entry.totp_secret) return { ok: false, error: 'No TOTP secret for this entry' }; + totpSecret = entry.totp_secret; + totpSecretCache.set(req.id, totpSecret); + } const now = Math.floor(Date.now() / 1000); - const code = w.generate_totp(entry.totp_secret, BigInt(now)); + const code = w.generate_totp(totpSecret, BigInt(now)); const remaining = 30 - (now % 30); return { ok: true, data: { code, remaining_seconds: remaining } }; diff --git a/extension/src/service-worker/vault.ts b/extension/src/service-worker/vault.ts index 3732a50..e89e639 100644 --- a/extension/src/service-worker/vault.ts +++ b/extension/src/service-worker/vault.ts @@ -28,8 +28,8 @@ export interface VaultMeta { /// Read the vault salt and KDF params from the git repo. export async function fetchVaultMeta(git: GitHost): Promise { - const saltBytes = await git.readFile('salt'); - const paramsRaw = await git.readFile('params.json'); + const saltBytes = await git.readFile('.idfoto/salt'); + const paramsRaw = await git.readFile('.idfoto/params.json'); const paramsJson = new TextDecoder().decode(paramsRaw); return { salt: saltBytes, paramsJson }; }