T-009 — Demo Flat-File Audit Log
This is the demo audit-log primitive used by the two-agent demo (T-011) and the herald verify <trace> CLI (T-010). It is intentionally minimal: append-only, hash-linked, no Signed Tree Heads, no inclusion proofs, no consistency proofs, no gossip. The full HERALD CT log lives in T-101 / T-102 / [EXT §2].
Run
cd learning/herald-protocol/manthan/state/findings/T-009/demo/audit-log
npm test # 10 tests
npm run demo # appends 3 entries, prints raw JSONL, replaysNo npm install required on Node 24+ (uses --experimental-strip-types).
File format
The log is a single JSONL file. One JSON object per line. Each line is a LogEntry:
{
"seq": 0,
"prev_hash": "<64 hex>",
"id": "<envelope id>",
"parent_hash": "<64 hex>",
"trace_hash": "<64 hex>",
"ts_bucket": 1746432000,
"entry_hash": "<64 hex>"
}seq— 0-based monotone index.prev_hash—entry_hashof the previous line, or the all-zero hash forseq == 0.id— the envelope'sidfield per [SPEC §3.5 lines 193-204].parent_hash— SHA-256 ofenvelope.parent, or zero hash for genesis. Per [EXT §2.2 line 133].trace_hash— SHA-256 ofenvelope.trace. Per [EXT §2.2 line 134].ts_bucket— Unix timestamp rounded down to the nearest hour. Per [EXT §2.2 line 135].entry_hash— SHA-256 of the deterministic CBOR encoding of the body (every field above exceptentry_hashitself). The chain is over log entries; each entry's body includes the previous entry'sentry_hash, so tampering propagates forward.
This is the minimal-metadata profile from [SPEC §5.3 line 310]: the log records id, hash of parent, hash of trace, and a coarse timestamp bucket — nothing more. No from, no to, no payload, no signatures. Wider profiles require an explicit log_metadata_consent attestation per [EXT §2.2 line 137]; that profile is out of scope for the demo.
Public API
import { AuditLog, ZERO_HASH, type EnvelopeSummary } from "./src/index.ts";
const log = new AuditLog("/tmp/herald-demo.jsonl");
log.init(); // create file if missing
const entry = log.append({ // append one envelope summary
id: "envid-...",
parent: null, // or another envelope id
trace: "envid-...",
ts: 1746432000,
});
log.read(); // -> LogEntry[]
log.tailEntry(); // -> LogEntry | null
log.replay(); // -> {status: "ok", count} | failurereplay() walks the file in order, re-derives each entry_hash, and verifies the prev_hash chain. It does NOT verify envelope signatures — that is the verifier's job (T-010). It returns one of:
| Status | Meaning |
|---|---|
ok | Whole file is internally consistent. |
tampered-entry | A line's stored entry_hash does not match its body. |
broken-chain | A line's prev_hash does not match the previous line's entry_hash. |
duplicate-seq | seq is non-monotonic. |
malformed | A line is not valid JSON or missing required fields. |
What this is NOT
- Not a HERALD CT log. No STH, no SIP, no SCT, no consistency proofs, no gossip. See T-101 onwards.
- Not concurrent-writer-safe. Single-process append only.
- Not a verifier. It does not validate envelope signatures; T-010 will sit on top of this.
Files
src/audit-log.ts—AuditLogclass,LogEntrytype, replay logic.src/cbor.ts— minimal deterministic CBOR encoder, vendored from T-003 to keep the audit log self-contained (no cross-sandbox imports).src/index.ts— re-exports.test/audit-log.test.ts— 10 tests: success, tamper, broken chain, edge cases, minimal-metadata invariant, persistence across instances.test/demo.ts— 3-envelope harness used by T-011.