/** * THE SECOND TOKEN STORE, WHICH NOTHING IN THIS APP READS AND NOTHING IN THIS APP WAS CLEARING. * * `amazon-cognito-identity-js` caches a whole session in `localStorage` of its own accord the moment a * password sign-in succeeds - `CognitoIdentityServiceProvider..LastAuthUser` plus an * `idToken`, `accessToken`, `refreshToken` and `clockDrift` under the username. We never asked it to and * we never read it: the app keeps its own copy under the portal prefix and translates the library's * session at the boundary. * * MEASURED ON A REAL STAFF BROWSER, 2026-09-02. Signing out cleared every * `akashic_staging_..._staff_*` key and a reload correctly landed on the sign-in form - and five * `CognitoIdentityServiceProvider.*` keys were still sitting there, including a refresh token, which * lasts seven days. The app will not restore from them, so the screen is honest; but the front desk is a * shared machine and a refresh token in its storage is a credential, whether or not our own code is the * thing that would spend it. * * TWO HALVES, AND THEY ANSWER DIFFERENT QUESTIONS. The sweep cleans a browser that has already been * signed in on; the in-memory store stops the write happening at all, which is what covers the person * who closes the laptop instead of signing out. */ /** The library's own namespace. Every key it writes begins with this. */ export const COGNITO_LIBRARY_KEY_PREFIX = 'CognitoIdentityServiceProvider.'; /** * The storage the library gets from us: one that forgets. * * IT CANNOT BREAK A SIGN-IN, and that is worth stating because it looks like it should. The session is * handed to `authenticateUser`'s success callback directly; the cache is written afterwards and only * ever read by `getCurrentUser`/`getSession`, neither of which we call. What the library persists is * therefore pure residue - the same tokens, in a second place, under keys nothing consults. * * A fresh object per sign-in rather than a module-level singleton, so two sign-ins on one page cannot * see each other's entries. */ export const memoryCognitoStorage = () => { const entries = new Map(); return { setItem: (key: string, value: string) => { entries.set(key, value); }, getItem: (key: string) => entries.get(key) ?? null, removeItem: (key: string) => { entries.delete(key); }, clear: () => { entries.clear(); }, }; }; /** * Remove one Cognito app client's cached session from the library's store. * * SCOPED TO THE CLIENT ID, NOT TO THE WHOLE NAMESPACE, for the same reason `clearPortalSession` is * scoped to one portal prefix: the patient portal, the related-person client and the practitioner client * are three different app clients sharing this one namespace, and the two portals hold sessions at the * same time by design. Sweeping `CognitoIdentityServiceProvider.` wholesale would sign a staff logout * into the patient session sitting beside it. * * Collects before removing: `removeItem` during a live index walk re-indexes the remaining entries and * silently skips every other match - the same trap `clearPortalSession` documents. * * Returns how many keys were removed, so a test can assert the sweep reached something rather than * asserting the absence of keys that may never have been there. */ export const clearCognitoLibrarySession = ( clientId: string, storage: Pick, ): number => { try { // THE TRAILING DOT IS LOAD-BEARING AND IS WHY THERE IS NO EMPTY-CLIENT-ID GUARD HERE. With it, an // empty id yields `CognitoIdentityServiceProvider..`, which matches nothing; without it, it // yields the bare namespace and sweeps every app client at once - the wholesale clear this // function is scoped to avoid, arriving silently and looking like a thorough sign-out. A guard // was written first and a mutant survived it, because the dot had already done the work. const prefix = `${COGNITO_LIBRARY_KEY_PREFIX}${clientId}.`; const doomed: string[] = []; for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index); if (key?.startsWith(prefix)) doomed.push(key); } doomed.forEach((key) => storage.removeItem(key)); return doomed.length; } catch { // Storage unavailable - private mode, or a quota. Nothing was cached in that case either, so // there is nothing left behind; taking the sign-out down with it would be the worse outcome. return 0; } };