diff --git a/doc/frontend-design-foundation.md b/doc/frontend-design-foundation.md index 3f79600..39a5973 100644 --- a/doc/frontend-design-foundation.md +++ b/doc/frontend-design-foundation.md @@ -52,7 +52,8 @@ Do not hardcode new palette or spacing values in component styles when a token a ## Authenticated Media Delivery - Document previews and thumbnails must load through authenticated fetch flows in `frontend/src/lib/api.ts`, then render via temporary object URLs. -- Runtime auth uses server-issued per-user session tokens persisted with `setRuntimeApiToken` and read by `getRuntimeApiToken`. +- Runtime auth keeps server-issued per-user session tokens only in active-tab memory via `setRuntimeApiToken` and `getRuntimeApiToken`. +- Users must sign in again after a full browser reload, new tab launch, or browser restart because tokens are not persisted in browser storage. - Static build-time token distribution is not supported. - Direct `window.open` calls for protected media endpoints are not allowed because browser navigation requests do not include the API token header. - Download actions for original files and markdown exports must use authenticated blob fetches plus controlled browser download triggers. diff --git a/doc/operations-and-configuration.md b/doc/operations-and-configuration.md index f6556fe..53036fb 100644 --- a/doc/operations-and-configuration.md +++ b/doc/operations-and-configuration.md @@ -56,7 +56,7 @@ docker compose logs -f - `AUTH_LOGIN_FAILURE_WINDOW_SECONDS` - `AUTH_LOGIN_LOCKOUT_BASE_SECONDS` - `AUTH_LOGIN_LOCKOUT_MAX_SECONDS` -- Frontend signs in through `/api/v1/auth/login` and stores issued session token in browser session storage. +- Frontend signs in through `/api/v1/auth/login` and keeps issued session token only in active runtime memory. ## DEV And LIVE Configuration Matrix @@ -119,7 +119,8 @@ Recommended LIVE pattern: ## Frontend Runtime - Frontend no longer consumes `VITE_API_TOKEN`. -- Session token storage key is `dcm.access_token` in browser session storage. +- Session tokens are not persisted to browser storage. +- Users must sign in again after full page reload, opening a new tab, or browser restart. - Protected media and file download flows still use authenticated fetch plus blob/object URL handling. ## Validation Checklist diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index c96a27f..d1a9210 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -48,46 +48,13 @@ function toRequestUrl(input: RequestInfo | URL): string { return input.url; } -/** - * Creates a minimal session storage implementation for Node-based tests. - */ -function createMemorySessionStorage(): Storage { - const values = new Map(); - return { - get length(): number { - return values.size; - }, - clear(): void { - values.clear(); - }, - getItem(key: string): string | null { - return values.has(key) ? values.get(key) ?? null : null; - }, - key(index: number): string | null { - return Array.from(values.keys())[index] ?? null; - }, - removeItem(key: string): void { - values.delete(key); - }, - setItem(key: string, value: string): void { - values.set(key, String(value)); - }, - }; -} - /** * Runs API helper tests for authenticated media and auth session workflows. */ async function runApiTests(): Promise { const originalFetch = globalThis.fetch; - const sessionStorageDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage'); try { - Object.defineProperty(globalThis, 'sessionStorage', { - configurable: true, - writable: true, - value: createMemorySessionStorage(), - }); setRuntimeApiToken(null); const requestUrls: string[] = []; @@ -115,7 +82,7 @@ async function runApiTests(): Promise { assert(requestAuthHeaders[1] === null, `Expected no auth header for preview request, got "${requestAuthHeaders[1]}"`); setRuntimeApiToken('session-user-token'); - assert(getRuntimeApiToken() === 'session-user-token', 'Expected session token readback to match persisted token'); + assert(getRuntimeApiToken() === 'session-user-token', 'Expected runtime token readback to match active token'); globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit): Promise => { const authHeader = new Headers(init?.headers).get('Authorization'); assert(authHeader === 'Bearer session-user-token', `Expected session token auth header, got "${authHeader}"`); @@ -203,11 +170,6 @@ async function runApiTests(): Promise { await assertRejects(async () => downloadDocumentContentMarkdown('doc-4'), 'Failed to download document markdown'); } finally { setRuntimeApiToken(null); - if (sessionStorageDescriptor) { - Object.defineProperty(globalThis, 'sessionStorage', sessionStorageDescriptor); - } else { - delete (globalThis as { sessionStorage?: Storage }).sessionStorage; - } globalThis.fetch = originalFetch; } } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2698e9f..92e9fbb 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -36,9 +36,11 @@ function resolveApiBase(): string { const API_BASE = resolveApiBase(); /** - * Session storage key used for per-user runtime token persistence. + * In-memory bearer token scoped to the active frontend runtime. + * + * This value is intentionally not persisted to browser storage. */ -export const API_TOKEN_RUNTIME_STORAGE_KEY = 'dcm.access_token'; +let runtimeApiToken: string | undefined; type ApiRequestInit = Omit & { headers?: HeadersInit }; @@ -56,45 +58,26 @@ function normalizeBearerToken(candidate: unknown): string | undefined { } /** - * Resolves bearer token persisted for current browser session. + * Resolves bearer token for the active browser runtime. */ export function getRuntimeApiToken(): string | undefined { - if (typeof globalThis.sessionStorage === 'undefined') { - return undefined; - } - try { - return normalizeBearerToken(globalThis.sessionStorage.getItem(API_TOKEN_RUNTIME_STORAGE_KEY)); - } catch { - return undefined; - } + return runtimeApiToken; } /** - * Resolves bearer token from authenticated browser-session storage. + * Resolves bearer token from active runtime memory. */ function resolveApiToken(): string | undefined { return getRuntimeApiToken(); } /** - * Stores or clears the per-user runtime API token in session storage. + * Stores or clears the per-user runtime API token in memory. * - * @param token Token value to persist for this browser session; clears persisted token when empty. + * @param token Token value for the active runtime; clears token when empty. */ export function setRuntimeApiToken(token: string | null | undefined): void { - if (typeof globalThis.sessionStorage === 'undefined') { - return; - } - try { - const normalized = normalizeBearerToken(token); - if (normalized) { - globalThis.sessionStorage.setItem(API_TOKEN_RUNTIME_STORAGE_KEY, normalized); - return; - } - globalThis.sessionStorage.removeItem(API_TOKEN_RUNTIME_STORAGE_KEY); - } catch { - return; - } + runtimeApiToken = normalizeBearerToken(token); } /**