export interface PracticeRemovalIntent { schema: 1; operationId: string; owner: string; organizationId: string; contractorId: string; createdAt: string; name: string; } export interface PracticeRemovalOutcome { operationId: string; kind: 'fenced' | 'unknown'; cleanup?: 'complete' | 'pending'; handoff?: 'pending' | 'unknown' | 'acknowledged'; originalActor?: { pool: string; sub: string }; admittedAt?: string; fencedGeneration?: string; revision?: number; } const object = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const text = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; const uuid = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); const date = (value: unknown): value is string => text(value) && Number.isFinite(Date.parse(value)); export function parsePracticeRemoval(intent: PracticeRemovalIntent, raw: unknown): PracticeRemovalOutcome { const unknown: PracticeRemovalOutcome = { operationId: intent.operationId, kind: 'unknown' }; if (!object(raw) || raw.protocol !== 'agency-removal-v1' || raw.kind !== 'link-revoke' || raw.operationId !== intent.operationId || raw.organizationId !== intent.organizationId || raw.contractorId !== intent.contractorId || raw.accountId !== undefined || raw.expectedRoleId !== undefined || raw.assignmentOperationId !== undefined || raw.originalGenerationFenced !== true || !text(raw.fencedGeneration) || raw.currentStanding !== 'requires-current-read' || !Number.isInteger(raw.revision) || Number(raw.revision) < 0 || !date(raw.admittedAt) || !object(raw.originalActor) || !text(raw.originalActor.pool) || !text(raw.originalActor.sub) || !object(raw.audit) || raw.audit.stored !== 'unverified' || !['pending', 'unknown', 'acknowledged'].includes(String(raw.audit.handoff)) || !['complete', 'pending'].includes(String(raw.cleanup)) || typeof raw.enumerationComplete !== 'boolean' || !Number.isInteger(raw.discoveredPages) || !Number.isInteger(raw.completedPages) || Number(raw.completedPages) < 0 || Number(raw.discoveredPages) < Number(raw.completedPages) || (raw.cleanup === 'complete') !== (raw.enumerationComplete && raw.completedPages === raw.discoveredPages)) return unknown; return { operationId: intent.operationId, kind: 'fenced', cleanup: raw.cleanup as 'complete' | 'pending', handoff: raw.audit.handoff as PracticeRemovalOutcome['handoff'], originalActor: { pool: raw.originalActor.pool, sub: raw.originalActor.sub }, admittedAt: raw.admittedAt, fencedGeneration: raw.fencedGeneration, revision: raw.revision as number }; } const key = (owner: string) => 'akashic:practice-agency-removals:v1:' + encodeURIComponent(owner); export function readPracticeRemovals(store: Storage, owner: string): PracticeRemovalIntent[] { const raw = store.getItem(key(owner)); if (raw === null) return []; const parsed: unknown = JSON.parse(raw); if (!Array.isArray(parsed) || parsed.length > 50 || parsed.some(row => !object(row) || row.schema !== 1 || row.owner !== owner || !uuid(row.operationId) || !text(row.organizationId) || !text(row.contractorId) || !date(row.createdAt) || !text(row.name)) || new Set(parsed.map(row => row.operationId)).size !== parsed.length) throw new Error('The saved requests could not be verified.'); return parsed as PracticeRemovalIntent[]; } export function retainPracticeRemoval(store: Storage, intent: PracticeRemovalIntent): PracticeRemovalIntent[] { const rows = readPracticeRemovals(store, intent.owner); const previous = rows.find(row => row.operationId === intent.operationId); if (previous && JSON.stringify(previous) !== JSON.stringify(intent)) throw new Error('The original request changed.'); if (!previous) { if (rows.length >= 50) throw new Error('Saved requests need checking before another can be retained.'); rows.push(intent); } store.setItem(key(intent.owner), JSON.stringify(rows)); const confirmed = readPracticeRemovals(store, intent.owner); if (!confirmed.some(row => JSON.stringify(row) === JSON.stringify(intent))) throw new Error('The request could not be retained.'); return confirmed; }