const TOKEN_STORAGE_KEY = 'tenant_admin.token.v1'; const TOKEN_SESSION_KEY = 'tenant_admin.token.session.v1'; export type StoredToken = { accessToken: string; abilities: string[]; issuedAt: number; }; export class AuthError extends Error { constructor(public code: 'unauthenticated' | 'unauthorized', message?: string) { super(message ?? code); this.name = 'AuthError'; } } let cachedToken: StoredToken | null = null; function decodeStoredToken(raw: string | null): StoredToken | null { if (!raw) { return null; } try { const parsed = JSON.parse(raw) as StoredToken; if (!parsed || typeof parsed.accessToken !== 'string') { return null; } return { accessToken: parsed.accessToken, abilities: Array.isArray(parsed.abilities) ? parsed.abilities : [], issuedAt: typeof parsed.issuedAt === 'number' ? parsed.issuedAt : Date.now(), } satisfies StoredToken; } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to decode stored token', error); } return null; } } function readTokenFromStorage(): StoredToken | null { if (typeof window === 'undefined') { return null; } try { const sessionValue = window.sessionStorage.getItem(TOKEN_SESSION_KEY); const fromSession = decodeStoredToken(sessionValue); if (fromSession) { return fromSession; } } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to read session stored token', error); } } try { const persistentValue = window.localStorage.getItem(TOKEN_STORAGE_KEY); return decodeStoredToken(persistentValue); } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to read persisted token', error); } return null; } } export function loadToken(): StoredToken | null { if (cachedToken) { return cachedToken; } const stored = readTokenFromStorage(); cachedToken = stored; return stored; } function persistToken(token: StoredToken): void { if (typeof window === 'undefined') { cachedToken = token; return; } const serialized = JSON.stringify(token); try { window.localStorage.setItem(TOKEN_STORAGE_KEY, serialized); } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to persist tenant token to localStorage', error); } } try { window.sessionStorage.setItem(TOKEN_SESSION_KEY, serialized); } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to persist tenant token to sessionStorage', error); } } cachedToken = token; } export function storePersonalAccessToken(accessToken: string, abilities: string[]): StoredToken { const stored: StoredToken = { accessToken, abilities, issuedAt: Date.now(), }; persistToken(stored); return stored; } export function clearTokens(): void { cachedToken = null; if (typeof window === 'undefined') { return; } try { window.localStorage.removeItem(TOKEN_STORAGE_KEY); } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to remove stored tenant token', error); } } try { window.sessionStorage.removeItem(TOKEN_SESSION_KEY); } catch (error) { if (import.meta.env.DEV) { console.warn('[Auth] Failed to remove session tenant token', error); } } } type AuthFailureHandler = () => void; const authFailureHandlers = new Set(); function notifyAuthFailure(): void { authFailureHandlers.forEach((handler) => { try { handler(); } catch (error) { console.error('[Auth] Failure handler threw', error); } }); } export function registerAuthFailureHandler(handler: AuthFailureHandler): () => void { authFailureHandlers.add(handler); return () => { authFailureHandlers.delete(handler); }; } export function isAuthError(value: unknown): value is AuthError { return value instanceof AuthError; } export async function authorizedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise { const stored = loadToken(); if (!stored) { notifyAuthFailure(); throw new AuthError('unauthenticated', 'No active tenant admin token'); } const headers = new Headers(init.headers); headers.set('Authorization', `Bearer ${stored.accessToken}`); if (!headers.has('Accept')) { headers.set('Accept', 'application/json'); } const response = await fetch(input, { ...init, headers }); if (response.status === 401) { clearTokens(); notifyAuthFailure(); throw new AuthError('unauthorized', 'Token rejected by API'); } return response; }