diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 85446662..0aef077b 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -400,14 +400,38 @@ export async function load({ fetch, params, parent, route, url }) { ae_loc_init['base_url'] = url.origin; ae_loc_init['hostname'] = url.hostname; + // --- Access key verification --- + // Sites without a configured key (most non-IDAA clients) always pass — no key required. + // Sites with a key (e.g. IDAA Novi iframe pages) require verification on first visit. + // + // There are three cases: + // + // CASE 1 — Site requires no key: immediate access, no localStorage needed. + // + // CASE 2 — Key present in URL (?key=...): always live-validated against the API response. + // - Match → allow_access = key value (truthy), key_checked = key value. + // - No match → allow_access = false. Access denied immediately. + // - This path is ALWAYS strict. There is no shortcut through cache. + // - The key only needs to be in the iframe src URL on the first page load (or after + // key rotation). Internal links (recovery meeting detail, BB posts, archive content) + // do NOT need to carry the key as a param once the user is in. + // + // CASE 3 — No key in URL: check localStorage for a previously verified key. + // - The stored key is compared against the CURRENT site config from the API — + // so if the key was rotated, the stored value will not match and access is denied + // until the user presents the new key via URL param again. + // - allow_access remains falsy if no stored key or stored key doesn't match. + // - Sites that never require a key (Case 1) never reach this branch. if ( !ae_loc_init['site_access_key'] && !ae_loc_init['site_domain_access_key'] ) { + // CASE 1: No key required for this site. ae_loc_init['key_checked'] = true; ae_loc_init['allow_access'] = true; } else { if (access_key) { + // CASE 2: Key present in URL — always live-validate, no cache shortcut. if (log_lvl) { console.log(`root +layout.ts: access_key = ${access_key}`); } @@ -420,11 +444,34 @@ export async function load({ fetch, params, parent, route, url }) { ae_loc_init['allow_access'] = ae_loc_init['site_domain_access_key']; } else { + // Key present but wrong — deny immediately. ae_loc_init['key_checked'] = true; ae_loc_init['allow_access'] = false; } } else { + // CASE 3: No key in URL — check localStorage for a prior verified key. + // Allows internal navigation (recovery meeting detail pages, BB posts, archive + // content, etc.) without appending ?key= to every internal link. + // Security contract: stored key is validated against the CURRENT site config + // from the API response — not blindly trusted from localStorage. If the key + // was rotated, the stored value won't match and access will be denied. ae_loc_init['key_checked'] = true; + try { + const ae_loc_raw = localStorage.getItem('ae_loc'); + if (ae_loc_raw) { + const stored = JSON.parse(ae_loc_raw); + const stored_key = stored.key_checked; + if ( + stored_key && + (stored_key === ae_loc_init['site_access_key'] || + stored_key === ae_loc_init['site_domain_access_key']) + ) { + ae_loc_init['allow_access'] = stored_key; + } + } + } catch { + // Silently fail on storage read + } } }