fix(idaa): re-verify UUID on SvelteKit navigation, not just full reloads
Root cause: url_uuid was read once from window.location.search (const), assuming UUID changes always cause a full iframe reload (Novi impersonation). Manual URL edits within the same SvelteKit session keep the layout mounted, leaving url_uuid stale — the TTL cache then hit for the OLD valid UUID, granting access under the wrong identity without re-verifying. Fix: - url_uuid is now $derived from $page.url.searchParams, updated on every SvelteKit navigation - url_uuid is read outside untrack() in Effect 2 so UUID changes trigger a fresh verification run - verify_failed (boolean) replaced with verify_failed_for_uuid (string|null) so the retry-loop latch is keyed to the specific failed UUID — a different UUID in the URL is always a clean slate that gets verified fresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ let log_lvl: number = 0;
|
|||||||
|
|
||||||
// *** Import Svelte specific
|
// *** Import Svelte specific
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
// *** Import Aether specific variables and functions
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
@@ -26,30 +27,38 @@ interface Props {
|
|||||||
|
|
||||||
let { data, children }: Props = $props();
|
let { data, children }: Props = $props();
|
||||||
|
|
||||||
// UUID is set by Novi when loading the iframe — fixed for this page's lifetime.
|
// Reactive UUID — derived from $page.url so it updates on every SvelteKit client-side
|
||||||
// Impersonation causes a full iframe reload (new page load), not a SvelteKit navigation,
|
// navigation, not just full page reloads.
|
||||||
// so reading this once is correct and avoids reactive noise from client-side navigation.
|
// WHY: The original impl read window.location.search once (const), assuming UUID changes
|
||||||
// NOTE: If Novi ever adds dynamic impersonation (no full reload), this needs revisiting —
|
// always cause a full iframe reload (Novi impersonation behavior). But manual URL edits
|
||||||
// reintroduce $derived.by on data.url and the UUID-change guards removed in this commit.
|
// in the same SvelteKit session keep the layout mounted and leave url_uuid stale — the
|
||||||
const url_uuid = browser ? new URLSearchParams(window.location.search).get('uuid') : null;
|
// TTL cache then hits for the OLD valid UUID, granting access under the wrong identity.
|
||||||
|
// $page.url.searchParams is always current, so any UUID change is caught and re-verified.
|
||||||
|
const url_uuid = $derived(browser ? ($page.url.searchParams.get('uuid') ?? null) : null);
|
||||||
|
|
||||||
// True while the Novi API call is in flight.
|
// True while the Novi API call is in flight.
|
||||||
// Pre-initialized to true when a UUID is present to prevent an "Access Denied" flash
|
// Pre-initialized to true when a UUID is present to prevent an "Access Denied" flash
|
||||||
// before the effect has a chance to run on first render.
|
// before the effect has a chance to run on first render.
|
||||||
let novi_verifying: boolean = $state(!!url_uuid);
|
// WHY direct window.location.search here (not url_uuid derived): $state initializers are
|
||||||
|
// not reactive — we want a one-time snapshot of "is there a UUID on load?". Using the
|
||||||
|
// $derived here would only capture its initial value anyway, and Svelte warns about it.
|
||||||
|
let novi_verifying: boolean = $state(
|
||||||
|
browser ? !!new URLSearchParams(window.location.search).get('uuid') : false
|
||||||
|
);
|
||||||
|
|
||||||
// Concurrency guard — separate from novi_verifying (the UI spinner).
|
// Concurrency guard — separate from novi_verifying (the UI spinner).
|
||||||
// Do NOT use novi_verifying as a concurrency guard: it is pre-initialized to true,
|
// Do NOT use novi_verifying as a concurrency guard: it is pre-initialized to true,
|
||||||
// which would cause the guard to fire immediately and skip verification entirely.
|
// which would cause the guard to fire immediately and skip verification entirely.
|
||||||
let verify_in_flight = false;
|
let verify_in_flight = false;
|
||||||
|
|
||||||
// Failure latch — set to true when verification definitively fails (e.g. Novi returns
|
// Failure latch — stores the UUID that definitively failed (Novi 200+empty, 4xx, network error).
|
||||||
// 200 with empty data for a non-existent UUID, or a 4xx response).
|
// WHY (loop prevention): writes to $idaa_loc in the catch block trigger svelte-persisted-store
|
||||||
// WHY: Without this, writes to $idaa_loc in the catch block trigger svelte-persisted-store
|
// storage events → $ae_loc re-notifies subscribers → Effect 2 re-runs. Without the latch,
|
||||||
// storage events, which cause $ae_loc to re-notify its subscribers, re-running Effect 2 in
|
// the same failing UUID would be re-verified infinitely.
|
||||||
// an infinite loop. The UUID is fixed for this page load (read once from window.location.search),
|
// WHY (UUID-keyed, not boolean): when the URL UUID changes (SvelteKit navigation), the new
|
||||||
// so retrying will always produce the same result. User must reload to retry.
|
// UUID must be verified fresh. A plain boolean would wrongly block verification of the new UUID.
|
||||||
let verify_failed = false;
|
// Storing the failed UUID means only that exact UUID is skipped; any other UUID is a clean slate.
|
||||||
|
let verify_failed_for_uuid: string | null = null;
|
||||||
|
|
||||||
// Show a manual reset button if the spinner is still visible after this many ms.
|
// Show a manual reset button if the spinner is still visible after this many ms.
|
||||||
// Handles the case where site_cfg_json loads without novi_idaa_api_key (stale cache)
|
// Handles the case where site_cfg_json loads without novi_idaa_api_key (stale cache)
|
||||||
@@ -83,10 +92,10 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Effect 2: Novi UUID verification
|
// Effect 2: Novi UUID verification
|
||||||
// The only reactive dependency is $ae_loc.site_cfg_json — the API key arrives async
|
// Reactive dependencies (read outside untrack):
|
||||||
// via SWR background fetch and may not be populated on first render. Reading it outside
|
// - $ae_loc.site_cfg_json: API key arrives async via SWR; re-run when config loads.
|
||||||
// untrack() ensures the effect re-runs when the config loads.
|
// - url_uuid ($derived from $page.url): re-run when URL UUID changes (SvelteKit navigation).
|
||||||
// The UUID is not reactive (read once above via window.location.search).
|
// Everything else is inside untrack() to prevent reactive loops from store writes.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
@@ -97,8 +106,12 @@ $effect(() => {
|
|||||||
const trusted_li: string[] = site_cfg.novi_trusted_li ?? [];
|
const trusted_li: string[] = site_cfg.novi_trusted_li ?? [];
|
||||||
const ttl_ms: number = site_cfg.novi_verified_ttl_ms ?? VERIFIED_TTL_MS_DEFAULT;
|
const ttl_ms: number = site_cfg.novi_verified_ttl_ms ?? VERIFIED_TTL_MS_DEFAULT;
|
||||||
|
|
||||||
|
// Read url_uuid here (outside untrack) to create a reactive dependency.
|
||||||
|
// Effect 2 re-runs whenever the UUID in the URL changes.
|
||||||
|
const current_uuid = url_uuid;
|
||||||
|
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if (!url_uuid) {
|
if (!current_uuid) {
|
||||||
// No UUID in URL — non-Novi path (user/pass or shared passcode sign-in).
|
// No UUID in URL — non-Novi path (user/pass or shared passcode sign-in).
|
||||||
$idaa_loc.novi_verified = false;
|
$idaa_loc.novi_verified = false;
|
||||||
novi_verifying = false;
|
novi_verifying = false;
|
||||||
@@ -107,10 +120,10 @@ $effect(() => {
|
|||||||
|
|
||||||
if (verify_in_flight) return;
|
if (verify_in_flight) return;
|
||||||
|
|
||||||
if (verify_failed) {
|
if (verify_failed_for_uuid === current_uuid) {
|
||||||
// Verification definitively failed for this UUID — do not retry.
|
// This exact UUID already definitively failed — do not retry.
|
||||||
// See comment on verify_failed declaration for the full explanation.
|
// A different UUID is a clean slate; see verify_failed_for_uuid declaration.
|
||||||
if (log_lvl) console.log(`IDAA Layout: skipping re-verification — already failed for ${url_uuid}`);
|
if (log_lvl) console.log(`IDAA Layout: skipping re-verification — already failed for ${current_uuid}`);
|
||||||
novi_verifying = false;
|
novi_verifying = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -120,11 +133,11 @@ $effect(() => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (
|
if (
|
||||||
$idaa_loc.novi_verified &&
|
$idaa_loc.novi_verified &&
|
||||||
$idaa_loc.novi_uuid === url_uuid &&
|
$idaa_loc.novi_uuid === current_uuid &&
|
||||||
$idaa_loc.novi_verified_ts &&
|
$idaa_loc.novi_verified_ts &&
|
||||||
now - $idaa_loc.novi_verified_ts < ttl_ms
|
now - $idaa_loc.novi_verified_ts < ttl_ms
|
||||||
) {
|
) {
|
||||||
if (log_lvl) console.log(`IDAA Layout: cached verification valid for ${url_uuid}`);
|
if (log_lvl) console.log(`IDAA Layout: cached verification valid for ${current_uuid}`);
|
||||||
novi_verifying = false;
|
novi_verifying = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -145,11 +158,11 @@ $effect(() => {
|
|||||||
$ae_loc.trusted_access ||
|
$ae_loc.trusted_access ||
|
||||||
($ae_loc.authenticated_access &&
|
($ae_loc.authenticated_access &&
|
||||||
$idaa_loc.novi_verified &&
|
$idaa_loc.novi_verified &&
|
||||||
$idaa_loc.novi_uuid === url_uuid);
|
$idaa_loc.novi_uuid === current_uuid);
|
||||||
if (!has_valid_session) {
|
if (!has_valid_session) {
|
||||||
novi_verifying = true;
|
novi_verifying = true;
|
||||||
}
|
}
|
||||||
verify_novi_uuid(url_uuid, api_key, api_root);
|
verify_novi_uuid(current_uuid, api_key, api_root);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -271,7 +284,7 @@ async function verify_novi_uuid(
|
|||||||
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
|
`IDAA Layout: Novi UUID verification failed for ${uuid}:`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
verify_failed = true; // Latch — stop the Effect 2 retry loop. See declaration comment.
|
verify_failed_for_uuid = uuid; // Latch — stop retry loop for this UUID. See declaration comment.
|
||||||
$idaa_loc.novi_uuid = null;
|
$idaa_loc.novi_uuid = null;
|
||||||
$idaa_loc.novi_email = null;
|
$idaa_loc.novi_email = null;
|
||||||
$idaa_loc.novi_full_name = null;
|
$idaa_loc.novi_full_name = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user