import { getPractitionerRoleWorkspaces, STAFF_WORKSPACES } from './staffRoleCatalog'; import type { PractitionerRole } from 'fhir/r4'; import { isClinicianEnabled } from './clinicianEnabled'; import { scopeDigest } from './messaging/sendRecovery'; export interface ClinicianSeatCreateExpected { operationId: string; organizationId: string; practitionerId: string; roleId?: string; purpose?: 'clinical' | 'nonclinical'; nonclinicalGroup?: string; nonclinicalAdmin?: boolean; nonclinicalWorkspaces?: readonly string[]; } export interface ClinicianSeatClientScope { account: string; actor: string; pool: string; organizationId: string; practitionerId: string; deployment: string; relatedPersonId: string; subjectPatientId: string; } export const hasProtectedAdminIntent = (role: PractitionerRole, group: string): boolean => getPractitionerRoleWorkspaces(role, group).includes(STAFF_WORKSPACES.ADMIN); export const isProtectedNonclinicalRole = (role: PractitionerRole, group: string): boolean => group === 'PlatformManagers' || group === 'Admins' || hasProtectedAdminIntent(role, group); const canonicalCommand = (value: unknown): string => { if (Array.isArray(value)) return `[${value.map(canonicalCommand).join(',')}]`; if (value !== null && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalCommand((value as Record)[key])}`).join(',')}}`; return JSON.stringify(value) ?? 'null'; }; export const clinicianSeatIntentDigest = (command: unknown): Promise => { const snapshot: unknown = JSON.parse(JSON.stringify(command)); return scopeDigest(['clinician-seat-intent-v1', canonicalCommand(snapshot)]); }; export const clinicianSeatScopeDigest = (scope: ClinicianSeatClientScope): Promise => { if (!scope.account || !scope.actor || !scope.pool || !scope.deployment || !scope.organizationId || !scope.practitionerId) throw new ClinicianSeatReferenceUnavailable(); return scopeDigest(['clinician-seat-scope-v1', canonicalCommand(scope)]); }; export interface KeyedClinicianSeatCreateResult { keyed: true; protocol: 'clinician-seat-create-v1'; operationId: string; roleId: string; phase: 'admitted' | 'role-created' | 'enable-pending' | 'enabled' | 'created-unseated'; standing: 'in-progress' | 'enabled' | 'checkout-required' | 'created-unseated' | 'created' | 'unavailable'; reason?: 'price-changed' | 'not-enabled' | 'changed-after-operation'; role?: PractitionerRole; } export type LegacyClinicianSeatCreateResult = { keyed: false; standing: 'ordinary'; role: PractitionerRole }; export type ClinicianSeatCreateResult = KeyedClinicianSeatCreateResult | LegacyClinicianSeatCreateResult; export class ClinicianSeatResultUnconfirmed extends Error { constructor() { super('The role creation result could not be confirmed. Check this operation before trying again.'); this.name = 'ClinicianSeatResultUnconfirmed'; } } const object = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); const id = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9.-]{1,64}$/.test(value); const uuid = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value); export const isClinicianSeatOperationId = uuid; function roleFor(candidate: unknown, roleId: string, scope: { organizationId: string; practitionerId: string }): PractitionerRole { if (!id(scope.organizationId) || !id(scope.practitionerId) || !object(candidate) || candidate.resourceType !== 'PractitionerRole' || candidate.id !== roleId || candidate.active !== undefined && typeof candidate.active !== 'boolean' || !object(candidate.organization) || candidate.organization.reference !== `Organization/${scope.organizationId}` || !object(candidate.practitioner) || candidate.practitioner.reference !== `Practitioner/${scope.practitionerId}` || !object(candidate.meta) || typeof candidate.meta.versionId !== 'string' || !candidate.meta.versionId || candidate.extension !== undefined && (!Array.isArray(candidate.extension) || !candidate.extension.every(extension => object(extension) && typeof extension.url === 'string'))) throw new ClinicianSeatResultUnconfirmed(); return candidate as unknown as PractitionerRole; } export function clinicianSeatCreateResult(value: unknown): LegacyClinicianSeatCreateResult; export function clinicianSeatCreateResult(value: unknown, expected: ClinicianSeatCreateExpected): KeyedClinicianSeatCreateResult; export function clinicianSeatCreateResult(value: unknown, expected?: ClinicianSeatCreateExpected): ClinicianSeatCreateResult { if (!object(value)) throw new ClinicianSeatResultUnconfirmed(); if (!expected) { if (value.resourceType !== 'PractitionerRole' || !id(value.id)) throw new ClinicianSeatResultUnconfirmed(); return { keyed: false, standing: 'ordinary', role: value as unknown as PractitionerRole }; } if (value.protocol !== 'clinician-seat-create-v1' || !uuid(value.operationId) || value.operationId !== expected.operationId || !id(value.roleId) || expected.roleId !== undefined && value.roleId !== expected.roleId || typeof value.phase !== 'string' || typeof value.standing !== 'string' || !['admitted', 'role-created', 'enable-pending', 'enabled', 'created-unseated'].includes(String(value.phase)) || !['in-progress', 'enabled', 'checkout-required', 'created-unseated', 'created', 'unavailable'].includes(String(value.standing)) || value.reason !== undefined && (typeof value.reason !== 'string' || !['price-changed', 'not-enabled', 'changed-after-operation'].includes(value.reason))) throw new ClinicianSeatResultUnconfirmed(); let role: PractitionerRole | undefined; if (value.role !== undefined) role = roleFor(value.role, value.roleId, expected); if (expected.purpose === 'nonclinical' && (!['admitted', 'role-created'].includes(value.phase) || !['in-progress', 'created', 'unavailable'].includes(value.standing))) throw new ClinicianSeatResultUnconfirmed(); const intendedWorkspaces = expected.nonclinicalWorkspaces ?? (expected.nonclinicalAdmin ? [STAFF_WORKSPACES.ADMIN] : getPractitionerRoleWorkspaces(undefined, expected.nonclinicalGroup ?? 'PlatformManagers')); if (value.standing === 'created' && (expected.purpose !== 'nonclinical' || value.phase !== 'role-created' || !role || isClinicianEnabled(role) || role.meta?.tag?.filter(tag => tag.system === 'https://akashichealthrecords.com/fhir/tags/cognitoGroup').length !== 1 || !role.meta.tag.some(tag => tag.system === 'https://akashichealthrecords.com/fhir/tags/cognitoGroup' && tag.code === (expected.nonclinicalGroup ?? 'PlatformManagers')) || !isProtectedNonclinicalRole(role, expected.nonclinicalGroup ?? 'PlatformManagers') || hasProtectedAdminIntent(role, expected.nonclinicalGroup ?? 'PlatformManagers') !== (expected.nonclinicalAdmin ?? false) || JSON.stringify(getPractitionerRoleWorkspaces(role, expected.nonclinicalGroup ?? 'PlatformManagers').sort()) !== JSON.stringify([...intendedWorkspaces].sort()))) throw new ClinicianSeatResultUnconfirmed(); if (['enabled', 'checkout-required', 'created-unseated', 'created'].includes(String(value.standing)) && !role || value.standing === 'enabled' && (!isClinicianEnabled(role) || role?.active === false)) throw new ClinicianSeatResultUnconfirmed(); return { keyed: true, protocol: 'clinician-seat-create-v1', operationId: value.operationId, roleId: value.roleId, phase: value.phase as KeyedClinicianSeatCreateResult['phase'], standing: value.standing as KeyedClinicianSeatCreateResult['standing'], ...(value.reason !== undefined ? { reason: value.reason as KeyedClinicianSeatCreateResult['reason'] } : {}), ...(role ? { role } : {}) }; } export interface ClinicianSeatCheckResult { standing: 'enabled' | 'disabled' | 'pending' | 'unavailable'; role?: PractitionerRole; } export function clinicianSeatCheckResult(value: unknown, expected: { roleId: string; organizationId: string; practitionerId: string }): ClinicianSeatCheckResult { if (!object(value) || !id(expected.roleId) || typeof value.standing !== 'string' || !['enabled', 'disabled', 'pending', 'unavailable'].includes(value.standing)) throw new ClinicianSeatResultUnconfirmed(); const role = value.role !== undefined ? roleFor(value.role, expected.roleId, expected) : undefined; const enabled = isClinicianEnabled(role) && role?.active !== false; if ((value.standing === 'enabled' || value.standing === 'disabled') && !role || value.standing === 'enabled' && !enabled || value.standing === 'disabled' && enabled) throw new ClinicianSeatResultUnconfirmed(); return { standing: value.standing as ClinicianSeatCheckResult['standing'], ...(role ? { role } : {}) }; } export interface ClinicianSeatCreateReference { scope: string; intent: string; operationId: string; roleId?: string; created?: true; } export class ClinicianSeatReferenceUnavailable extends Error { constructor() { super('This role creation cannot be confirmed here. Check the existing staff record before trying again.'); this.name = 'ClinicianSeatReferenceUnavailable'; } } type ReferenceStorage = Pick; const REFERENCE_KEY = 'akashic_clinicianSeatCreate_v1'; const seenJournals = new WeakSet(); const sha256 = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value); const reference = (value: unknown): value is ClinicianSeatCreateReference => object(value) && Object.keys(value).every(key => ['scope', 'intent', 'operationId', 'roleId', 'created'].includes(key)) && sha256(value.scope) && sha256(value.intent) && uuid(value.operationId) && (value.roleId === undefined || id(value.roleId)) && (value.created === undefined || value.created === true && id(value.roleId)); const sameReference = (left: ClinicianSeatCreateReference, right: ClinicianSeatCreateReference) => left.scope === right.scope && left.intent === right.intent && left.operationId === right.operationId && left.roleId === right.roleId && left.created === right.created; export function clinicianSeatReferences(storage: ReferenceStorage | undefined, allowInitialization = true) { const read = (): ClinicianSeatCreateReference[] => { if (!storage) throw new ClinicianSeatReferenceUnavailable(); try { let raw = storage.getItem(REFERENCE_KEY); if (raw === null) { if (!allowInitialization || seenJournals.has(storage)) throw new ClinicianSeatReferenceUnavailable(); // An empty journal stays after retirement so its disappearance is never a fresh save. seenJournals.add(storage); storage.setItem(REFERENCE_KEY, JSON.stringify({ v: 1, held: [] })); raw = storage.getItem(REFERENCE_KEY); if (raw === null) throw new ClinicianSeatReferenceUnavailable(); } const parsed: unknown = JSON.parse(raw); if (!object(parsed) || parsed.v !== 1 || Object.keys(parsed).some(key => !['v', 'held'].includes(key)) || !Array.isArray(parsed.held) || parsed.held.length > 20 || !parsed.held.every(reference) || new Set(parsed.held.map(each => each.scope)).size !== parsed.held.length || new Set(parsed.held.map(each => each.operationId)).size !== parsed.held.length) throw new ClinicianSeatReferenceUnavailable(); seenJournals.add(storage); return parsed.held; } catch { throw new ClinicianSeatReferenceUnavailable(); } }; const write = (held: ClinicianSeatCreateReference[]) => { try { if (!storage) throw new ClinicianSeatReferenceUnavailable(); seenJournals.add(storage); storage.setItem(REFERENCE_KEY, JSON.stringify({ v: 1, held })); if (JSON.stringify(read()) !== JSON.stringify(held)) throw new ClinicianSeatReferenceUnavailable(); } catch { throw new ClinicianSeatReferenceUnavailable(); } }; return { held: (scope: string): ClinicianSeatCreateReference | undefined => { if (!sha256(scope)) throw new ClinicianSeatReferenceUnavailable(); return read().find(each => each.scope === scope); }, reserve: (scope: string, intent: string, mint: () => string): ClinicianSeatCreateReference => { if (!sha256(scope) || !sha256(intent)) throw new ClinicianSeatReferenceUnavailable(); const held = read(); const existing = held.find(each => each.scope === scope); if (existing) { if (existing.intent !== intent) throw new ClinicianSeatReferenceUnavailable(); return existing; } if (held.length >= 20) throw new ClinicianSeatReferenceUnavailable(); const operationId = mint(); if (!uuid(operationId) || held.some(each => each.operationId === operationId)) throw new ClinicianSeatReferenceUnavailable(); const next = { scope, intent, operationId }; write([...held, next]); return next; }, bindRole: (expected: ClinicianSeatCreateReference, roleId: string, created = true): ClinicianSeatCreateReference => { const held = read(), current = held.find(each => each.scope === expected.scope); if (!current || !sameReference(current, expected) || !id(roleId) || current.roleId !== undefined && current.roleId !== roleId) throw new ClinicianSeatReferenceUnavailable(); const next: ClinicianSeatCreateReference = { ...current, roleId, ...(created || current.created ? { created: true } : {}) }; write(held.map(each => each === current ? next : each)); return next; }, settle: (expected: ClinicianSeatCreateReference): boolean => { const held = read(); if (!held.some(each => sameReference(each, expected))) return false; write(held.filter(each => !sameReference(each, expected))); return true; }, }; }