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 { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
@@ -26,30 +27,38 @@ interface Props {
|
||||
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
// UUID is set by Novi when loading the iframe — fixed for this page's lifetime.
|
||||
// Impersonation causes a full iframe reload (new page load), not a SvelteKit navigation,
|
||||
// so reading this once is correct and avoids reactive noise from client-side navigation.
|
||||
// NOTE: If Novi ever adds dynamic impersonation (no full reload), this needs revisiting —
|
||||
// reintroduce $derived.by on data.url and the UUID-change guards removed in this commit.
|
||||
const url_uuid = browser ? new URLSearchParams(window.location.search).get('uuid') : null;
|
||||
// Reactive UUID — derived from $page.url so it updates on every SvelteKit client-side
|
||||
// navigation, not just full page reloads.
|
||||
// WHY: The original impl read window.location.search once (const), assuming UUID changes
|
||||
// always cause a full iframe reload (Novi impersonation behavior). But manual URL edits
|
||||
// in the same SvelteKit session keep the layout mounted and leave url_uuid stale — the
|
||||
// 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.
|
||||
// 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.
|
||||
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).
|
||||
// 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.
|
||||
let verify_in_flight = false;
|
||||
|
||||
// Failure latch — set to true when verification definitively fails (e.g. Novi returns
|
||||
// 200 with empty data for a non-existent UUID, or a 4xx response).
|
||||
// WHY: Without this, writes to $idaa_loc in the catch block trigger svelte-persisted-store
|
||||
// storage events, which cause $ae_loc to re-notify its subscribers, re-running Effect 2 in
|
||||
// an infinite loop. The UUID is fixed for this page load (read once from window.location.search),
|
||||
// so retrying will always produce the same result. User must reload to retry.
|
||||
let verify_failed = false;
|
||||
// Failure latch — stores the UUID that definitively failed (Novi 200+empty, 4xx, network error).
|
||||
// WHY (loop prevention): 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,
|
||||
// the same failing UUID would be re-verified infinitely.
|
||||
// WHY (UUID-keyed, not boolean): when the URL UUID changes (SvelteKit navigation), the new
|
||||
// UUID must be verified fresh. A plain boolean would wrongly block verification of the new UUID.
|
||||
// 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.
|
||||
// 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
|
||||
// The only reactive dependency is $ae_loc.site_cfg_json — the API key arrives async
|
||||
// via SWR background fetch and may not be populated on first render. Reading it outside
|
||||
// untrack() ensures the effect re-runs when the config loads.
|
||||
// The UUID is not reactive (read once above via window.location.search).
|
||||
// Reactive dependencies (read outside untrack):
|
||||
// - $ae_loc.site_cfg_json: API key arrives async via SWR; re-run when config loads.
|
||||
// - url_uuid ($derived from $page.url): re-run when URL UUID changes (SvelteKit navigation).
|
||||
// Everything else is inside untrack() to prevent reactive loops from store writes.
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
@@ -97,8 +106,12 @@ $effect(() => {
|
||||
const trusted_li: string[] = site_cfg.novi_trusted_li ?? [];
|
||||
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(() => {
|
||||
if (!url_uuid) {
|
||||
if (!current_uuid) {
|
||||
// No UUID in URL — non-Novi path (user/pass or shared passcode sign-in).
|
||||
$idaa_loc.novi_verified = false;
|
||||
novi_verifying = false;
|
||||
@@ -107,10 +120,10 @@ $effect(() => {
|
||||
|
||||
if (verify_in_flight) return;
|
||||
|
||||
if (verify_failed) {
|
||||
// Verification definitively failed for this UUID — do not retry.
|
||||
// See comment on verify_failed declaration for the full explanation.
|
||||
if (log_lvl) console.log(`IDAA Layout: skipping re-verification — already failed for ${url_uuid}`);
|
||||
if (verify_failed_for_uuid === current_uuid) {
|
||||
// This exact UUID already definitively failed — do not retry.
|
||||
// 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 ${current_uuid}`);
|
||||
novi_verifying = false;
|
||||
return;
|
||||
}
|
||||
@@ -120,11 +133,11 @@ $effect(() => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
$idaa_loc.novi_verified &&
|
||||
$idaa_loc.novi_uuid === url_uuid &&
|
||||
$idaa_loc.novi_uuid === current_uuid &&
|
||||
$idaa_loc.novi_verified_ts &&
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@@ -145,11 +158,11 @@ $effect(() => {
|
||||
$ae_loc.trusted_access ||
|
||||
($ae_loc.authenticated_access &&
|
||||
$idaa_loc.novi_verified &&
|
||||
$idaa_loc.novi_uuid === url_uuid);
|
||||
$idaa_loc.novi_uuid === current_uuid);
|
||||
if (!has_valid_session) {
|
||||
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}:`,
|
||||
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_email = null;
|
||||
$idaa_loc.novi_full_name = null;
|
||||
|
||||
Reference in New Issue
Block a user