import type { Condition, Extension } from 'fhir/r4'; /** What the content-class route answers: the record as now stored, the edition classified and where it was recorded. */ export interface ContentClassResult { condition: Condition; class: ContentClass; sourceVersion: string; recordedVersion: string; reconciled?: boolean; } // What a Condition says about its content class and its author, read the way the server writes it (assertion schema // version 1). The server decides; this only shows what is stored and whether to offer the author the action. const EXTENSIONS = 'https://akashichealthrecords.com/fhir/extensions'; export const CONTENT_CLASS_ASSERTION_URL = `${EXTENSIONS}/content-class-assertion`; export const SHARE_ORIGIN_EVIDENCE_URL = `${EXTENSIONS}/share-origin-evidence`; export const SHARE_CONTENT_CLASS_SYSTEM = 'https://akashic.health/fhir/CodeSystem/share-content-class'; export const CONTENT_CLASSES = ['ordinary', 'psychotherapy-note', 'sud-counseling-note'] as const; export type ContentClass = (typeof CONTENT_CLASSES)[number]; export type ConditionSource = 'my-own-entry' | 'from-elsewhere'; export const CONTENT_CLASS_WORDS: Record = { ordinary: { label: 'Ordinary clinical content', detail: 'Not a psychotherapy note or a substance use counseling note.' }, 'psychotherapy-note': { label: 'Psychotherapy note', detail: 'Kept apart from the rest of the record and never shared without its own authorization.' }, 'sud-counseling-note': { label: 'Substance use counseling note', detail: 'Restricted the same way, under its own label.' }, }; export type Classification = | { kind: 'classified'; contentClass: ContentClass; sourceVersion: string; recordedVersion?: string } | { kind: 'unclassified' } | { kind: 'unreadable' }; const sub = (extension: Extension, url: string) => extension.extension?.find((part) => part.url === url); export interface ContentAssertion { assertionId: string; class: ContentClass; sourceVersion: string; digestSchema: 'condition-content-v1'; contentDigest: string; author: string; organization: string; recordedAt: string; } const ASSERTION_VALUES = { schemaVersion: 'valueString', assertionId: 'valueString', class: 'valueCode', sourceVersion: 'valueString', digestSchema: 'valueString', contentDigest: 'valueString', author: 'valueReference', organization: 'valueReference', recordedAt: 'valueInstant', } as const; const object = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value); const version = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9.-]{1,64}$/.test(value); function isInstant(value: unknown): value is string { if (typeof value !== 'string') return false; const match = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d+)?(Z|[+-](?:0\d|1[0-4]):[0-5]\d)$/.exec(value); if (!match) return false; const [, year, month, day, , , , zone] = match; const y = Number(year), m = Number(month), d = Number(day); const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return y > 0 && m >= 1 && m <= 12 && d >= 1 && d <= days[m - 1] && !/^[+-]14:(?!00)/.test(zone) && Number.isFinite(Date.parse(value)); } /** Validate stored facts without recomputing the server's clinical-content digest in the browser. */ export function readContentAssertion(condition: Condition | undefined): ContentAssertion | 'none' | 'malformed' { const extensions: unknown = condition?.extension ?? []; const security: unknown = condition?.meta?.security ?? []; if (!Array.isArray(extensions) || !Array.isArray(security)) return 'malformed'; const assertions = extensions.filter((e: unknown) => object(e) && e.url === CONTENT_CLASS_ASSERTION_URL); const codings = security.filter((c: unknown) => object(c) && c.system === SHARE_CONTENT_CLASS_SYSTEM); if (!assertions.length && !codings.length) return 'none'; if (assertions.length !== 1 || codings.length !== 1) return 'malformed'; const extension = assertions[0]; if (Object.keys(extension).some((key) => !['url', 'extension'].includes(key))) return 'malformed'; const parts: unknown = extension.extension; const fields = Object.entries(ASSERTION_VALUES); if (!Array.isArray(parts) || parts.length !== fields.length) return 'malformed'; const values: Record = {}; for (const [name, valueKey] of fields) { const matching = parts.filter((p: unknown) => object(p) && p.url === name); if (matching.length !== 1) return 'malformed'; const part = matching[0]; if (Object.keys(part).length !== 2 || !Object.prototype.hasOwnProperty.call(part, valueKey)) return 'malformed'; values[name] = part[valueKey]; } const reference = (name: string, type: string) => { const value = values[name]; return object(value) && Object.keys(value).length === 1 && typeof value.reference === 'string' && new RegExp(`^${type}/[A-Za-z0-9.-]{1,64}$`).test(value.reference) ? value.reference : undefined; }; const author = reference('author', 'Practitioner'), organization = reference('organization', 'Organization'); if (values.schemaVersion !== '1' || !CONTENT_CLASSES.includes(values.class as ContentClass) || codings[0].code !== values.class || typeof values.assertionId !== 'string' || !/^[A-Za-z0-9-]{8,64}$/.test(values.assertionId) || !version(values.sourceVersion) || values.digestSchema !== 'condition-content-v1' || typeof values.contentDigest !== 'string' || !/^[0-9a-f]{64}$/.test(values.contentDigest) || !author || !organization || !isInstant(values.recordedAt)) return 'malformed'; return { assertionId: values.assertionId, class: values.class as ContentClass, sourceVersion: values.sourceVersion, digestSchema: values.digestSchema, contentDigest: values.contentDigest, author, organization, recordedAt: values.recordedAt, }; } /** The stored classification: present only as one well-formed assertion with its one matching coding. */ export function classificationOf(condition: Condition | undefined): Classification { const assertion = readContentAssertion(condition); if (assertion === 'none') return { kind: 'unclassified' }; if (assertion === 'malformed') return { kind: 'unreadable' }; return { kind: 'classified', contentClass: assertion.class, sourceVersion: assertion.sourceVersion, recordedVersion: condition?.meta?.versionId }; } const NATIVE_PRODUCERS = new Set(['native-condition', 'native-condition-edit']); /** The clinician and practice whose own native entry this is, by the server's latest evidence; otherwise undefined. */ export function nativeAuthorOf(condition: Condition | undefined): { author: string; organization: string } | undefined { const evidence = (condition?.extension ?? []).filter((e) => e.url === SHARE_ORIGIN_EVIDENCE_URL); const latest = evidence[evidence.length - 1]; if (!latest || !NATIVE_PRODUCERS.has(sub(latest, 'producer')?.valueString ?? '') || sub(latest, 'path')?.valueCode !== 'native-entry') return undefined; const author = sub(latest, 'actor')?.valueReference?.reference; const organization = sub(latest, 'organization')?.valueReference?.reference; return author && organization ? { author, organization } : undefined; } /** Whether this viewer, at this practice, is the author the server would let classify or own-edit this record. */ export const isOwnNativeEntry = (condition: Condition | undefined, practitionerId: string | undefined, organizationId: string | undefined) => { const native = nativeAuthorOf(condition); return Boolean(native && practitionerId && organizationId && native.author === `Practitioner/${practitionerId}` && native.organization === `Organization/${organizationId}`); };