// Package audit records system-affecting actions (signal writes by users or the // control-logic engine, interface and control-logic mutations) to an append-only // SQLite log that audit staff can query later. It is enabled via [audit] in the // config; when disabled a no-op Recorder is used so call sites need no guards. package audit import "time" // Actor types distinguish human-initiated actions from automated ones. const ( ActorUser = "user" // action attributed to an authenticated end-user ActorSystem = "system" // action performed by the control-logic engine ) // Outcomes record whether the action succeeded. const ( OutcomeOK = "ok" OutcomeError = "error" ) // Event is a single audit record. Optional fields are omitted from the database // when empty. Time defaults to the current time when zero. type Event struct { Time time.Time `json:"time"` Actor string `json:"actor"` // username, or graph name for system actions ActorType string `json:"actorType"` // ActorUser | ActorSystem Action string `json:"action"` // e.g. "signal.write", "interface.update" DS string `json:"ds,omitempty"` // data source (signal writes) Signal string `json:"signal,omitempty"` // signal / target name Value string `json:"value,omitempty"` // serialised written value Detail string `json:"detail,omitempty"` // free-form context (id, graph, trigger) IP string `json:"ip,omitempty"` // client address (user actions) Outcome string `json:"outcome"` // OutcomeOK | OutcomeError Error string `json:"error,omitempty"` // message when Outcome==OutcomeError } // Filter selects a subset of events for Query. Zero-valued fields are ignored. type Filter struct { Start time.Time End time.Time Actor string Action string DS string Signal string Limit int // <=0 means a default cap is applied } // Recorder appends events and answers queries. Implementations must be safe for // concurrent use and Record must never block the caller for long. type Recorder interface { Record(Event) Query(Filter) ([]Event, error) Close() error } // nopRecorder is used when auditing is disabled. It discards every event. type nopRecorder struct{} func (nopRecorder) Record(Event) {} func (nopRecorder) Query(Filter) ([]Event, error) { return []Event{}, nil } func (nopRecorder) Close() error { return nil } // Nop returns a Recorder that discards everything. Call sites can hold a Recorder // unconditionally and call Record without checking whether auditing is enabled. func Nop() Recorder { return nopRecorder{} }