Documentation
¶
Overview ¶
Package audit is the durable, append-only, field-level audit trail (roadmap E1): a standardized record of who changed what — entity, field, before/after, actor, capacity, impersonator, request id — written INSIDE the business transaction so an audit row commits iff the change does. Append-only is enforced by the grants (app_rt has no UPDATE/DELETE on audit_logs); this package never offers a mutate path. Cryptographic tamper-evidence (hash-chaining, S6) layers on top of this table later.
Index ¶
- func ExportAnchors(ctx context.Context, pool *pgxpool.Pool) (int, error)
- type Entry
- type Filter
- type Log
- type Redactor
- type VerifyResult
- type Writer
- func (w *Writer) Anchor(ctx context.Context, db database.TenantDB) (seq int64, headHash string, err error)
- func (w *Writer) CheckAnchor(ctx context.Context, db database.TenantDB, seq int64, headHash string) (present bool, err error)
- func (w *Writer) Query(ctx context.Context, db database.TenantDB, f Filter) ([]Log, error)
- func (w *Writer) Record(ctx context.Context, db database.TenantDB, e Entry) error
- func (w *Writer) Verify(ctx context.Context, db database.TenantDB) (VerifyResult, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExportAnchors ¶
ExportAnchors snapshots every tenant's current audit-chain head into the append-only audit_anchors table as immutable tamper-evidence (roadmap CA-11), returning the number of anchors written. Drive it from the leader-safe scheduler (a single replica claims each interval) so anchors are written once, not once per replica. It runs cross-tenant as app_platform (one INSERT..SELECT for all tenants, mirroring the idempotency sweep) on the platform pool.
A tenant is anchored only when its chain head advanced past its last anchored seq, so re-running within an interval (or on a quiescent chain) writes nothing and the table stays bounded — one anchor per burst of audit activity.
Types ¶
type Entry ¶
type Entry struct {
Action string
EntityType string
EntityID uuid.UUID // uuid.Nil → NULL
Field string
OldValue string
NewValue string
Reason string
ActorKind string // user | system | webhook (optional)
ImpersonatorID uuid.UUID // support impersonation (optional)
Metadata map[string]any
}
Entry is a change to record. Action is required (e.g. "document.download", "receipt.void"); the rest are optional. For a field-level change set Field + OldValue + NewValue; for a whole-entity action leave Field empty. Values are passed through the Writer's redactor before persistence.
type Filter ¶
type Filter struct {
EntityType string
EntityID uuid.UUID
ActorID uuid.UUID
Action string
Limit int
}
Filter narrows a Query. Zero-valued fields are ignored; Limit defaults to 100.
type Log ¶
type Log struct {
ID uuid.UUID
OccurredAt time.Time
ActorID *uuid.UUID
ActorKind string
ImpersonatorID *uuid.UUID
RequestID string
Action string
EntityType string
EntityID *uuid.UUID
Field string
OldValue string
NewValue string
Reason string
TxID string // database transaction id (forensic correlation; roadmap CA-11)
}
Log is a persisted audit row returned by Query.
type Redactor ¶
type Redactor func(*Entry)
Redactor may mutate an Entry before it is written — e.g. mask the values of known-sensitive fields so they never land in the audit table. It is the module's per-record redaction hook (blueprint 07 §1 "per-module redaction").
type VerifyResult ¶
type VerifyResult struct {
OK bool
Count int64 // rows checked
HeadSeq int64 // last seq seen
BrokenSeq int64 // first seq where the chain broke (0 when OK)
Reason string // why it broke
}
VerifyResult reports a chain verification. OK is true when every row's hash recomputes and links to its predecessor with no seq gap.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer appends and queries audit rows. It is stateless beyond its id generator and optional redactor.
func (*Writer) Anchor ¶
func (w *Writer) Anchor(ctx context.Context, db database.TenantDB) (seq int64, headHash string, err error)
Anchor returns the tenant's current chain head — the last seq and its hash. Exporting/publishing an anchor lets a later Verify prove no tampering occurred up to that seq. Returns (0, "") when the tenant has no audit rows yet.
func (*Writer) CheckAnchor ¶
func (w *Writer) CheckAnchor(ctx context.Context, db database.TenantDB, seq int64, headHash string) (present bool, err error)
CheckAnchor reports whether the tenant's LIVE chain still contains the row at anchored seq with anchored hash — the offline verifier's primitive against a previously exported anchor (roadmap CA-11). It closes Verify's blind spot: a tail truncation (drop the last k rows and rewind audit_chain.head_hash) leaves the remaining chain internally consistent, so Verify still returns OK; but the anchored (seq, hash) is gone, so CheckAnchor returns false. present=false means the tail was truncated or the anchored row rewritten after the anchor was taken. Read-only; safe in a read-only transaction.
func (*Writer) Query ¶
Query returns audit rows matching the filter, newest first, in the caller's tenant transaction (RLS-scoped). All filter values are bound as parameters.
func (*Writer) Record ¶
Record appends one audit row in db's transaction (so it commits with the business write) and extends the tenant's hash chain (S6). The acting actor id and request id are read from ctx; the caller supplies the semantic fields via e. Action is required. Concurrent audit writes for a tenant serialize on the audit_chain head row, so seq is gap-free and the chain is well-ordered.
func (*Writer) Verify ¶
Verify walks the current tenant's audit chain in seq order, recomputing each row's hash and checking the prev-links and sequence continuity. It detects any mutation of a past row (its hash no longer matches) and any deletion (a seq gap). Read-only; safe in a read-only transaction.