@zanii/health
v0.1.0
Published
Medical audit trails without leaking the pattern: per-episode unlinkable tags (the patient supplies the tag, never the seed), plus who accessed the record, which model recommended what under which protocol, and which named clinician took responsibility. B
Downloads
54
Readme
@zanii/health
Medical audit trails without leaking the pattern.
For medicine, the metadata is the sensitive data. Zanii's usual privacy story ("we store only hashes; payloads never leave your systems") is what makes it safe everywhere else — and it is exactly what fails here. A stable public subject slice leaks that a record was opened, by how many parties, how often, and when. Access frequency to an oncology or psychiatric record is itself PHI.
So this package deliberately breaks the one property Zanii otherwise relies on — a single, stable, linkable identifier per subject.
Design target: an episode is verifiable; a life is not linkable except by the patient.
Threat model (it decides everything): we do not need to hide the encounter from the treating provider — they hold the chart. We must stop everyone else — a second provider, an insurer, the log operator — from linking encounters into a history.
🔴 The rule that makes or breaks this
The patient supplies the TAG. The patient never supplies the SEED.
If a provider held the seed it could enumerate n = 1, 2, 3… and reconstruct the patient's
entire cross-institution history — the privacy gain would be zero, and illusory. The
patient's app derives the tag for this encounter and hands over one opaque string.
There is a red test (the seed must never reach a provider) in both languages, existing
purely so nobody "optimises" the seed into the provider's hands later.
npm install @zanii/health @zanii/coreimport { episodeSeed, episodeTag, buildAccessReceipt, buildRecommendation,
buildClinicianConfirmation, buildBreakGlass, fetchEpisodes } from '@zanii/health';
// patient app (device keystore) — the seed never leaves here
const seed = episodeSeed();
const tag = episodeTag(seed, 12); // hand THIS to the provider, and nothing else
// provider side — learns one encounter, nothing about any other
await zanii.record({ ...buildAccessReceipt({ episodeTag: tag, actor: clinician.did,
purpose: 'treatment', consentRef, ts }), subjectTag: tag });
// patient side — the only party who can link the chain
const history = await fetchEpisodes(seed);
history.pendingBreakGlass; // emergency access on my record that nobody reviewedThe four-party question (what a malpractice case actually turns on)
| Question | Receipt |
|---|---|
| Who accessed the record? | health.access — actor did:key, purpose (limitation is not optional), consent ref |
| Which model recommended what? | health.recommendation — model_id + runtime_hash |
| Under which clinical protocol? | same receipt — manifest_hash is required; a recommendation with no protocol on record is rejected |
| Which named human took responsibility? | health.confirmation — the clinician signs personally. The model did not decide; a person did. |
protocolTrail is not ruleConsistency — and that distinction is clinical
In @zanii/gov, "the same rulebook decided everyone" is the fairness property and
interleaved rulebooks are a red flag. That does not transfer to medicine.
Individualised care is correct. Protocols legitimately coexist — comorbidities, cohorts,
trial arms. Flagging interleaved protocols would false-alarm on good medicine and train
clinicians to ignore the tool. So protocolTrail reports disclosure and change, and passes
no verdict on uniformity (there is no consistent field to fail):
- which protocol governed each recommendation,
- when protocols changed,
- deviation — recorded, never prevented, and never treated as a defect (departing from a protocol is legitimate and common),
unprotocolled— a recommendation with no protocol. That is the defect.
Break-glass: loud, not impossible
An unconscious patient supplies no tag — yet care must proceed and be recorded. Break-glass is the classic PHI-snooping vector (the celebrity-record case), so:
- every break-glass emits a receipt — a
reasonis mandatory; - the acting clinician signs it personally — a named human puts their name on it, and that signature is the deterrent;
pendingBreakGlass()counts events with no review on record — visible in the institution's own audit and to the patient.
We ship the metric, not the mandate. We do not enforce a review policy we have no power to enforce — we make ignoring it undeniable. A regulator can require review; the institution cannot hide that it didn't do it.
This is a hole we deliberately keep open — a closed one kills patients.
Honest limits (read these)
- We hide linkage, not existence. An episode is verifiable; a life is not linkable except by the patient. Anyone who obtains one tag still learns that encounter's provider — and for a single-specialty clinic, that lone inference is itself sensitive. Cannot be eliminated; must be disclosed.
- Seed loss ≠ record loss. Lose the seed and you lose linkability of past episodes — the chart persists, care is unaffected. The system degrades to unlinkable, never lost. This is precisely why we can refuse dangerous recovery schemes.
- No social/Shamir recovery — deliberately. In health, the guardians are frequently the adversary (coercion, abuse, an employer who is also a relative). Social recovery is right for a wallet and wrong for a medical history. Use a device keystore + an offline recovery code.
- Institution-held seed escrow is a labelled downgrade, never a default. If your provider can decrypt the seed, they can link your history. Say it in those words.
- A permissioned slice reintroduces server trust for confidentiality. It stops opportunistic pattern-mining, not the operator. Deploy sovereign where that matters.
- This proves the process, not the medicine. Which model, which protocol, which clinician signed — never that the treatment was right.
Changelog
- 0.1.0 — initial release:
episodeSeed/episodeTag/episodeTags(unlinkable per-episode identity),buildAccessReceipt,buildRecommendation(protocol required),buildClinicianConfirmation,buildBreakGlass+pendingBreakGlass/markReviewed,protocolTrail(notruleConsistency),fetchEpisodes.
License
Apache-2.0.
