// What a signature attempt on the practice's BAA is known to have done, and what the server shows now. // // THE PLAIN SIGN ROUTE CARRIES NO ATTEMPT IDENTITY. Only this attempt's own valid answer proves this attempt // signed; a later read proves only what the practice's agreement is now, which another signer may have made. // So an attempt whose answer was lost is kept, per attempt, until its own answer settles it - never by what a // later read shows, and never by a newer attempt. Nothing kept here names the signer or repeats the form. export const NOT_TRACKED = 'This browser could not keep track of the signature, so it was not sent.'; export const SIGN_UNCONFIRMED = 'We could not confirm whether your signature was recorded. Check before signing again.'; export const EARLIER_UNCONFIRMED = 'An earlier signature may have been recorded. Check before signing again.'; export const NOT_YET_CONFIRMED = 'Not yet confirmed: an earlier signature may still arrive.'; export const CHECK_UNAVAILABLE = 'It could not be checked just now. Try again in a moment.'; export const SIGN_AGAIN_WARNING = 'An earlier signature may still arrive. If you sign again and it does, both signatures are recorded.'; const PREFIX = 'akashic.legal-sign.v1.org-baa.'; export interface SignScope { practiceId: string; viewer: string; apiOrigin: string; } export interface SignAttemptValue { v: 1; edition: string | null; priorSignedAt: string | null; intentDigest: string | null; createdAt: string; } export interface HeldAttempt { key: string; raw: string; } export interface OwnAttempt extends HeldAttempt { attemptId: string; } const scopePrefix = (scope: SignScope): string => `${PREFIX}${encodeURIComponent(scope.practiceId)}.${encodeURIComponent(scope.viewer)}.${encodeURIComponent(scope.apiOrigin)}.`; const sessionStore = (): Storage => window.sessionStorage; const ISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-](\d{2}):(\d{2}))$/; /** A whole ISO-8601 date-time whose every part is a real calendar value; a parseable prefix is not enough. */ export const isIso = (value: unknown): value is string => { if (typeof value !== 'string') return false; const m = ISO.exec(value); if (!m) return false; const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number); const days = new Date(Date.UTC(year, month, 0)).getUTCDate(); const offsetOk = m[7] === undefined || (Number(m[7]) < 24 && Number(m[8]) < 60); return month >= 1 && month <= 12 && day >= 1 && day <= days && hour < 24 && minute < 60 && second < 60 && offsetOk && !Number.isNaN(Date.parse(value)); }; // Every attempt kept or settled in this tab is announced, so a form opened later rereads what it holds. const heldListeners = new Set<() => void>(); const announce = () => { for (const listener of [...heldListeners]) listener(); }; export const onHeldAttemptsChange = (listener: () => void): (() => void) => { heldListeners.add(listener); return () => { heldListeners.delete(listener); }; }; const validValue = (raw: string | null): boolean => { if (raw === null) return false; try { const value = JSON.parse(raw) as Partial | null; return !!value && value.v === 1 && isIso(value.createdAt) && (value.edition === null || typeof value.edition === 'string') && (value.priorSignedAt === null || isIso(value.priorSignedAt)) && (value.intentDigest === null || typeof value.intentDigest === 'string'); } catch { return false; } }; /** Writes the value and reads it back; only a byte-exact readback counts as kept. */ const keep = (store: Storage, key: string, raw: string): boolean => { try { store.setItem(key, raw); return store.getItem(key) === raw; } catch { return false; } }; /** * Kept before anything is awaited, so a reload while the request is still out finds it. Undefined when this * browser cannot keep it: the signature is then not sent at all. */ export const reserveAttempt = (scope: SignScope, value: SignAttemptValue, store: Storage = sessionStore()): OwnAttempt | undefined => { const attemptId = crypto.randomUUID(); const key = `${scopePrefix(scope)}${attemptId}`; const raw = JSON.stringify(value); const kept = keep(store, key, raw); announce(); return kept ? { key, raw, attemptId } : undefined; }; /** Replaces this attempt's own value only while it is still the one it wrote; the stored value it ends with. */ export const updateAttempt = (own: OwnAttempt, value: SignAttemptValue, store: Storage = sessionStore()): OwnAttempt => { try { if (store.getItem(own.key) !== own.raw) return own; } catch { return own; } const raw = JSON.stringify(value); return keep(store, own.key, raw) ? { ...own, raw } : own; }; /** Settles only this attempt, and only while its stored value is exactly what it wrote; false leaves it held. */ export const settleAttempt = (own: HeldAttempt, store: Storage = sessionStore()): boolean => { try { if (store.getItem(own.key) !== own.raw) return false; store.removeItem(own.key); const gone = store.getItem(own.key) === null; announce(); return gone; } catch { return false; } }; export type HeldRead = { held: HeldAttempt[]; unreadable: boolean }; /** Every attempt kept for this practice, viewer and API. A value that does not parse is held, never dropped. */ export const readHeldAttempts = (scope: SignScope, store?: Storage): HeldRead => { const prefix = scopePrefix(scope); try { const source = store ?? sessionStore(); const held: HeldAttempt[] = []; let unreadable = false; for (let index = 0; index < source.length; index += 1) { const key = source.key(index); if (!key?.startsWith(prefix)) continue; const raw = source.getItem(key); if (!validValue(raw)) unreadable = true; held.push({ key, raw: raw ?? '' }); } return { held, unreadable }; } catch { return { held: [], unreadable: true }; } }; const hex = (buffer: ArrayBuffer): string => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, '0')).join(''); /** What was asked for, as a digest only: instrument, practice, edition and the signer's particulars. */ export const intentDigest = async (parts: readonly (string | null | undefined)[]): Promise => hex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(['org-baa', ...parts.map((part) => part ?? null)])))); /** The documentKey the sign route writes for this practice: `baa//`, whole components only. */ const ownKey = (documentKey: unknown, practiceId: string): boolean => { if (typeof documentKey !== 'string') return false; const parts = documentKey.split('/'); return parts.length === 3 && parts[0] === 'baa' && parts[1] === practiceId && parts[2].length > 0; }; export interface SignReceipt { status: 'active'; signedAt: string; documentKey: string; } /** This attempt's own answer, only when it is the route's real success for the practice it was sent for. */ export const signReceiptOf = (body: unknown, practiceId: string): SignReceipt | undefined => { const value = body as Partial | null | undefined; if (!value || typeof value !== 'object' || value.status !== 'active' || !isIso(value.signedAt)) return undefined; if (!ownKey(value.documentKey, practiceId)) return undefined; return { status: 'active', signedAt: value.signedAt, documentKey: value.documentKey as string }; }; export type BaaObservation = | { kind: 'signed'; signedAt: string; documentKey: string; edition: string | null; record: Record } | { kind: 'unsigned'; record: Record } | { kind: 'unknown' }; const isObject = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const has = (record: Record, field: string): boolean => Object.prototype.hasOwnProperty.call(record, field) && record[field] !== undefined; const optionalString = (record: Record, field: string): boolean => !has(record, field) || typeof record[field] === 'string'; // A supplied signer names its parts as text; an absent part is a legacy row, not a malformed one. const signerOk = (value: unknown): boolean => isObject(value) && optionalString(value, 'name') && optionalString(value, 'title') && optionalString(value, 'orgName'); const documentOk = (value: unknown): boolean => value === null || (isObject(value) && typeof value.version === 'string' && value.version.length > 0 && typeof value.title === 'string' && Array.isArray(value.blocks)); const pendingOk = (value: unknown, practiceId: string): boolean => value === null || (isObject(value) && isIso(value.signedAt) && ownKey(value.documentKey, practiceId) && (!has(value, 'signedBy') || signerOk(value.signedBy))); // Every supplied fact the route can give, checked before any of it is used; an absent optional one is fine. const suppliedFactsOk = (record: Record, practiceId: string): boolean => (!has(record, 'pending') || pendingOk(record.pending, practiceId)) && (!has(record, 'document') || documentOk(record.document)) && (!has(record, 'documentVersion') || record.documentVersion === null || typeof record.documentVersion === 'string') && (!has(record, 'signedBy') || signerOk(record.signedBy)) && (!has(record, 'signedAt') || isIso(record.signedAt)) && (!has(record, 'documentKey') || ownKey(record.documentKey, practiceId)) && (!has(record, 'activatedAt') || isIso(record.activatedAt)); /** * What a read of this practice's agreement shows. Unsigned only as the route says it - its own explicit * `status: null`. Signed only as an active row of this practice's own key. A pending-only row, any other * status, or any malformed supplied fact is not known. */ export const observeBaa = (body: unknown, practiceId: string): BaaObservation => { if (!isObject(body) || !Object.prototype.hasOwnProperty.call(body, 'status')) return { kind: 'unknown' }; const record = body; if (!suppliedFactsOk(record, practiceId)) return { kind: 'unknown' }; if (record.status === null) return { kind: 'unsigned', record }; if (record.status !== 'active' || !isIso(record.signedAt) || !ownKey(record.documentKey, practiceId)) { return { kind: 'unknown' }; } const edition = typeof record.documentVersion === 'string' && record.documentVersion ? record.documentVersion : null; return { kind: 'signed', signedAt: record.signedAt, documentKey: record.documentKey as string, edition, record }; }; /** What a parent is told: this attempt's own receipt, or only what a later read shows the agreement to be. */ export type SignProof = | { proof: 'receipt'; receipt: SignReceipt; standing: ReceiptStanding } | { proof: 'observed-standing' }; /** How a known receipt stands against the read that followed it. The receipt itself is never undone by it. */ export type ReceiptStanding = 'corroborated' | 'confirmation-pending' | 'other-current'; /** A recorded signature alone does not answer which agreement is currently in force. */ export const observedBaaActive = (record: { status: unknown; currentUnconfirmed?: true } | null | undefined): boolean | undefined => record == null || record.currentUnconfirmed ? undefined : record.status === 'active'; /** * The record a parent keeps after a signature: what was observed now, with this attempt's receipt as its own * fact beside it. A receipt with no current read carries nothing of the earlier agreement's signature or copy - * only the publication and the separate MSA standing, which no BAA signature changes. */ export const signedRecordFor = (previous: R | null | undefined, record: R, proof?: SignProof): R => { if (!proof || proof.proof !== 'receipt' || proof.standing === 'corroborated') return record; if (proof.standing === 'other-current') return { ...record, ownReceipt: proof.receipt }; const before = (previous ?? {}) as Record; const kept: Record = {}; for (const field of ['msa', 'document', 'pdfTemplateUrl', 'htmlTemplateUrl']) { if (has(before, field)) kept[field] = before[field]; } return { ...kept, ...record, ownReceipt: proof.receipt, currentUnconfirmed: true } as R; }; export const receiptStanding = (receipt: SignReceipt, observation: BaaObservation): ReceiptStanding => { if (observation.kind === 'unknown' || observation.kind === 'unsigned') return 'confirmation-pending'; return observation.signedAt === receipt.signedAt && observation.documentKey === receipt.documentKey ? 'corroborated' : 'other-current'; };