WORM audit log
A WORM audit log is an audit trail stored write once, read many: entries can be appended but never altered or deleted, not even by an administrator. Caisson's audit-worm package hash-chains each entry, then writes a write-once anchor for the chain to WORM storage on every append, so tamper, truncation, and rewrite each surface on verify.
In code
const entries = await loadEntries(tx, accountId);
const anchor = anchorChain(entries);
// The trusted commitment lands in WORM under a LENGTH-keyed, write-once key. A second anchor
// for the same length (a truncate-then-re-append, a replay) hits the existing immutable object
// → ArtifactExistsError → ConflictError: the original tip can never be overwritten (TM-H).
try {
await this.store.put(
anchorKey(accountId, anchor.length),
encodeAnchor(anchor),
{
retainUntil,
contentType: "application/json",
},
);
} catch (err) {
if (err instanceof ArtifactExistsError) {
throw new ConflictError(
"audit chain anchor already exists for this length",
{ accountId, length: anchor.length },
);
}
throw err;
}
return { entry, anchor };How it holds
Two composable guarantees, one class
AuditChainStore composes the kernel's pure hash-chain algebra with a write-once WORM object store: the DB grant can't be rewritten (no UPDATE/DELETE privilege) and the anchor object can't be overwritten either (a write-once key); no single control has to hold alone.
Every append mints a fresh anchor, in the same call
append() inserts the chain entry and, before returning, mints a length-keyed anchor over the resulting chain and writes it write-once to WORM storage; the anchor and the entry it commits to land together, never as a later batch job that could be skipped.
Re-anchoring a length collides, it never overwrites
A truncate-then-replay or a duplicate append for an already-anchored length hits the WORM store's existing immutable object and throws ConflictError: the original tip can never be silently replaced by a second write.
Tenant-scoped and serialized against forks
Every append runs inside withTenant behind a per-tenant advisory lock, so a forgotten tenant filter can't cross-write another tenant's chain and two concurrent appends can't mint two different tips for the same length.