Evidence and audit
client.evidence, the Evidence namespace, lets a person entitled to Export produce a signed, self-contained record. The record covers the tenant, a dataset or one subject. Anyone can check the record later, without an enrolled device. SealdHealthcare.verifyPack runs the check offline, against the trust root built into the SDK. A customer’s auditor needs no device certificate (card) and no live connection to trust a pack.
Exporting and resolving refs
The samples use the allowed helper from Access decisions.
const pack = await allowed(await client.evidence.export({ tenant: true }, { from, to }));const byDataset = await allowed(await client.evidence.export({ datasets: ['encounters'] }, { from, to }));// an accounting of disclosures: the same export, scoped to one subjectconst bySubject = await allowed(await client.evidence.export({ subject: medicalRecordNumber }, { from, to }));if (pack) await writeFile(`evidence-${pack.packId}.seald`, await pack.bytes());// an ExportedPack: { packId, eventId, events, bytes() }
const epochsInPeriod = await client.evidence.epochs({ from, to });for (const epoch of epochsInPeriod) { const ref = await client.evidence.subjectRef(medicalRecordNumber, epoch); // { ref, epoch } const personRef = await client.evidence.personRef(alice, epoch);}let pack = try await allowed(client.evidence.export(.tenant, from: from, to: to))let byDataset = try await allowed(client.evidence.export(.datasets(["encounters"]), from: from, to: to))let bySubject = try await allowed(client.evidence.export(.subject(medicalRecordNumber), from: from, to: to))if let pack { try await writeFile("evidence-\(pack.packId).sealdhealthcare", await pack.bytes()) }
let epochsInPeriod = try await client.evidence.epochs(from: from, to: to)for epoch in epochsInPeriod { let ref = try await client.evidence.subjectRef(medicalRecordNumber, epoch: epoch) let personRef = try await client.evidence.personRef(alice, epoch: epoch)}val pack = allowed(client.evidence.export(ExportScope.Tenant, from = from, to = to))val byDataset = allowed(client.evidence.export(ExportScope.Datasets(listOf("encounters")), from = from, to = to))val bySubject = allowed(client.evidence.export(ExportScope.Subject(medicalRecordNumber), from = from, to = to))pack?.let { writeFile("evidence-${it.packId}.sealdhealthcare", it.bytes()) }
val epochsInPeriod = client.evidence.epochs(from = from, to = to)for (epoch in epochsInPeriod) { val ref = client.evidence.subjectRef(medicalRecordNumber, epoch) val personRef = client.evidence.personRef(alice, epoch)}An accounting of disclosures is the same export, scoped with subject instead of datasets or tenant. The SDK tokenizes the customer’s own identifier on the device, per the epoch it falls in. The Seald Healthcare Cloud never sees it. To resolve a Ref back to a person or a subject, make the same ref for a candidate. Use subjectRef or personRef at the pack’s epoch, then compare the two refs.
Verifying a pack offline
const verification = await sealdhealthcare.verifyPack(await readFile('evidence-3f1c.sealdhealthcare'));if (!verification.verified) return render(verification.failure); // the first failure, in the order the checks runrenderEvents(verification.events, verification.controlStatus);// { verified, failure?, tenantId, period, scope, events, policyVersions, checkpoints, controlStatus }let verification = try await sealdhealthcare.verifyPack(readFile("evidence-3f1c.sealdhealthcare"))guard verification.verified else { return render(verification.failure) }renderEvents(verification.events, verification.controlStatus)val verification = sealdhealthcare.verifyPack(readFile("evidence-3f1c.sealdhealthcare"))if (!verification.verified) return render(verification.failure)renderEvents(verification.events, verification.controlStatus)PackVerification.failure, when present, names the first check that failed, in the order the checks run: chain, checkpoint, identity-authority, card, manifest, pack-manifest or policy-version. policyVersions carries every policy version in force over the period. checkpoints carries the timestamp authority checkpoints that fix the chain in time. controlStatus carries the tenant’s control status for the period.
PackEvent fields
| Field | Holds |
|---|---|
at | When the event happened |
actor | A Ref: the acting person, tokenized, never a name |
organization | The actor’s organization as the identity provider named it |
device | The RecipientId of the device that made the call |
action | The audit action, such as View or Export |
subject | A Ref to the record’s subject, when the object has one |
policyVersion | The versionId of the policy version that decided the event |
outcome | allow, deny or challenge |
riskScore | A relative measure of how unusual the event was, for triage, never a decision the SDK acts on |
hash | The event’s own hash, chained to the one before it |
What the SDK does for you
- Sends the exporter’s own manifest over every event a pack covers. A pack is then signed evidence of what was exported and by whom.
- Tokenizes a
subjectscope’s identifier on the device, per epoch, and never sends the identifier itself. - Checks a pack against the trust root built into the SDK and the timestamp authorities’ roots. It needs no key, session or tenant enrollment.
- Reports the first failed check, not every downstream effect of it.
verification.failurealways points at the actual break in the chain.
Decisions and errors you may see
Outcome or ErrorCode | When | What to do |
|---|---|---|
deny on export | The caller does not hold Export for the scope asked. | Ask a person entitled to Export that scope. |
failure.check is chain or checkpoint | The pack’s hash chain or its timestamp checkpoints do not verify. | Treat the pack as untrusted. Ask for a fresh export. |
failure.check is card or identity-authority | An actor’s card or the tenant’s identity authority failed its check against the trust root built into the SDK. | Treat the pack as untrusted. Contact support. |
failure.check is policy-version | An event names a policy version the pack cannot verify. | Treat the pack as untrusted. Contact support. |