Harden frontend auth token handling in runtime memory

This commit is contained in:
2026-03-01 21:29:11 -03:00
parent 8eaaa01186
commit a9333ec973
4 changed files with 16 additions and 69 deletions

View File

@@ -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<string, string>();
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<void> {
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<void> {
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<Response> => {
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<void> {
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;
}
}

View File

@@ -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<RequestInit, 'headers'> & { 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);
}
/**