feat: session-expired banner via ae_auth_error store

- Add ae_auth_error writable store to ae_stores.ts
- Wire api_get_object, api_post_object, api_patch_object to set
  ae_auth_error on 401/403 (browser-only guard, never fires SSR)
- Root layout watches ae_auth_error; only raises flag_expired when
  a JWT is present (prevents false trigger on unauthenticated loads)
- Dismissible amber banner added to root layout (non-blocking, above content)
- Tested via debug menu trigger; banner fires and clears correctly
This commit is contained in:
Scott Idem
2026-03-11 16:56:07 -04:00
parent 60ca3b2f6c
commit 53c517ec30
6 changed files with 363 additions and 38 deletions

View File

@@ -1,3 +1,5 @@
import { browser } from '$app/environment';
import { ae_auth_error } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores';
/**
@@ -74,8 +76,8 @@ export const patch_object = async function patch_object({
// Handle "Bootstrap Paradox" for unauthenticated requests
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
const is_valid_bypass = bypass_val === 'bypass' ||
bypass_val === 'Nothing to See Here' ||
const is_valid_bypass = bypass_val === 'bypass' ||
bypass_val === 'Nothing to See Here' ||
params['key'] ||
bypass_val === 'direct-download';
@@ -84,7 +86,7 @@ export const patch_object = async function patch_object({
delete merged_headers['x-account-id'];
delete merged_headers['x_account_id'];
} else {
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
// but DO NOT strip the valid Account ID.
delete merged_headers['x-no-account-id'];
delete merged_headers['x_no_account_id'];
@@ -102,10 +104,10 @@ export const patch_object = async function patch_object({
}
// Auto-inject Authorization header if JWT is present but header is missing
let jwt = headers_cleaned['jwt'] ||
headers_cleaned['JWT'] ||
api_cfg['jwt'] ||
api_cfg['headers']?.['jwt'] ||
let jwt = headers_cleaned['jwt'] ||
headers_cleaned['JWT'] ||
api_cfg['jwt'] ||
api_cfg['headers']?.['jwt'] ||
api_cfg['headers']?.['JWT'];
// Final Fallback: Direct check of primary ae_loc key
@@ -185,7 +187,7 @@ export const patch_object = async function patch_object({
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
if (response.status === 401 || response.status === 403) {
console.warn(`AUTH DIAGNOSTICS (PATCH): Headers sent for ${endpoint}:`, {
has_auth: !!headers_cleaned['Authorization'],
@@ -193,6 +195,8 @@ export const patch_object = async function patch_object({
has_account_id: !!headers_cleaned['x-account-id'],
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
});
// Signal the root layout to show the session-expired banner.
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
}
// Structured Error Handling (V3): Attempt to get rich error metadata
@@ -204,9 +208,9 @@ export const patch_object = async function patch_object({
}
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
if (error_json?.meta?.details) {
return error_json;
return error_json;
}
// Fallback for standard FastAPI "detail" errors