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 };
}