Audit Chain Primitives
Low-level audit-chain hash + verify primitives — b.audit composes on top of these so operators rarely call them directly. Every audit row carries prevHash + rowHash + nonce and the chain math is:
rowHash = SHA3-512( prevHash || canonicalize(row-fields-except-hash) || nonce )
Each row's prevHash equals the previous row's rowHash in monotonic-counter order. The first row uses ZERO_HASH as the anchor. verifyChain walks every row forward, recomputing each hash; any mismatch returns { ok: false, reason, breakAt, ... } and the caller (audit boot, b.cli verify-chain, restore-rollback, forensic snapshot) decides whether to refuse-to-boot or just log.
Checkpoint signing (SLH-DSA-SHAKE-256f over (atRow || atRowHash)) lives in b.auditSign. This module owns the chain hash math only; verification is O(n) over audit_log rows.
Operators reach for b.auditChain.verifyChain directly when restoring from backup (verify the restored DB before promoting it), when running a forensic offline check, or when extending the chain primitive into a custom append-only table. Day-to-day appends go through b.audit.record / b.audit.safeEmit.
b.auditChain.canonicalize(row, excludeKeys) #
RFC 8785 (JSON Canonicalization Scheme) serialization of an audit row's logical fields, used as the middle slice of the row-hash preimage. Sorted keys, Buffer values rendered as hex, every other value passed through the shared lib/canonical-json walker so the four canonicalize sites in the framework (chain, audit-tools, config-drift, pagination) emit byte-identical output.
var bytes = b.auditChain.canonicalize(
{ actor: "u-42", action: "auth.login.success", recordedAt: 1700000000000 },
["prevHash", "rowHash", "nonce"]
);
// → '{"action":"auth.login.success","actor":"u-42","recordedAt":1700000000000}'
b.auditChain.computeRowHash(prevHash, rowFields, nonce) #
Compute a row's rowHash given its predecessor's hash, the row's logical fields (already excluding prevHash / rowHash / nonce), and the row's nonce buffer. The hash is SHA3-512(prevHashBytes || canonicalize(rowFields) || nonce), returned as a 128-char lowercase hex string.
prevHash must be the 128-char hex form (use b.auditChain.ZERO_HASH for the chain anchor). nonce must be a non-empty Buffer; the framework writes 16 random bytes per row.
var rowHash = b.auditChain.computeRowHash(
b.auditChain.ZERO_HASH,
{ action: "system.boot", recordedAt: 1700000000000, outcome: "success" },
Buffer.from("0123456789abcdef0123456789abcdef", "hex")
);
// → "<128-char SHA3-512 hex>"
b.auditChain.getChainTip(queryOneAsync, tableName, opts?) #
{
chainKey: string, // partition column for a multi-chain table
keyValue: any, // the partition value to scope the tip to (bound)
}
Read the current chain tip (last row's rowHash + monotonicCounter) for a given audit table. Empty tables return { prevHash: ZERO_HASH, counter: 0 } so callers can treat first-row insert and append uniformly. Async so operator-supplied external-db drivers can use any await-able query function of the shape async (sql, params?) -> row | null.
Pass { chainKey, keyValue } to scope the tip to one partition of a multi-chain table (one chain per account / device / tenant) — the tip read filters WHERE with the value bound, never interpolated.
async function queryOne(sql) {
var rows = await myDriver.query(sql);
return rows[0] || null;
}
var tip = await b.auditChain.getChainTip(queryOne, "audit_log");
// → { prevHash: "<128-char hex>", counter: 4217 }
b.auditChain.verifyChain(queryAllAsync, tableName, opts) #
{
maxRows: number, // stop after N rows per (sub-)chain (default: walk every row)
chainKey: string, // partition column — verify each sub-chain independently
maxChains: number, // max partitions to verify under chainKey (default 100000; fails closed)
from: number, // single-chain only: verify rows with monotonicCounter >= from, anchored at the predecessor's rowHash (incremental verify after a known-good checkpoint)
to: number, // single-chain only: verify rows with monotonicCounter <= to
}
Walk the entire chain forward, recomputing each row's hash and comparing against the stored prevHash / rowHash. Returns { ok: true, table, rowsVerified, lastHash } on a clean walk, or { ok: false, table, rowsVerified, breakAt, breakRowId, reason, expected, actual } on the first mismatch. Callers decide how to react — b.audit.verify refuses-to-boot, b.cli verify-chain exits non-zero, b.restoreRollback blocks promotion.
For audit_log: if a _blamejs_audit_purge_anchor row exists, the walk starts at lastPurgedCounter+1 with prevHash = lastPurgedRowHash. The anchor is written by b.auditTools.purge after a successful archive and lets the chain math survive deletion of historical rows without the archive bundle as source of truth.
Pass { chainKey } to verify a MULTI-chain table partitioned by a key column (one chain per account / device / tenant): each key's sub-chain is walked independently from ZERO_HASH, and the first break in any key returns { ok:false, chainKey, breakAt, ... }. Under chainKey, maxRows is per-sub-chain and maxChains bounds the partition fan-out, failing closed when exceeded. The audit_log purge-anchor logic is single-chain-only and is skipped when a chainKey is given.
async function queryAll(sql) { return await myDriver.query(sql); }
var result = await b.auditChain.verifyChain(queryAll, "audit_log", {});
// → { ok: true, table: "audit_log", rowsVerified: 4217, lastHash: "" }
Last updated 2026-08-08T16:39:49.652Z by seeder.