@qadi/audit
v0.8.0
Published
GxP-style audit trail, staging, circuit breaker, retention, and e-signature capture for @qadi/core, composed onto DecisionSink
Maintainers
Readme
@qadi/audit
Audit trail, staging, a circuit breaker, retention/archival, and e-signature
capture for @qadi/core, composed
into one assembled pipeline hung off DecisionSink.
pnpm add @qadi/audit @qadi/core effectNarrows ADR-QD-016
the way ADR-QD-054
narrowed ADR-QD-024: an optional, separately versioned, dependency-free
companion package — @qadi/core gains no dependency of any kind through this
package existing, and @qadi/audit itself opens no connection, generates no
key, and assumes no schema. Every capability that needs real storage,
identity or crypto is a caller-supplied port.
import { AuditDecisionSinkLive } from "@qadi/audit";
import * as Layer from "effect/Layer";
const AppLayer = AuditDecisionSinkLive({ failureThreshold: 5, resetTimeoutMs: 30_000 }).pipe(
Layer.provide(myAuditTrailPortLive), // the caller's own storage
);Assembled, not individually correct
This is the whole point of the package: AuditDecisionSinkLive's record()
sequence — encode, stage if wired, write, react to the outcome — is reachable
through the one call every evaluation already makes, unlike the reference
implementation this was compared against, where the equivalent pieces were
each unit-tested and never called from the real enforcement path.
Refuses rather than approximates
A resource carrying a value with no safe durable representation fails
AuditEntryNotEncodable rather than being partially written or silently
dropped. An unknown decommissioning step id fails UnknownDecommissioningStep
rather than silently no-opping. No e-signature default ships, not even a
no-op one — Qadi.enforce's existing fail-closed behavior on an unwired
obligation is the safe default already.
Not tamper-evident (WD-07)
The audit trail this package produces is not cryptographically
tamper-evident, and archiving it does not make it so. verifySequenceIntegrity
(see SequenceIntegrity.ts) catches a gap or a duplicate in a caller-assigned
sequence number — accidental loss or reordering in the caller's own store — not
deliberate tampering: there is no per-entry hash, nothing links one entry to
the next, and an attacker who can modify stored rows can renumber them and
pass the check. AuditArchive's keyMaterial is opaque pass-through metadata
this package never uses to sign or verify anything (see AuditArchive.ts),
and DecommissioningChecklist's "Revoke signing keys" step names an action
this package has no part in performing. If a deployment needs tamper-evidence
— a hash chain, a signature per archive, a WORM store — that has to be built
and verified outside this library; nothing here provides it or claims to.
Structurally outside the pipeline
Retention, archival, sequence-integrity verification (gap-and-duplicate
detection — not cryptographic tamper-evidence, see SequenceIntegrity.ts) and
the decommissioning checklist are pure functions and data — caller-invoked,
caller-scheduled, since this package has no scheduler of its own. E-signature
capture is wired through Qadi.ts's ObligationHandler, not DecisionSink:
Nothing here connects the two: getPurgeableEntries selects by age alone and
has no idea whether an entry was ever handed to archiveAuditTrail. Archive
before you purge is a documented invariant a caller must uphold itself, not
one this package can check — see the doc comments on Retention.ts's
exports.
import { signatureObligationHandler, SIGNATURE_MEANINGS } from "@qadi/audit";
import * as Qadi from "@qadi/core";
Qadi.enforce(policy, {
onObligations: signatureObligationHandler(mySignaturePort, SIGNATURE_MEANINGS.APPROVED),
});Testing
AuditTrailPortTest/AuditStagingPortTest ship as public, deterministic,
in-memory Layer factories for any consumer's own tests.
See ADR-QD-056.
License
MIT
