// The access group the token lists for a practice's administrators (pre-token organization_roles). const ADMINS_GROUP = 'Admins'; /** Whether this access token lists the Admins group for the practice; undefined when it cannot be read. */ export function tokenGrantsAdmin(accessToken: string | null | undefined, organizationId: string | undefined): boolean | undefined { if (!accessToken || !organizationId) return undefined; try { const payload = accessToken.split('.')[1]; if (!payload) return undefined; const claims = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); const raw = claims?.organization_roles; if (raw === undefined || raw === null) return undefined; const roles = typeof raw === 'string' ? JSON.parse(raw) : raw; const named = roles?.[organizationId]; return Array.isArray(named) && named.includes(ADMINS_GROUP); } catch { return undefined; } } export type AdminGrantOutcome = /** A renewed token carries Admin for the practice and the profile reloaded with it. */ | 'granted' /** The grant never showed in a renewed token within the attempts; the session is unchanged. */ | 'pending' /** The token carries Admin, but the profile could not be re-read; the session is kept. */ | 'reload'; interface SettleDeps { organizationId: string; reMint: () => Promise; /** Re-reads the signed-in person without ever ending the session; false when the read failed. */ reloadProfile: () => Promise; wait: (ms: number) => Promise; attempts?: number; intervalMs?: number; } // The server writes the Admin role and group before it answers, but the token is minted from a role // search that can trail that write, so a renewed token is checked for the grant, a few times, bounded. export async function settleAdminGrant(deps: SettleDeps): Promise { const attempts = deps.attempts ?? 4; for (let attempt = 0; attempt < attempts; attempt++) { if (attempt > 0) await deps.wait(deps.intervalMs ?? 1500); const token = await deps.reMint().catch(() => null); if (tokenGrantsAdmin(token, deps.organizationId) === true) { const loaded = await deps.reloadProfile().catch(() => false); return loaded ? 'granted' : 'reload'; } } return 'pending'; }