diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte index 583865e..9f850d0 100644 --- a/frontend/src/components/Home.svelte +++ b/frontend/src/components/Home.svelte @@ -3,9 +3,7 @@ import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action'; import { api } from '../lib/api'; import { session } from '../lib/session.svelte'; - import { - decryptPayload, base64ToBytes, type PrivatePayload, - } from '../lib/crypto'; + import { decryptPrivateCleartext } from '../lib/privateCleartext'; import ActivityForm from './ActivityForm.svelte'; import ActivityRow from './ActivityRow.svelte'; import type { Activity } from '../../../shared/types'; @@ -56,20 +54,7 @@ } /** Pre-decrypt private payloads once per render so search can match. */ - const privateCleartext = $derived.by(() => { - const map = new Map(); - if (!session.dek) return map; - for (const a of activities) { - if (a.visibility !== 'private') continue; - try { - map.set(a.id, decryptPayload( - { ciphertext: base64ToBytes(a.ciphertext), nonce: base64ToBytes(a.nonce) }, - session.dek, - )); - } catch { /* skip */ } - } - return map; - }); + const privateCleartext = $derived(decryptPrivateCleartext(activities, session.dek)); function matchesQuery(a: Activity, q: string): boolean { if (!q) return true; diff --git a/frontend/src/components/TagPage.svelte b/frontend/src/components/TagPage.svelte index 17757ce..ac03b20 100644 --- a/frontend/src/components/TagPage.svelte +++ b/frontend/src/components/TagPage.svelte @@ -2,9 +2,7 @@ import { onMount } from 'svelte'; import { api } from '../lib/api'; import { session } from '../lib/session.svelte'; - import { - decryptPayload, base64ToBytes, type PrivatePayload, - } from '../lib/crypto'; + import { decryptPrivateCleartext } from '../lib/privateCleartext'; import ActivityRow from './ActivityRow.svelte'; import type { Activity } from '../../../shared/types'; @@ -37,24 +35,9 @@ } } - /** - * Pre-decrypt private payloads once so we can both filter by tag and let - * ActivityRow render without re-doing the work. Same pattern as Home.svelte. - */ - const privateCleartext = $derived.by(() => { - const map = new Map(); - if (!session.dek) return map; - for (const a of activities) { - if (a.visibility !== 'private') continue; - try { - map.set(a.id, decryptPayload( - { ciphertext: base64ToBytes(a.ciphertext), nonce: base64ToBytes(a.nonce) }, - session.dek, - )); - } catch { /* skip undecryptable rows */ } - } - return map; - }); + /** Pre-decrypt private rows once so we can filter by tag and let + * ActivityRow render without redoing the work. */ + const privateCleartext = $derived(decryptPrivateCleartext(activities, session.dek)); function hasTag(a: Activity): boolean { if (a.visibility === 'private') { diff --git a/frontend/src/lib/privateCleartext.ts b/frontend/src/lib/privateCleartext.ts new file mode 100644 index 0000000..fd2333d --- /dev/null +++ b/frontend/src/lib/privateCleartext.ts @@ -0,0 +1,37 @@ +/** + * Bulk-decrypt the private rows in a list of activities, returning a + * Map. Rows that fail to decrypt (corrupted ciphertext, + * wrong DEK) are silently skipped — the caller treats their absence as + * "no plaintext available" rather than an error, matching how the UI + * already displays undecryptable rows. + * + * Originally duplicated in Home.svelte and TagPage.svelte; consolidated + * here so any future change (parallelism, telemetry on decrypt failures) + * lives in one place. + */ +import { decryptPayload, base64ToBytes, type PrivatePayload } from './crypto'; +import type { Activity } from '../../../shared/types'; + +export function decryptPrivateCleartext( + activities: Activity[], + dek: Uint8Array | null, +): Map { + const map = new Map(); + if (!dek) return map; + for (const a of activities) { + if (a.visibility !== 'private') continue; + try { + map.set( + a.id, + decryptPayload( + { ciphertext: base64ToBytes(a.ciphertext), nonce: base64ToBytes(a.nonce) }, + dek, + ), + ); + } catch { + // Skip rows we can't decrypt — UI shows them as "log in again" via + // the !session.dek branch in ActivityRow. + } + } + return map; +}