import { CognitoIdentityProviderClient, GetTokensFromRefreshTokenCommand } from '@aws-sdk/client-cognito-identity-provider'; import { environment } from '../environments/environment'; import { DEMO_ROLES } from '../utils/demoEntrance'; import { dpopHeaderFor } from '../utils/dpopRequestHeader'; import { readDpopJkt } from '../utils/dpopSession'; import { demoClientFor } from './authStorage'; import type { DemoOperation } from './demoAllowance'; import { demoClaimsFrom, isEndingRefusal, type DemoTabSession } from './demoTab'; export type DemoStanding = 'active' | 'ended' | 'mismatch' | 'unavailable'; // One absolute allowance for confirming a visit on load; past it the tab offers to check again. export const DEMO_STANDING_BUDGET_MS = 8_000; const tokenKey = (name: string) => environment.localStoragePrefix + '_' + name; const live = (session: DemoTabSession) => Date.parse(session.expiresAt) > Date.now(); // A refresh the identity provider refuses as no longer authorized ends the visit; anything else it says - // throttling, limits, an outage - only means the visit could not be confirmed yet. const refreshRefused = (error: unknown) => (error as { name?: string })?.name === 'NotAuthorizedException'; const refreshed = async (store: Storage, session: DemoTabSession, operation: DemoOperation): Promise => { const refreshToken = store.getItem(tokenKey('refreshToken')); const clientId = demoClientFor(DEMO_ROLES[session.role].mount === 'staff'); const jkt = readDpopJkt(store.getItem(tokenKey('accessToken'))); if (!refreshToken || !clientId || !jkt) return 'unusable'; const client = new CognitoIdentityProviderClient({ region: environment.region }); operation.admit(); const result = await operation.settle(client.send( new GetTokensFromRefreshTokenCommand({ ClientId: clientId, RefreshToken: refreshToken, ClientMetadata: { dpop_jkt: jkt } }), { abortSignal: operation.signal }, )); const tokens = result.AuthenticationResult; if (!tokens?.AccessToken || !tokens.IdToken || !tokens.ExpiresIn) throw new Error('The refresh answered without tokens.'); store.setItem(tokenKey('accessToken'), tokens.AccessToken); store.setItem(tokenKey('idToken'), tokens.IdToken); if (tokens.RefreshToken) store.setItem(tokenKey('refreshToken'), tokens.RefreshToken); store.setItem(tokenKey('tokenExpiration'), String(Date.now() + tokens.ExpiresIn * 1000)); return tokens.AccessToken; }; /** * Asks the server whether this tab's visit is still live: capability, token generation and role, and clinic * must all agree with the lease. Activation is idempotent for an active matching lease, so a reload confirms * the same visit without leasing again. Every wait settles within the operation's allowance; only a current * answer, received while the visit is still within its expiry, is active. */ export const confirmDemoStanding = async ( session: DemoTabSession, store: Storage, operation: DemoOperation, ): Promise => { const claims = demoClaimsFrom(store.getItem(tokenKey('idToken'))); if (!claims || claims.role !== session.role || claims.generation !== session.generation) return 'mismatch'; if (!live(session)) return 'ended'; try { const expiresAt = Number(store.getItem(tokenKey('tokenExpiration')) ?? '0'); const accessToken = Date.now() < expiresAt ? store.getItem(tokenKey('accessToken')) : await refreshed(store, session, operation); if (!accessToken || accessToken === 'unusable') return 'mismatch'; const after = demoClaimsFrom(store.getItem(tokenKey('idToken'))); if (!after || after.role !== session.role || after.generation !== session.generation) return 'mismatch'; const url = environment.apiUrl + '/demo/activate'; const proof = await operation.settle(dpopHeaderFor('POST', url)); operation.admit(); const response = await operation.settle(fetch(url, { method: 'POST', signal: operation.signal, // Named like every request of the visit: without its clinic, a staff role resolves to no practice and is refused. headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + accessToken, 'X-Organization-ID': session.clinicId, ...proof }, body: JSON.stringify({ leaseId: session.leaseId, leaseSecret: session.leaseSecret }), })); const body = await operation.settle(response.json().catch(() => ({}))); if (isEndingRefusal(response.status, body?.code)) return 'ended'; if (!response.ok) return 'unavailable'; if (body?.clinicId !== session.clinicId || body?.role !== session.role) return 'mismatch'; operation.admit(); return live(session) ? 'active' : 'ended'; } catch (error) { return refreshRefused(error) ? 'ended' : 'unavailable'; } };