import { environment } from '../environments/environment'; import { demoAppOrigin } from '../utils/demoEntrance'; import { readDemoSession, type DemoTabSession } from './demoTab'; import { CredentialCoordinator, indexedDbCredentialStorage, type CredentialRecord, type CredentialStorage, } from './credentialRecord'; // Decided once, when the page loads: every tab on the demo origin keeps its whole session in its own // sessionStorage, whether or not it holds a visit yet. Every other tab keeps the ordinary session in localStorage. const onDemoOrigin = typeof window !== 'undefined' && !!demoAppOrigin() && window.location.origin === demoAppOrigin(); export const demoTabAtLoad: DemoTabSession | undefined = onDemoOrigin ? readDemoSession(window.sessionStorage) : undefined; const named = (name: 'localStorage' | 'sessionStorage'): Storage => globalThis[name] ?? (typeof window !== 'undefined' ? window[name] : globalThis[name]); const store = (): Storage => named(onDemoOrigin ? 'sessionStorage' : 'localStorage'); // Resolved on each call rather than at import, so code that never touches the session needs no storage. export const authStorage: Storage = { getItem: (key) => store().getItem(key), setItem: (key, value) => store().setItem(key, value), removeItem: (key) => store().removeItem(key), clear: () => store().clear(), key: (index) => store().key(index), get length() { return store().length; }, }; /** * A demo tab's credential record lives in its own sessionStorage under the keys the demo entry writes and its exit * reads, so the ordinary store is never opened, copied or cleared there. One tab, so its writes need no coordination. */ export const demoTabCredentialStorage = (store: Storage, prefix: string, clientId: string): CredentialStorage => { let held: Omit | undefined; const name = (part: string) => prefix + '_' + part; const read = (key: string): CredentialRecord | undefined => { const accessToken = store.getItem(name('accessToken')); const idToken = store.getItem(name('idToken')); const base: CredentialRecord = held && held.key === key ? { ...held } : { schema: 1, key, identity: { origin: window.location.origin, stack: environment.credentialStack, portal: environment.sessionPortal }, generation: 0 }; if (!accessToken || !idToken) return base; let epoch = store.getItem(name('sessionId')); if (!epoch) { epoch = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); store.setItem(name('sessionId'), epoch); } const refreshToken = store.getItem(name('refreshToken')) ?? undefined; return { ...base, committed: { epoch, generation: Number(store.getItem(name('credentialGeneration')) ?? '0'), operationId: store.getItem(name('credentialOperation')) ?? 'demo-entry', tuple: { accessToken, idToken, expiresAt: Number(store.getItem(name('tokenExpiration')) ?? '0'), clientId, ...(refreshToken ? { refreshToken } : {}) }, }, }; }; return { async transact(key, change) { const out = change(read(key), { atCall: true }); if (out.next) { const { committed, ...rest } = out.next; held = rest; if (committed) { store.setItem(name('accessToken'), committed.tuple.accessToken); store.setItem(name('idToken'), committed.tuple.idToken); if (committed.tuple.refreshToken) store.setItem(name('refreshToken'), committed.tuple.refreshToken); else store.removeItem(name('refreshToken')); store.setItem(name('tokenExpiration'), String(committed.tuple.expiresAt)); store.setItem(name('sessionId'), committed.epoch); store.setItem(name('credentialGeneration'), String(committed.generation)); store.setItem(name('credentialOperation'), committed.operationId); } else { for (const part of ['accessToken', 'idToken', 'refreshToken', 'tokenExpiration', 'sessionId', 'credentialGeneration', 'credentialOperation']) { store.removeItem(name(part)); } } } return out.result; }, async read(key) { return read(key); }, subscribe: () => () => undefined, notify: () => undefined, }; }; let storageOverride: CredentialStorage | undefined; let coordinator: CredentialCoordinator | undefined; const credentialStorage = (): CredentialStorage => { if (storageOverride) return storageOverride; if (onDemoOrigin) return demoTabCredentialStorage(window.sessionStorage, environment.localStoragePrefix, demoClientFor(environment.sessionPortal === 'staff') ?? ''); return indexedDbCredentialStorage(); }; /** This page's one credential coordinator for its physical portal. */ export const credentials = (): CredentialCoordinator => { coordinator ??= new CredentialCoordinator(credentialStorage(), { origin: typeof window === 'undefined' ? '' : window.location.origin, stack: environment.credentialStack, portal: environment.sessionPortal, }); return coordinator; }; /** Test seam: swap the persistence adapter; the next `credentials()` starts from it. */ export const __setCredentialStorage = (next: CredentialStorage | undefined): void => { coordinator?.dispose(); coordinator = undefined; storageOverride = next; }; /** Which committed session this page shows; a new sign-in, even by the same account, changes it. */ export const authSessionId = (): string | null => credentials().epoch(); /** The storage area this tab's session-owned caches belong in, or undefined where there is none. */ export const authStorageArea = (): Storage | undefined => { try { return store() ?? undefined; } catch { return undefined; } }; /** The dedicated demo client for this physical mount, or undefined when the stack configures none. */ export const demoClientFor = (staff: boolean): string | undefined => environment.demoCognitoClients?.[staff ? 'staff' : 'patient'];