Chronicle. Provider. Ecto
(chronicle v0.1.0)
Copy Markdown
Stores audit groups and events in an Ecto repository.
Required option:
:repo- theEcto.Repomodule
Optional options:
:events_table- defaults to"audit_events":ledger_heads_table- defaults to"audit_ledger_heads":ledger_entries_table- defaults to"audit_ledger_entries":prefix- database prefix/schema:chunk_size- event insert batch size, defaults to500:integrity- HMAC ledger options; required
A group and its events share the record table: the group is a row with
kind: "group" carrying the unit's outcome, timing, and child count, and
its children reference it by group_id. All of them are inserted in one
repository transaction and covered by one ledger entry.
If the caller is already inside a transaction on the same repo, Ecto uses
that transaction. Integrity configuration is fail-closed: omit it and the
write fails. The Ecto provider has no unsigned mode.
The signature covers the row, not the intention
Every write builds its ledger entry from content_row/1 — a projection of
the row as it is about to be stored — and does so before the insert. The
ordering matters more than it looks. Signing the caller's original values
and then storing a normalized version would produce a signature that
verifies against something the database does not contain, and the mismatch
would surface years later as an unexplained tamper alert.
Which columns that projection covers is decided in exactly one place,
Chronicle.Ecto.Schema.Event.content_fields/0, which derives the list from
the schema's own fields rather than restating them. A new column is signed
because it exists, not because someone remembered to add it twice, and the
adversarial suite generates a tamper case per column from the same list —
so new columns arrive with coverage already attached.
The one deliberate exclusion is inserted_at, which is why it is subtracted
explicitly and visibly there rather than simply left out.
A group is signed as a unit
One ledger entry covers a group and all its children. The root row carries the child count, and it is signed, so adding or removing a child is detectable even though the children themselves have no entries of their own. This is why the whole group is written in one transaction: a partially written group is a group whose signed count does not match reality.