fix(bootstrap): validate access_key server-side, prevent stale cache bypass

When a URL access_key is present, skip the Dexie cache fast-path in
lookup_site_domain entirely — the key must be validated against the API.
Previously, a stale cached entry with a previously-valid key would be
returned immediately, allowing access even after the key changed or
was revoked in the URL.

Also: add site_domain_access_key to properties_to_save__site_domain
so domain-level keys are persisted to Dexie for cache validation;
remove shadow access_key re-declaration in +layout.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-31 15:07:41 -04:00
parent 84dc3dd158
commit e6daf6b503
3 changed files with 76 additions and 38 deletions

View File

@@ -112,29 +112,33 @@ export async function lookup_site_domain({
console.log(`*** lookup_site_domain() *** fqdn=${fqdn} (Cache-First)`);
}
// 1. FAST PATH: Check local cache first
let cached = null;
try {
cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
if (cached) {
if (log_lvl)
console.log(
'BOOTSTRAP: Cache hit. Returning cached site domain immediately.'
);
// 1. FAST PATH: Check local cache first.
// Skip when access_key is provided — the key must be validated server-side.
// A stale cached entry with a previously-valid key must not grant access if the
// URL key has changed or been revoked.
if (!access_key) {
try {
const cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
if (cached) {
if (log_lvl)
console.log(
'BOOTSTRAP: Cache hit. Returning cached site domain immediately.'
);
// Trigger background refresh to keep cache fresh, but don't await it
_refresh_site_domain_background({
api_cfg,
fqdn,
view,
log_lvl: 0,
access_key
});
// Trigger background refresh to keep cache fresh, but don't await it
_refresh_site_domain_background({
api_cfg,
fqdn,
view,
log_lvl: 0,
access_key
});
return cached as any;
return cached as ae_SiteDomain;
}
} catch (err) {
console.warn('BOOTSTRAP: Cache read failed.', err);
}
} catch (err) {
console.warn('BOOTSTRAP: Cache read failed.', err);
}
// 2. SLOW PATH: Wait for API if cache is empty
@@ -730,6 +734,7 @@ const properties_to_save__site_domain = [
'account_name',
'fqdn',
'access_key',
'site_domain_access_key',
'enable',
'enable_from',
'enable_to',

View File

@@ -139,9 +139,46 @@ export async function load({ fetch, params, parent, route, url }) {
if (cached) {
if (log_lvl)
console.log(
'ROOT LOAD: Found cached site domain. Unblocking layout.'
'ROOT LOAD: Found cached site domain. Evaluating cache vs URL key.'
);
result = cached;
// If the URL contains an access_key, we must ensure the cached
// entry is valid for that key. If the cache has no keys or the
// keys don't match, force a live lookup to revalidate.
if (access_key) {
const cachedSiteKey =
(cached as any).access_key ||
(cached as any).site_access_key ||
(cached as any).site_domain_access_key ||
'';
const cachedDomainKey =
(cached as any).site_domain_access_key ||
(cached as any).site_access_key ||
(cached as any).access_key ||
'';
if (!cachedSiteKey && !cachedDomainKey) {
if (log_lvl)
console.log(
'BOOTSTRAP: Cache has no access keys; forcing live lookup because URL contains a key.'
);
// leave `result` null so slow-path lookup runs
} else if (
access_key === cachedSiteKey ||
access_key === cachedDomainKey
) {
if (log_lvl)
console.log('BOOTSTRAP: URL key matches cached access key; using cached object.');
result = cached as any;
} else {
if (log_lvl)
console.log('BOOTSTRAP: URL key does not match cached keys; forcing live lookup to revalidate.');
// leave `result` null so slow-path lookup runs
}
} else {
// No access_key in URL — safe to use cached value
result = cached as any;
}
}
} catch (e) {
if (log_lvl)
@@ -370,8 +407,6 @@ export async function load({ fetch, params, parent, route, url }) {
ae_loc_init['key_checked'] = true;
ae_loc_init['allow_access'] = true;
} else {
const access_key = url.searchParams.get('key');
if (access_key) {
if (log_lvl) {
console.log(`root +layout.ts: access_key = ${access_key}`);