diff --git a/documentation/AE__Permissions_and_Security.md b/documentation/AE__Permissions_and_Security.md index c0d62829..3e206bad 100644 --- a/documentation/AE__Permissions_and_Security.md +++ b/documentation/AE__Permissions_and_Security.md @@ -113,6 +113,37 @@ Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for thre - IDAA users authenticate via Novi UUID at `authenticated` level or higher. - A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1. +#### IDAA IndexedDB (IDB) Caching — Auth-Before-Cache Rule + +**Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions. + +**The fix — established pattern for all IDAA routes:** + +1. **Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching. +2. **Data loading = `$effect` in `.svelte` files**, gated on: + ```svelte + if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; + ``` +3. **Three IDB purge paths** in `(idaa)/+layout.svelte` (auth failure, anonymous no-UUID, Reset & Retry button) clear `db_posts`, `db_archives`, and `db_events` tables. + +**Auth path matrix:** + +| User type | `novi_verified` | `trusted_access` | Can load data? | Purge fires? | +| --- | --- | --- | --- | --- | +| Anonymous / unauthenticated | false | false | No | Yes (Case 1) | +| Novi-verified IDAA member | true | false | Yes | No | +| Manager / trusted access | false | true | Yes | No (Case 3 exemption) | + +**Applied to routes (as of 2026-04-19):** +- `idaa/bb/+page.svelte` — `$effect` gate added; `bb/+page.ts` stripped +- `idaa/bb/[post_id]/+page.ts` — stripped; loading handled by trigger in `bb/+layout.svelte` +- `idaa/archives/+page.svelte` — `$effect` gate added; `archives/+layout.ts` stripped +- `idaa/archives/[archive_id]/+page.svelte` — `$effect` gate added; `[archive_id]/+page.ts` stripped +- `idaa/recovery_meetings/+page.svelte` — `$effect` gate already present; `+layout.ts` stripped +- `idaa/recovery_meetings/[event_id]/+page.svelte` — `$effect` gate added; `+page.ts` stripped + +**When adding a new IDAA route:** never put API calls in `+page.ts`/`+layout.ts`. Always gate data fetching with the `$effect` pattern above. + ### Journals - Private personal data. Always authenticated. Passcode/encryption features exist. - Never expose journal content publicly. diff --git a/src/routes/idaa/(idaa)/archives/+layout.ts b/src/routes/idaa/(idaa)/archives/+layout.ts index 224e77ed..660ddad1 100644 --- a/src/routes/idaa/(idaa)/archives/+layout.ts +++ b/src/routes/idaa/(idaa)/archives/+layout.ts @@ -1,12 +1,10 @@ /** @type {import('./$types').LayoutLoad} */ -// console.log(`IDAA BB - [account_id] +layout.ts start`); -// import { error } from '@sveltejs/kit'; -import { browser } from '$app/environment'; -import { archives_func } from '$lib/ae_archives/ae_archives_functions'; +// Data loading for IDAA Archives has been moved to the $effect in +page.svelte +// (gated on novi_verified / trusted_access). +layout.ts runs before layout effects and +// fires during SvelteKit link prefetch, making it unsafe for private IDAA content. -export async function load({ fetch, params, parent }) { - // route +export async function load({ parent }) { const log_lvl: number = 0; const data = await parent(); @@ -21,40 +19,10 @@ export async function load({ fetch, params, parent }) { ); ae_acct = { api: data.ae_api || {}, - slct: { - account_id: account_id - } + slct: { account_id: account_id } }; } - if (browser) { - const load_archive_obj_li = archives_func.load_ae_obj_li__archive({ - api_cfg: ae_acct.api, - for_obj_type: 'account', - for_obj_id: account_id, - inc_content_li: false, - enabled: 'enabled', - hidden: 'not_hidden', - limit: 29, - order_by_li: { - priority: 'DESC', - sort: 'DESC', - updated_on: 'DESC', - created_on: 'DESC', - name: 'ASC' - }, - params: params, - try_cache: true, - log_lvl: log_lvl - }); - if (log_lvl) { - console.log(`load_archive_obj_li = `, load_archive_obj_li); - } - ae_acct.slct.archive_obj_li = load_archive_obj_li; - } - - // WARNING: Precaution against shared data between sites and sessions. data[account_id] = ae_acct; - return data; } diff --git a/src/routes/idaa/(idaa)/archives/+page.svelte b/src/routes/idaa/(idaa)/archives/+page.svelte index 992cad1e..47a593ad 100644 --- a/src/routes/idaa/(idaa)/archives/+page.svelte +++ b/src/routes/idaa/(idaa)/archives/+page.svelte @@ -9,6 +9,7 @@ let log_lvl: number = $state(0); // *** Import Svelte specific // import { page } from '$app/state'; +import { untrack } from 'svelte'; import { browser } from '$app/environment'; // import { goto, invalidate, pushState, replaceState } from '$app/navigation'; @@ -36,7 +37,7 @@ import { idaa_slct, idaa_trig } from '$lib/stores/ae_idaa_stores'; -// import { archives_func } from '$lib/ae_archives/ae_archives_functions'; +import { archives_func } from '$lib/ae_archives/ae_archives_functions'; import Comp__archive_obj_li from './ae_idaa_comp__archive_obj_li.svelte'; import Help_tech from '$lib/app_components/e_app_help_tech.svelte'; @@ -68,6 +69,34 @@ let lq__archive_obj_li = $derived( }) ); +// Initial archive list load — gated on novi_verified. +// WHY $effect and not +layout.ts: layout load functions fire on SvelteKit link prefetch, +// causing private IDAA data to be written to IDB before Novi auth runs. +// $effect only runs post-mount, after the layout has completed Novi verification. +$effect(() => { + if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; + untrack(() => { + archives_func.load_ae_obj_li__archive({ + api_cfg: $ae_api, + for_obj_type: 'account', + for_obj_id: $ae_loc.account_id, + inc_content_li: false, + enabled: 'enabled', + hidden: 'not_hidden', + limit: 29, + order_by_li: { + priority: 'DESC', + sort: 'DESC', + updated_on: 'DESC', + created_on: 'DESC', + name: 'ASC' + }, + try_cache: true, + log_lvl: log_lvl + }); + }); +}); + if (browser) { console.log('Browser environment detected.'); diff --git a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte index 9e2357b6..569e2a61 100644 --- a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte +++ b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte @@ -9,7 +9,7 @@ let { data }: Props = $props(); let log_lvl: number = 0; // *** Import Svelte specific -import { onMount, onDestroy } from 'svelte'; +import { onMount, onDestroy, untrack } from 'svelte'; // *** Import other supporting libraries import { Modal } from 'flowbite-svelte'; @@ -64,6 +64,33 @@ $effect(() => { $idaa_slct.archive_id = page.params.archive_id; // $idaa_slct.archive_obj = ae_acct.slct.archive_obj; +// Load single archive with content — gated on auth. +// WHY $effect and not +page.ts: +page.ts runs during SvelteKit link prefetch, +// causing private IDAA data to be written to IDB before Novi auth runs. +// $effect only runs post-mount, after the layout has completed Novi verification. +$effect(() => { + if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; + const archive_id = $idaa_slct.archive_id; + if (!archive_id) return; + untrack(() => { + archives_func.load_ae_obj_id__archive({ + api_cfg: $ae_api, + archive_id: archive_id, + inc_content_li: true, + enabled: 'enabled', + hidden: 'all', + limit: 99, + log_lvl: log_lvl + }).then((results) => { + if (!results) { + console.warn( + `IDAA Archives [archive_id] $effect: Archive ${archive_id} not found via API or Cache.` + ); + } + }); + }); +}); + // *** Functions and Logic let lq__archive_obj = $derived( liveQuery(async () => { diff --git a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.ts b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.ts index 2f432cd7..37cbca83 100644 --- a/src/routes/idaa/(idaa)/archives/[archive_id]/+page.ts +++ b/src/routes/idaa/(idaa)/archives/[archive_id]/+page.ts @@ -1,56 +1,30 @@ import type { PageLoad } from './$types'; -console.log(`ae_p_idaa_archives [archive_id] +page.ts start`); - -import { browser } from '$app/environment'; -import { archives_func } from '$lib/ae_archives/ae_archives_functions'; +// Data loading for IDAA Archives [archive_id] has been moved to the $effect in +page.svelte +// (gated on novi_verified / trusted_access). +page.ts runs before layout effects and +// fires during SvelteKit link prefetch, making it unsafe for private IDAA content. export const load = (async ({ params, parent }) => { - // route const log_lvl: number = 0; const data = await parent(); data.log_lvl = log_lvl; const account_id = data.account_id; - const ae_acct = data[account_id]; + let ae_acct = data[account_id]; - const archive_id = params.archive_id; - - ae_acct.slct.archive_id = archive_id; - - if (browser) { - if (log_lvl) { - console.log( - `ae_idaa_archives archives [archive_id] +page.ts: archive_id = `, - archive_id - ); - } - // NOTE: Fire in background — do NOT await. Newly created archives are already in Dexie - // (saved by create_ae_obj__archive), so the liveQuery renders immediately. For direct - // links or refreshes, the archive loads from the API and Dexie updates reactively. - // Awaiting here blocked the SvelteKit navigation, causing a visible "refresh" delay. - archives_func - .load_ae_obj_id__archive({ - api_cfg: ae_acct.api, - archive_id: archive_id, - inc_content_li: true, - enabled: 'enabled', - hidden: 'all', // 'not_hidden' to load only visible entries - limit: 99, - log_lvl: log_lvl - }) - .then((results) => { - if (!results) { - console.warn( - `ae IDAA Archives [archive_id] +page.ts: Archive ${archive_id} not found via API or Cache.` - ); - } - }); + if (!ae_acct) { + console.warn( + `ae IDAA Archives [archive_id] +page.ts: Account ${account_id} not found in data. Initializing ghost acct.` + ); + ae_acct = { + api: data.ae_api || {}, + slct: { account_id: account_id } + }; } - // WARNING: Precaution against shared data between sites and presentations. - data[account_id] = ae_acct; + ae_acct.slct.archive_id = params.archive_id; + data[account_id] = ae_acct; return data; }) satisfies PageLoad; diff --git a/src/routes/idaa/(idaa)/bb/[post_id]/+page.ts b/src/routes/idaa/(idaa)/bb/[post_id]/+page.ts index 5c061d45..f59517ee 100644 --- a/src/routes/idaa/(idaa)/bb/[post_id]/+page.ts +++ b/src/routes/idaa/(idaa)/bb/[post_id]/+page.ts @@ -1,53 +1,31 @@ import type { PageLoad } from './$types'; -console.log(`ae_idaa_bulletin_board [post_id] +page.ts start`); - -import { browser } from '$app/environment'; -import { posts_func } from '$lib/ae_posts/ae_posts_functions'; +// Data loading for IDAA BB [post_id] has been moved to the $effect in bb/+layout.svelte +// (triggered via $idaa_trig.post_id, gated inside the auth-verified layout render). +// +page.ts runs before layout effects and fires during SvelteKit link prefetch, +// making it unsafe for private IDAA content. export const load = (async ({ params, parent }) => { - // route const log_lvl: number = 0; const data = await parent(); data.log_lvl = log_lvl; const account_id = data.account_id; - const ae_acct = data[account_id]; + let ae_acct = data[account_id]; - const post_id = params.post_id; - - ae_acct.slct.post_id = post_id; - - if (browser) { - if (log_lvl) { - console.log( - `ae_idaa_posts posts [post_id] +page.ts: post_id = `, - post_id - ); - } - // NOTE: Fire in background — do NOT await. Newly created posts are already in Dexie - // (saved by create_ae_obj__post), so the liveQuery renders immediately. For direct - // links or refreshes, the post loads from the API and Dexie updates reactively. - // Awaiting here blocked the SvelteKit navigation, causing a visible "refresh" delay. - posts_func - .load_ae_obj_id__post({ - api_cfg: ae_acct.api, - post_id: post_id, - inc_comment_li: true, - log_lvl: log_lvl - }) - .then((results) => { - if (!results) { - console.warn( - `ae IDAA BB [post_id] +page.ts: Post ${post_id} not found via API or Cache.` - ); - } - }); + if (!ae_acct) { + console.warn( + `ae IDAA BB [post_id] +page.ts: Account ${account_id} not found in data. Initializing ghost acct.` + ); + ae_acct = { + api: data.ae_api || {}, + slct: { account_id: account_id } + }; } - // WARNING: Precaution against shared data between sites. - data[account_id] = ae_acct; + ae_acct.slct.post_id = params.post_id; + data[account_id] = ae_acct; return data; }) satisfies PageLoad; diff --git a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte index b805c49b..58bf5a27 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte @@ -26,7 +26,7 @@ import { slct_trigger } from '$lib/stores/ae_stores'; import { db_events } from '$lib/ae_events/db_events'; -// import { events_func } from '$lib/ae_events/ae_events_functions'; +import { events_func } from '$lib/ae_events/ae_events_functions'; import { idaa_loc, idaa_sess, @@ -53,6 +53,29 @@ $effect(() => { }); }); +// Load single event — gated on auth. +// WHY $effect and not +page.ts: +page.ts runs during SvelteKit link prefetch, +// causing private IDAA data to be written to IDB before Novi auth runs. +// $effect only runs post-mount, after the layout has completed Novi verification. +$effect(() => { + if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return; + const event_id = ae_acct?.slct?.event_id; + if (!event_id) return; + untrack(() => { + events_func.load_ae_obj_id__event({ + api_cfg: $ae_api, + event_id: event_id, + log_lvl: log_lvl + }).then((results) => { + if (!results) { + console.warn( + `IDAA Recovery Meetings [event_id] $effect: Event ${event_id} not found via API or Cache.` + ); + } + }); + }); +}); + // *** Functions and Logic let lq__event_obj = $derived( liveQuery(async () => { diff --git a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.ts b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.ts index e99b1f89..8ead846a 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.ts +++ b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.ts @@ -1,13 +1,10 @@ import type { PageLoad } from './$types'; -console.log(`ae_p_idaa_events [event_id] +page.ts start`); - -// import { error } from '@sveltejs/kit'; -import { browser } from '$app/environment'; -import { events_func } from '$lib/ae_events/ae_events_functions'; +// Data loading for IDAA Recovery Meetings [event_id] has been moved to the $effect in +page.svelte +// (gated on novi_verified / trusted_access). +page.ts runs before layout effects and +// fires during SvelteKit link prefetch, making it unsafe for private IDAA content. export const load = (async ({ params, parent }) => { - // route const log_lvl: number = 0; const data = await parent(); @@ -22,54 +19,12 @@ export const load = (async ({ params, parent }) => { ); ae_acct = { api: data.ae_api || {}, - slct: { - account_id: account_id - } + slct: { account_id: account_id } }; } - const event_id = params.event_id; + ae_acct.slct.event_id = params.event_id; - ae_acct.slct.event_id = event_id; - - if (browser) { - if (log_lvl) { - console.log( - `ae_idaa_events events [event_id] +page.ts: event_id = `, - event_id - ); - } - // Load event object - const load_event_obj = await events_func.load_ae_obj_id__event({ - api_cfg: ae_acct.api, - event_id: event_id, - log_lvl: log_lvl - }); - // .then((results) => { - // if (!results) { - // error(404, { - // message: 'IDAA Recovery Meetings - Event not found' - // }); - // } else { - // // ae_acct.slct.event_obj = results; - // } - // }); - - if (log_lvl) { - console.log(`load_event_obj = `, load_event_obj); - } - - if (!load_event_obj) { - console.warn( - `ae IDAA Recovery Meeting [event_id] +page.ts: Event ${event_id} not found via API or Cache.` - ); - } else { - ae_acct.slct.event_obj = load_event_obj; - } - } - - // WARNING: Precaution against shared data between sites and presentations. data[account_id] = ae_acct; - return data; }) satisfies PageLoad;