import type { AcceptMsaRequest } from '../services/BAAService'; import type { MsaPreview } from './msaAgreement'; export type HeldMsaOperation = { organizationId: string; actorId: string; request: AcceptMsaRequest; preview: MsaPreview }; export const msaObject = (value: unknown): value is Record => typeof value === 'object' && value !== null; const text = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; export const msaOperationId = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value); const runs = (value: unknown): boolean => value === undefined || (Array.isArray(value) && value.every((run: unknown) => msaObject(run) && typeof run.text === 'string' && (run.bold === undefined || run.bold === true) && (run.italic === undefined || run.italic === true) && (run.href === undefined || typeof run.href === 'string'))); const block = (value: unknown): boolean => { if (!msaObject(value)) return false; if (value.type === 'list') return Array.isArray(value.items) && value.items.every((item: unknown) => typeof item === 'string') && (value.ordered === undefined || value.ordered === true) && (value.itemRuns === undefined || (Array.isArray(value.itemRuns) && value.itemRuns.every(runs))); return (value.type === 'title' || value.type === 'heading' || value.type === 'paragraph') && typeof value.text === 'string' && runs(value.runs); }; export const isMsaRequest = (value: unknown): value is AcceptMsaRequest => msaObject(value) && msaOperationId(value.operationId) && [value.signerName, value.signerTitle, value.documentVersion, value.editionHash, value.signDate, value.factsDigest].every(text) && msaObject(value.signingAttestation) && text(value.signingAttestation.statementVersion); export const sameMsaRequest = (a: AcceptMsaRequest, b: AcceptMsaRequest): boolean => a.operationId === b.operationId && a.signerName === b.signerName && a.signerTitle === b.signerTitle && a.documentVersion === b.documentVersion && a.editionHash === b.editionHash && a.signDate === b.signDate && a.factsDigest === b.factsDigest && a.signingAttestation.statementVersion === b.signingAttestation.statementVersion; export const isMsaPreview = (value: unknown): value is MsaPreview => { if (!msaObject(value) || !msaObject(value.facts)) return false; const facts = value.facts; return [value.version, value.editionHash, value.title, value.signDate, value.factsDigest].every(text) && Array.isArray(value.blocks) && value.blocks.every(block) && ['orgName', 'orgAddress', 'orgPhone'].every((key) => typeof facts[key] === 'string'); }; export const msaPreviewMatches = (preview: MsaPreview, request: AcceptMsaRequest): boolean => preview.version === request.documentVersion && preview.editionHash === request.editionHash && preview.signDate === request.signDate && preview.factsDigest === request.factsDigest; export const heldMsaKey = (organizationId: string, actorId: string): string => 'akashic.msa.operation.' + organizationId + '.' + actorId; export const heldMsaRecordKey = (organizationId: string, actorId: string, operationId: string): string => heldMsaKey(organizationId, actorId) + '.op.' + operationId; const decodeHeld = (raw: string, organizationId: string, actorId: string): HeldMsaOperation | 'unavailable' => { const value: unknown = JSON.parse(raw); return msaObject(value) && value.organizationId === organizationId && value.actorId === actorId && isMsaRequest(value.request) && isMsaPreview(value.preview) && msaPreviewMatches(value.preview, value.request) ? { organizationId, actorId, request: value.request, preview: value.preview } : 'unavailable'; }; export const readHeldMsa = (organizationId: string, actorId: string): HeldMsaOperation | 'unavailable' | undefined => { try { const raw = window.localStorage.getItem(heldMsaKey(organizationId, actorId)); if (raw) return decodeHeld(raw, organizationId, actorId); // Each act keeps its own record: a concurrent pointer clear cannot lose another window's reference. const prefix = heldMsaKey(organizationId, actorId) + '.op.'; const keys: string[] = []; for (let i = 0; i < window.localStorage.length; i += 1) { const key = window.localStorage.key(i); if (key?.startsWith(prefix)) keys.push(key); } for (const key of keys.sort()) { const retained = window.localStorage.getItem(key); if (retained) return decodeHeld(retained, organizationId, actorId); } return window.localStorage.getItem('akashic.msa.operation.' + organizationId) ? 'unavailable' : undefined; } catch { return 'unavailable'; } }; /** Only the original holder may be replaced or cleared; a late A never removes B's reference. */ export const writeHeldMsa = (organizationId: string, actorId: string, held: HeldMsaOperation | undefined, expectedOperationId?: string): boolean => { const key = heldMsaKey(organizationId, actorId); const current = readHeldMsa(organizationId, actorId); if (current === 'unavailable') throw new Error('The recovery reference could not be read.'); if (current && current.request.operationId !== (held?.request.operationId ?? expectedOperationId)) return false; if (current && held && !sameMsaRequest(current.request, held.request)) return false; if (held) { const encoded = JSON.stringify(held); const recordKey = heldMsaRecordKey(organizationId, actorId, held.request.operationId); window.localStorage.setItem(recordKey, encoded); if (window.localStorage.getItem(recordKey) !== encoded) throw new Error('The recovery reference could not be saved.'); window.localStorage.setItem(key, encoded); if (window.localStorage.getItem(key) !== encoded) throw new Error('The recovery reference could not be saved.'); } else if (current) { window.localStorage.removeItem(heldMsaRecordKey(organizationId, actorId, current.request.operationId)); window.localStorage.removeItem(key); } return true; };