AxioRank Docs

Audit integrity

Prove the audit log hasn't been altered with a sealed, tamper-evident hash chain.

Every decision AxioRank makes is written to an audit log. Audit integrity makes that log tamper-evident: finalized rows are sealed into a per-workspace hash chain, so any later edit, insertion, deletion, or reordering is detectable.

How sealing works

A background job (hourly, or on demand) seals finalized audit_logs rows into a chain of checkpoints. Each checkpoint covers a time window and stores:

  • batch_hash: an ordered hash of the row hashes in the window,
  • prev_hash: the previous checkpoint's hash (or a genesis value),
  • checkpoint_hash: hash(prev_hash, batch_hash, from_ts, to_ts, row_count),
  • merkle_root: an RFC 6962 Merkle root over the same rows, signed (Ed25519) into a tree head so a single row can be proven with an offline receipt (see Verifiable receipts).

Linking each checkpoint to the one before it forms a chain: change anything in a sealed window (or a checkpoint itself) and the recomputed hashes stop matching from that point forward.

Grace period

Rows are sealed only after a grace period longer than the approval-hold timeout, so a held call's decision is final before it's committed to the chain.

Verify the chain

GET /api/audit-integrity/verify (workspace member) recomputes every sealed checkpoint from the live rows and walks the chain. It detects altered rows (batch hash), inserted or deleted rows (row count + batch hash), reordering (the batch hash is order-sensitive), and edited checkpoints (a forward-chain break). It stops at the first break.

{
  "ok": true,
  "checkpoints": 184,
  "rows": 51230,
  "purgedCheckpoints": 12
}

On a break, ok is false and broken carries the failing checkpoint: { "seq": 97, "reason": "batch hash mismatch" }.

POST /api/audit-integrity/seal forces a seal of any finalized rows immediately (the same operation the hourly job runs), returning { sealed, seq?, rowCount? }.

Verifiable receipts

The chain above is something you verify against your own live rows. A receipt goes further: it lets a customer or an auditor verify a single action offline, with nothing but a public key, no AxioRank account and no trust in us.

A receipt proves three things:

  1. Membership: the audit row is in the sealed Merkle tree (an RFC 6962 inclusion proof).
  2. Integrity: that tree's root was signed by the log (an Ed25519 signed tree head).
  3. Provenance: who or what authorized the action, as a signed delegation chain (the operator who approved, the agent key, the upstream trace step).

Verify with the open-source package

The verifier is published as @axiorank/audit-verify (npm, zero dependencies) and as verify_receipt in the axiorank Python SDK. Read the whole thing in a few minutes and trust nothing else.

npx @axiorank/audit-verify receipt.json --jwks key.json
#  ✓ leaf hash
#  ✓ inclusion proof
#  ✓ signed tree head
#  ✓ delegation provenance
#
#  VERIFIED
import { verifyReceipt } from "@axiorank/audit-verify";

const result = verifyReceipt(receipt, jwks); // jwks pinned out of band
if (!result.ok) throw new Error(result.reason);
from axiorank import verify_receipt  # pip install axiorank[verify]

assert verify_receipt(receipt, jwks) is True

Endpoints

  • GET /api/v1/audit/public-key: the JWKS (active and retired keys). Pin a key by kid out of band and verify against the pinned copy. Public.
  • POST /api/v1/audit/verify-receipt: a convenience verifier. Not the trust anchor; the open-source package run against a pinned key is.
  • GET /api/v1/audit/checkpoints/{seq}: a signed tree head by sequence number (logs:read). Pin the series over time to detect a retroactive edit yourself.

What a receipt does not prove

A verified receipt proves the action is in a signed, append-only log and was authorized as stated, unaltered since sealing. On its own it does not defend against a split-view attack (different signed histories shown to different parties); that needs gossiping tree heads to an external witness. Pinning the checkpoint series over time lets you catch a retroactive edit.

Retention and purges

When rows are purged under data-retention policy, their checkpoint keeps its stored batch_hash and stays in the chain, so the forward chain still verifies even though the underlying data is gone. Those windows are reported as purgedCheckpoints: the integrity proof outlives the data.

Next steps

On this page