/** * - THE STORED SESSION IS SCOPED TO A STACK, SO ITS KEY MUST BE TOO. * * The defect * `environment.ts` is one file with the staging values live at the top and the production block * commented out beneath - and **both declared `localStoragePrefix: 'akashic_dev'`**. That field is not * cosmetic: it is the key prefix for the stored session, read by `AuthService`, `OrganizationService` * and `signOutBeacon`. Two builds pointed at DIFFERENT stacks, on the SAME origin, wrote the same * localStorage keys - and the plugin runs on the practice's own WordPress origin, so "the same origin" * is the normal case rather than a contrived one. * * Nothing is at risk today and the card's own raiser corrected that: there is no production traffic, * HIS-Prod is stale on purpose, and the commented-out block gets rewritten at the cutover anyway. This * is a **precondition on two future events** - the prod cutover, and any second stack reachable from one * practice origin (the sandbox). * * WHY DERIVED RATHER THAN DECLARED * A prefix declared next to the endpoints is a value someone must remember to change when they change * the endpoints - and the collision this card exists for is precisely someone not remembering, twice. * Derived, the two cannot disagree: **change the stack and the key changes with it, whether or not * anybody thought about storage.** * * THE PROPERTY, AND ITS CONTROL, WHICH MATTER EQUALLY * Two builds pointed at DIFFERENT stacks must not share a key. * Two builds pointed at the SAME stack MUST still share one. The obvious wrong fix is a key that is * unique per build (a timestamp, a random id, a version) - it satisfies the first half, looks fixed, * and **silently breaks sign-in persistence across every rebuild.** That is why this is a pure * function of the stack's own identity and of nothing else. */ /** * The fields that identify WHICH STACK a stored session belongs to. * * Deliberately more than the Cognito pool. A token is only valid within its pool, so the pool alone * would cover `accessToken` - but the same prefix also keys `organizationId`, `practitionerRole`, * `workspace` and `loggedInAs`, which are facts about the DATA stack. Two stacks sharing an auth pool * and differing in datastore is exactly the sandbox shape, and it must not collide. */ export interface StackIdentity { cognitoDomain: string; cognitoPractitionerClient: string; cognitoPatientClient: string; cognitoRelatedPersonClient: string; apiUrl: string; fhirFoundationApi: string; fhirBaseApi: string; fhirIdentityApi: string; fhirClinicalApi: string; orgAuthorizationApi: string; fhirGuardianshipApi: string; websocketApi: string; } /** * FNV-1a, 32-bit, rendered base36. * * A hash rather than a cryptographic digest on purpose: this is a NAMESPACE, not a secret. It must be * stable, dependency-free and synchronous at module load - `crypto.subtle` is async and would turn a * constant into a promise every consumer would have to await. Nothing is being authenticated here; a * collision would merely let two stacks share a namespace, which is the state we are leaving. */ const fingerprint = (value: string): string => { let hash = 0x811c9dc5; for (let i = 0; i < value.length; i++) { hash ^= value.charCodeAt(i); // The FNV prime, as shifts - `hash * 16777619` overflows the float mantissa and stops being // deterministic across engines. hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0; } return hash.toString(36); }; /** * A short readable name for the stack, so a key in devtools says which one it belongs to. * * Read from the Cognito domain's first label: `akashichealthrecords-staging` `staging`, * `akashichealthrecords` `prod`. It is a LABEL, never the identity - two stacks could reasonably * produce the same one, which is what the fingerprint is for. Anything unrecognisable degrades to * `stack` rather than throwing: a wrong-looking label is a cosmetic problem, and a config file that * throws at import time takes the whole plugin down. */ export const stackLabel = (cognitoDomain: string): string => { const host = String(cognitoDomain ?? '') .replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') .split('/')[0] .trim() .toLowerCase(); const firstLabel = host.split('.')[0] ?? ''; const suffix = firstLabel.split('-').slice(1).join('-'); if (suffix) { // Keep it to the charset a storage key should carry, and short enough to stay readable. const clean = suffix.replace(/[^a-z0-9-]/g, '').slice(0, 24); if (clean) return clean; } // No suffix at all is the production pool - `akashichealthrecords.auth...` against // `akashichealthrecords-staging.auth...`. return firstLabel ? 'prod' : 'stack'; }; /** * The localStorage key prefix for a given stack. * * @returns `akashic_