Documentation
¶
Overview ¶
Package sqlite implements the Chronicle store interface using grove ORM with the SQLite driver.
Index ¶
- Variables
- type ArchiveModel
- type CheckpointModel
- type ErasureModel
- type EventModel
- type Option
- type ReportModel
- type RetentionPolicyModel
- type Store
- func (s *Store) Aggregate(ctx context.Context, q *audit.AggregateQuery) (*audit.AggregateResult, error)
- func (s *Store) Append(ctx context.Context, event *audit.Event) error
- func (s *Store) AppendBatch(ctx context.Context, events []*audit.Event) error
- func (s *Store) AppendCheckpoint(ctx context.Context, cp *checkpoint.Checkpoint) error
- func (s *Store) ByUser(ctx context.Context, userID string, opts audit.TimeRange) (*audit.QueryResult, error)
- func (s *Store) CheckpointsInRange(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]*checkpoint.Checkpoint, error)
- func (s *Store) Close() error
- func (s *Store) CompleteErasure(ctx context.Context, erasureID id.ID, o erasure.Outcome) error
- func (s *Store) Count(ctx context.Context, q *audit.CountQuery) (int64, error)
- func (s *Store) CountBySubject(ctx context.Context, sq erasure.SubjectQuery) (int64, error)
- func (s *Store) CountErasures(ctx context.Context, sc erasure.Scope) (int64, error)
- func (s *Store) CreateStream(ctx context.Context, st *stream.Stream) error
- func (s *Store) DeletePolicy(ctx context.Context, policyID id.ID) error
- func (s *Store) DeleteReport(ctx context.Context, reportID id.ID) error
- func (s *Store) EventRange(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]*audit.Event, error)
- func (s *Store) EventsOlderThan(ctx context.Context, pq retention.PurgeQuery) ([]*audit.Event, error)
- func (s *Store) Gaps(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]uint64, error)
- func (s *Store) Get(ctx context.Context, eventID id.ID) (*audit.Event, error)
- func (s *Store) GetCheckpoint(ctx context.Context, checkpointID id.ID) (*checkpoint.Checkpoint, error)
- func (s *Store) GetErasure(ctx context.Context, erasureID id.ID) (*erasure.Erasure, error)
- func (s *Store) GetPolicy(ctx context.Context, policyID id.ID) (*retention.Policy, error)
- func (s *Store) GetReport(ctx context.Context, reportID id.ID) (*compliance.Report, error)
- func (s *Store) GetStream(ctx context.Context, streamID id.ID) (*stream.Stream, error)
- func (s *Store) GetStreamByScope(ctx context.Context, appID, tenantID string) (*stream.Stream, error)
- func (s *Store) LastHash(ctx context.Context, streamID id.ID) (string, error)
- func (s *Store) LastSequence(ctx context.Context, streamID id.ID) (uint64, error)
- func (s *Store) LatestCheckpoint(ctx context.Context, streamID id.ID) (*checkpoint.Checkpoint, error)
- func (s *Store) ListArchives(ctx context.Context, opts retention.ListOpts) ([]*retention.Archive, error)
- func (s *Store) ListCheckpoints(ctx context.Context, streamID id.ID, opts checkpoint.ListOpts) ([]*checkpoint.Checkpoint, error)
- func (s *Store) ListErasures(ctx context.Context, opts erasure.ListOpts) ([]*erasure.Erasure, error)
- func (s *Store) ListPolicies(ctx context.Context, opts retention.ListPoliciesOpts) ([]*retention.Policy, error)
- func (s *Store) ListReports(ctx context.Context, opts compliance.ListOpts) ([]*compliance.Report, error)
- func (s *Store) ListStreams(ctx context.Context, opts stream.ListOpts) ([]*stream.Stream, error)
- func (s *Store) MarkErased(ctx context.Context, sq erasure.SubjectQuery, erasureID id.ID) (int64, error)
- func (s *Store) Migrate(ctx context.Context) error
- func (s *Store) Ping(ctx context.Context) error
- func (s *Store) PurgeEvents(ctx context.Context, eventIDs []id.ID) (int64, error)
- func (s *Store) Query(ctx context.Context, q *audit.Query) (*audit.QueryResult, error)
- func (s *Store) RecordArchive(ctx context.Context, a *retention.Archive) error
- func (s *Store) RecordErasure(ctx context.Context, e *erasure.Erasure) error
- func (s *Store) SavePolicy(ctx context.Context, p *retention.Policy) error
- func (s *Store) SaveReport(ctx context.Context, r *compliance.Report) error
- func (s *Store) SetHasher(h *hash.Chain)
- func (s *Store) SubjectKeyUsage(ctx context.Context, subjectID string) ([]erasure.KeyUsage, error)
- func (s *Store) UpdateStreamHead(ctx context.Context, streamID id.ID, hash string, seq uint64) error
- func (s *Store) UpdateStreamScheme(ctx context.Context, streamID id.ID, scheme string, since uint64) error
- type StreamModel
Constants ¶
This section is empty.
Variables ¶
var Migrations = func() *migrate.Group { g := migrate.NewGroup("chronicle") g.MustRegister( &migrate.Migration{ Name: "create_streams_table", Version: "20240101000000", Comment: "Create chronicle_streams table", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_streams ( id TEXT PRIMARY KEY, app_id TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT '', head_hash TEXT NOT NULL DEFAULT '', head_seq INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(app_id, tenant_id) ); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, `DROP TABLE IF EXISTS chronicle_streams;`) return err }, }, &migrate.Migration{ Name: "create_events_table", Version: "20240101000001", Comment: "Create chronicle_events table with hash chain columns", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_events ( id TEXT PRIMARY KEY, stream_id TEXT NOT NULL REFERENCES chronicle_streams(id), sequence INTEGER NOT NULL, hash TEXT NOT NULL, prev_hash TEXT NOT NULL DEFAULT '', -- Scope app_id TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT '', user_id TEXT NOT NULL DEFAULT '', ip TEXT NOT NULL DEFAULT '', -- What action TEXT NOT NULL, resource TEXT NOT NULL, category TEXT NOT NULL, resource_id TEXT NOT NULL DEFAULT '', metadata TEXT DEFAULT '{}', outcome TEXT NOT NULL DEFAULT 'success', severity TEXT NOT NULL DEFAULT 'info', reason TEXT NOT NULL DEFAULT '', -- GDPR subject_id TEXT NOT NULL DEFAULT '', encryption_key_id TEXT NOT NULL DEFAULT '', erased INTEGER NOT NULL DEFAULT 0, erased_at TEXT, erasure_id TEXT, timestamp TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(stream_id, sequence) ); CREATE INDEX IF NOT EXISTS idx_chronicle_events_scope ON chronicle_events (app_id, tenant_id, timestamp); CREATE INDEX IF NOT EXISTS idx_chronicle_events_category ON chronicle_events (category, timestamp); CREATE INDEX IF NOT EXISTS idx_chronicle_events_action ON chronicle_events (action, outcome, timestamp); CREATE INDEX IF NOT EXISTS idx_chronicle_events_user ON chronicle_events (user_id, timestamp); CREATE INDEX IF NOT EXISTS idx_chronicle_events_subject ON chronicle_events (subject_id); CREATE INDEX IF NOT EXISTS idx_chronicle_events_severity ON chronicle_events (severity, timestamp); CREATE INDEX IF NOT EXISTS idx_chronicle_events_resource ON chronicle_events (resource, resource_id, timestamp); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, `DROP TABLE IF EXISTS chronicle_events;`) return err }, }, &migrate.Migration{ Name: "create_erasures_table", Version: "20240101000002", Comment: "Create chronicle_erasures table", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_erasures ( id TEXT PRIMARY KEY, subject_id TEXT NOT NULL, reason TEXT NOT NULL, requested_by TEXT NOT NULL, events_affected INTEGER NOT NULL DEFAULT 0, key_destroyed INTEGER NOT NULL DEFAULT 0, app_id TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_chronicle_erasures_subject ON chronicle_erasures (subject_id); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, `DROP TABLE IF EXISTS chronicle_erasures;`) return err }, }, &migrate.Migration{ Name: "create_retention_tables", Version: "20240101000003", Comment: "Create chronicle_retention_policies and chronicle_archives tables", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_retention_policies ( id TEXT PRIMARY KEY, category TEXT NOT NULL UNIQUE, duration INTEGER NOT NULL, archive INTEGER NOT NULL DEFAULT 0, app_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS chronicle_archives ( id TEXT PRIMARY KEY, policy_id TEXT NOT NULL, category TEXT NOT NULL, event_count INTEGER NOT NULL, from_timestamp TEXT NOT NULL, to_timestamp TEXT NOT NULL, sink_name TEXT NOT NULL, sink_ref TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` DROP TABLE IF EXISTS chronicle_archives; DROP TABLE IF EXISTS chronicle_retention_policies; `) return err }, }, &migrate.Migration{ Name: "create_reports_table", Version: "20240101000004", Comment: "Create chronicle_reports table", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_reports ( id TEXT PRIMARY KEY, title TEXT NOT NULL, type TEXT NOT NULL, period_from TEXT NOT NULL, period_to TEXT NOT NULL, app_id TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT '', format TEXT NOT NULL DEFAULT 'json', data TEXT NOT NULL, generated_by TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_chronicle_reports_scope ON chronicle_reports (app_id, tenant_id, created_at); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, `DROP TABLE IF EXISTS chronicle_reports;`) return err }, }, &migrate.Migration{ Name: "scope_retention_to_app_and_tenant", Version: "20240101000005", Comment: "Key retention policies per (app_id, tenant_id, category) and scope archives", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE chronicle_retention_policies_new ( id TEXT PRIMARY KEY, category TEXT NOT NULL, duration INTEGER NOT NULL, archive INTEGER NOT NULL DEFAULT 0, app_id TEXT NOT NULL DEFAULT '', tenant_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(app_id, tenant_id, category) ); INSERT INTO chronicle_retention_policies_new (id, category, duration, archive, app_id, created_at, updated_at) SELECT id, category, duration, archive, app_id, created_at, updated_at FROM chronicle_retention_policies; DROP TABLE chronicle_retention_policies; ALTER TABLE chronicle_retention_policies_new RENAME TO chronicle_retention_policies; ALTER TABLE chronicle_archives ADD COLUMN app_id TEXT NOT NULL DEFAULT ''; ALTER TABLE chronicle_archives ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ''; CREATE INDEX IF NOT EXISTS idx_chronicle_archives_scope ON chronicle_archives (app_id, tenant_id, created_at); CREATE INDEX IF NOT EXISTS idx_chronicle_events_retention ON chronicle_events (app_id, tenant_id, category, timestamp); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` DROP INDEX IF EXISTS idx_chronicle_events_retention; DROP INDEX IF EXISTS idx_chronicle_archives_scope; CREATE TABLE chronicle_archives_old ( id TEXT PRIMARY KEY, policy_id TEXT NOT NULL, category TEXT NOT NULL, event_count INTEGER NOT NULL, from_timestamp TEXT NOT NULL, to_timestamp TEXT NOT NULL, sink_name TEXT NOT NULL, sink_ref TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); INSERT INTO chronicle_archives_old (id, policy_id, category, event_count, from_timestamp, to_timestamp, sink_name, sink_ref, created_at) SELECT id, policy_id, category, event_count, from_timestamp, to_timestamp, sink_name, sink_ref, created_at FROM chronicle_archives; DROP TABLE chronicle_archives; ALTER TABLE chronicle_archives_old RENAME TO chronicle_archives; CREATE TABLE chronicle_retention_policies_old ( id TEXT PRIMARY KEY, category TEXT NOT NULL UNIQUE, duration INTEGER NOT NULL, archive INTEGER NOT NULL DEFAULT 0, app_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); INSERT INTO chronicle_retention_policies_old (id, category, duration, archive, app_id, created_at, updated_at) SELECT id, category, duration, archive, app_id, created_at, updated_at FROM chronicle_retention_policies GROUP BY category; DROP TABLE chronicle_retention_policies; ALTER TABLE chronicle_retention_policies_old RENAME TO chronicle_retention_policies; `) return err }, }, &migrate.Migration{ Name: "record_hash_scheme", Version: "20240101000006", Comment: "Record the digest scheme per event and pin it per stream", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` ALTER TABLE chronicle_events ADD COLUMN hash_scheme TEXT NOT NULL DEFAULT ''; ALTER TABLE chronicle_events ADD COLUMN hash_key_id TEXT NOT NULL DEFAULT ''; ALTER TABLE chronicle_streams ADD COLUMN scheme TEXT NOT NULL DEFAULT 'chronicle/v2'; ALTER TABLE chronicle_streams ADD COLUMN scheme_since INTEGER NOT NULL DEFAULT 0; -- head_seq is not trusted to equal the true high-water mark: Append derives -- the next sequence from MAX(sequence), not head_seq, precisely because a -- crash between the event insert and the head update can leave head_seq -- lagging. Pinning from a lagging head_seq alone would misclassify the -- already-written events above it as a permanent downgrade rather than as -- history. MAX(a, b) below is SQLite's two-argument scalar max, not the -- single-argument aggregate. UPDATE chronicle_streams SET scheme_since = MAX( head_seq, (SELECT COALESCE(MAX(sequence), 0) FROM chronicle_events e WHERE e.stream_id = chronicle_streams.id) ) + 1 WHERE scheme_since = 0; `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` ALTER TABLE chronicle_streams DROP COLUMN scheme_since; ALTER TABLE chronicle_streams DROP COLUMN scheme; ALTER TABLE chronicle_events DROP COLUMN hash_key_id; ALTER TABLE chronicle_events DROP COLUMN hash_scheme; `) return err }, }, &migrate.Migration{ Name: "create_checkpoints_table", Version: "20240101000007", Comment: "Signed checkpoints over a stream's sequence range", Up: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, ` CREATE TABLE IF NOT EXISTS chronicle_checkpoints ( id TEXT PRIMARY KEY, stream_id TEXT NOT NULL REFERENCES chronicle_streams(id), app_id TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT '', from_seq INTEGER NOT NULL, to_seq INTEGER NOT NULL, from_hash TEXT NOT NULL, to_hash TEXT NOT NULL, event_count INTEGER NOT NULL, prev_checkpoint TEXT NOT NULL DEFAULT '', algorithm TEXT NOT NULL, sign_key_id TEXT NOT NULL, signature BLOB NOT NULL, signed_payload TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), -- Two constraints, not one. UNIQUE(stream_id, to_seq) only decides a -- race between checkpointers that read the same head, and two replicas -- on a busy stream routinely do not: each snapshots the stream list at -- the top of its own tick, so replica A can commit 6-10 while replica -- B, whose read predated it, is still about to commit 6-15. Both -- succeed, and the stream then reports tampered forever, because -- checkpoints are append-only and the overlap cannot be removed. -- UNIQUE(stream_id, from_seq) is what stops it: both racers derive the -- same from_seq from the same stale latest checkpoint, so the loser -- fails cleanly with checkpoint.ErrExists. UNIQUE(stream_id, to_seq), UNIQUE(stream_id, from_seq) ); CREATE INDEX IF NOT EXISTS idx_chronicle_checkpoints_stream ON chronicle_checkpoints (stream_id, to_seq DESC); CREATE INDEX IF NOT EXISTS idx_chronicle_checkpoints_scope ON chronicle_checkpoints (app_id, tenant_id, created_at DESC); `) return err }, Down: func(ctx context.Context, exec migrate.Executor) error { _, err := exec.Exec(ctx, `DROP TABLE IF EXISTS chronicle_checkpoints;`) return err }, }, &migrate.Migration{ Name: "fixed_width_timestamps", Version: "20240101000008", Comment: "Rewrite RFC3339Nano timestamps with a fixed nine-digit fraction", Up: func(ctx context.Context, exec migrate.Executor) error { for _, tc := range timestampColumns { if _, err := exec.Exec(ctx, normalizeTimestampSQL(tc[0], tc[1])); err != nil { return fmt.Errorf("normalize %s.%s: %w", tc[0], tc[1], err) } } return nil }, Down: func(context.Context, migrate.Executor) error { return nil }, }, &migrate.Migration{ Name: "add_request_correlation_columns", Version: "20260922000001", Comment: "Add user_agent, request_id and session_id to chronicle_events", Up: func(ctx context.Context, exec migrate.Executor) error { for _, stmt := range []string{ `ALTER TABLE chronicle_events ADD COLUMN user_agent TEXT NOT NULL DEFAULT ''`, `ALTER TABLE chronicle_events ADD COLUMN request_id TEXT NOT NULL DEFAULT ''`, `ALTER TABLE chronicle_events ADD COLUMN session_id TEXT NOT NULL DEFAULT ''`, `CREATE INDEX IF NOT EXISTS idx_chronicle_events_session ON chronicle_events (session_id, timestamp DESC) WHERE session_id != ''`, } { if _, err := exec.Exec(ctx, stmt); err != nil { return err } } return nil }, Down: func(ctx context.Context, exec migrate.Executor) error { for _, stmt := range []string{ `DROP INDEX IF EXISTS idx_chronicle_events_session`, `ALTER TABLE chronicle_events DROP COLUMN session_id`, `ALTER TABLE chronicle_events DROP COLUMN request_id`, `ALTER TABLE chronicle_events DROP COLUMN user_agent`, } { if _, err := exec.Exec(ctx, stmt); err != nil { return err } } return nil }, }, &migrate.Migration{ Name: "add_erasure_status", Version: "20260929000001", Comment: "Add status and legacy_key_retained to chronicle_erasures", Up: func(ctx context.Context, exec migrate.Executor) error { for _, stmt := range []string{ `ALTER TABLE chronicle_erasures ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'`, `ALTER TABLE chronicle_erasures ADD COLUMN legacy_key_retained INTEGER NOT NULL DEFAULT 0`, } { if _, err := exec.Exec(ctx, stmt); err != nil { return err } } return nil }, Down: func(ctx context.Context, exec migrate.Executor) error { for _, stmt := range []string{ `ALTER TABLE chronicle_erasures DROP COLUMN legacy_key_retained`, `ALTER TABLE chronicle_erasures DROP COLUMN status`, } { if _, err := exec.Exec(ctx, stmt); err != nil { return err } } return nil }, }, ) return g }()
Migrations is the grove migration group for the Chronicle SQLite store.
Functions ¶
This section is empty.
Types ¶
type ArchiveModel ¶
type ArchiveModel struct {
grove.BaseModel `grove:"table:chronicle_archives,alias:a"`
ID string `grove:"id,pk"`
PolicyID string `grove:"policy_id"`
Category string `grove:"category"`
EventCount int64 `grove:"event_count"`
FromTimestamp string `grove:"from_timestamp"` // TEXT, written with timeLayout
ToTimestamp string `grove:"to_timestamp"` // TEXT, written with timeLayout
SinkName string `grove:"sink_name"`
SinkRef string `grove:"sink_ref"`
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
}
ArchiveModel is the grove ORM model for the chronicle_archives table.
type CheckpointModel ¶
type CheckpointModel struct {
grove.BaseModel `grove:"table:chronicle_checkpoints,alias:cp"`
ID string `grove:"id,pk"`
StreamID string `grove:"stream_id"`
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
FromSeq uint64 `grove:"from_seq"`
ToSeq uint64 `grove:"to_seq"`
FromHash string `grove:"from_hash"`
ToHash string `grove:"to_hash"`
EventCount int64 `grove:"event_count"`
PrevCheckpoint string `grove:"prev_checkpoint"`
Algorithm string `grove:"algorithm"`
SignKeyID string `grove:"sign_key_id"`
Signature []byte `grove:"signature"` // BLOB
SignedPayload string `grove:"signed_payload"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
}
CheckpointModel is the grove ORM model for the chronicle_checkpoints table.
type ErasureModel ¶
type ErasureModel struct {
grove.BaseModel `grove:"table:chronicle_erasures,alias:er"`
ID string `grove:"id,pk"`
SubjectID string `grove:"subject_id"`
Reason string `grove:"reason"`
RequestedBy string `grove:"requested_by"`
EventsAffected int64 `grove:"events_affected"`
KeyDestroyed int `grove:"key_destroyed"` // INTEGER boolean
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
Status string `grove:"status"`
LegacyKeyRetained int `grove:"legacy_key_retained"` // INTEGER boolean
}
ErasureModel is the grove ORM model for the chronicle_erasures table.
type EventModel ¶
type EventModel struct {
grove.BaseModel `grove:"table:chronicle_events,alias:e"`
ID string `grove:"id,pk"`
StreamID string `grove:"stream_id"`
Sequence uint64 `grove:"sequence"`
Hash string `grove:"hash"`
PrevHash string `grove:"prev_hash"`
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
UserID string `grove:"user_id"`
IP string `grove:"ip"`
UserAgent string `grove:"user_agent"`
RequestID string `grove:"request_id"`
SessionID string `grove:"session_id"`
Action string `grove:"action"`
Resource string `grove:"resource"`
Category string `grove:"category"`
ResourceID string `grove:"resource_id"`
Metadata string `grove:"metadata"` // JSON TEXT in SQLite
Outcome string `grove:"outcome"`
Severity string `grove:"severity"`
Reason string `grove:"reason"`
SubjectID string `grove:"subject_id"`
EncryptionKeyID string `grove:"encryption_key_id"`
Erased int `grove:"erased"` // INTEGER boolean in SQLite
ErasedAt string `grove:"erased_at"` // TEXT nullable timestamp
ErasureID string `grove:"erasure_id"`
Timestamp string `grove:"timestamp"` // TEXT, written with timeLayout
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
HashScheme string `grove:"hash_scheme"`
HashKeyID string `grove:"hash_key_id"`
}
EventModel is the grove ORM model for the chronicle_events table.
type Option ¶
type Option func(*Store)
Option configures a Store constructed by New.
func WithHasher ¶
WithHasher sets the chain the store re-links with.
Append re-derives the sequence and prev_hash inside its transaction, which means it must also recompute the digest. That recomputation has to use the same scheme Chronicle was configured with; a store left on the default plain chain would quietly downgrade every event it re-linked.
type ReportModel ¶
type ReportModel struct {
grove.BaseModel `grove:"table:chronicle_reports,alias:r"`
ID string `grove:"id,pk"`
Title string `grove:"title"`
Type string `grove:"type"`
PeriodFrom string `grove:"period_from"` // TEXT, written with timeLayout
PeriodTo string `grove:"period_to"` // TEXT, written with timeLayout
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
Format string `grove:"format"`
Data string `grove:"data"` // JSON TEXT from compliance.EncodeReportBody
GeneratedBy string `grove:"generated_by"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
}
ReportModel is the grove ORM model for the chronicle_reports table.
type RetentionPolicyModel ¶
type RetentionPolicyModel struct {
grove.BaseModel `grove:"table:chronicle_retention_policies,alias:rp"`
ID string `grove:"id,pk"`
Category string `grove:"category"`
Duration int64 `grove:"duration"` // nanoseconds
Archive int `grove:"archive"` // INTEGER boolean
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
UpdatedAt string `grove:"updated_at"` // TEXT, written with timeLayout
}
RetentionPolicyModel is the grove ORM model for the chronicle_retention_policies table.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store implements the Chronicle store interface using grove ORM with SQLite.
func New ¶
New creates a new grove ORM store with the given database connection.
Without WithHasher, Append re-links under a zero-value hash.Chain, which is SchemePlainV4 with no key provider: unkeyed, but with the unambiguous content encoding, so a default-configured store is not writing forgeable digests.
func (*Store) Aggregate ¶
func (s *Store) Aggregate(ctx context.Context, q *audit.AggregateQuery) (*audit.AggregateResult, error)
Aggregate returns grouped event statistics.
func (*Store) Append ¶
Append persists a single audit event, allocating its sequence number authoritatively inside a transaction.
This mirrors the postgres backend: the sequence is a positional counter and is not part of the hash chain, so the store owns its allocation. Deriving the next value from the greater of the stored head and the actual MAX(sequence) makes a desynced head self-healing, and doing it inside the transaction that inserts the event keeps concurrent appends from colliding on UNIQUE(stream_id, sequence).
func (*Store) AppendBatch ¶
AppendBatch persists multiple events atomically in a transaction.
func (*Store) AppendCheckpoint ¶
func (s *Store) AppendCheckpoint(ctx context.Context, cp *checkpoint.Checkpoint) error
AppendCheckpoint persists a checkpoint. Checkpoints are append-only: there is no update or delete, since a checkpoint that could be revised would assert nothing.
UNIQUE(stream_id, to_seq) and UNIQUE(stream_id, from_seq) on chronicle_checkpoints are the structural backstop against two checkpointers racing on one stream -- a background ticker, an operator-triggered run, or a second replica's ticker -- so the loser of that race gets checkpoint.ErrExists straight from the database rather than from a read-then-insert check a concurrent writer could still slip past.
Both constraints are load-bearing. See migration 007 for why to_seq alone lets two replicas reading different heads write overlapping checkpoints.
func (*Store) ByUser ¶
func (s *Store) ByUser(ctx context.Context, userID string, opts audit.TimeRange) (*audit.QueryResult, error)
ByUser returns events for a specific user within a time range.
func (*Store) CheckpointsInRange ¶
func (s *Store) CheckpointsInRange( ctx context.Context, streamID id.ID, fromSeq, toSeq uint64, ) ([]*checkpoint.Checkpoint, error)
CheckpointsInRange returns every checkpoint whose range overlaps [fromSeq, toSeq], ascending by ToSeq.
Overlap rather than containment: a caller verifying sequences 150 to 160 still needs the checkpoint covering 101 to 200, because that is the one whose signed assertion those events fall under. Containment would return nothing for a range that sits inside a wider checkpoint.
func (*Store) CompleteErasure ¶
CompleteErasure writes an erasure's outcome and marks it completed.
func (*Store) CountBySubject ¶
CountBySubject returns the number of events for a subject within the query's scope.
Security-critical: without the scope filter this reveals how many events other tenants hold on the subject.
func (*Store) CountErasures ¶
CountErasures returns the number of erasure records in the given scope.
func (*Store) CreateStream ¶
CreateStream initializes a new hash chain stream.
func (*Store) DeletePolicy ¶
DeletePolicy removes a retention policy.
func (*Store) DeleteReport ¶
DeleteReport removes a report by ID.
func (*Store) EventRange ¶
func (s *Store) EventRange(ctx context.Context, streamID id.ID, fromSeq, toSeq uint64) ([]*audit.Event, error)
EventRange returns events in a sequence range for chain verification.
func (*Store) EventsOlderThan ¶
func (s *Store) EventsOlderThan( ctx context.Context, pq retention.PurgeQuery, ) ([]*audit.Event, error)
EventsOlderThan returns the events the purge query selects.
Security-critical: the scope filter is what keeps one tenant's policy from selecting, and therefore purging, every tenant's history. Both columns are always compared, so an empty TenantID selects untenanted events rather than every tenant. The bound keeps a large backlog from being loaded into memory all at once.
func (*Store) Gaps ¶
Gaps detects missing sequence numbers in a range. Since SQLite does not have generate_series by default, we query existing sequences and compute the gaps in Go.
func (*Store) GetCheckpoint ¶
func (s *Store) GetCheckpoint(ctx context.Context, checkpointID id.ID) (*checkpoint.Checkpoint, error)
GetCheckpoint returns one checkpoint by ID.
func (*Store) GetErasure ¶
GetErasure returns an erasure record by ID.
func (*Store) GetStreamByScope ¶
func (s *Store) GetStreamByScope(ctx context.Context, appID, tenantID string) (*stream.Stream, error)
GetStreamByScope returns the stream for a given app+tenant scope.
func (*Store) LastSequence ¶
LastSequence returns the highest sequence number for a stream.
func (*Store) LatestCheckpoint ¶
func (s *Store) LatestCheckpoint(ctx context.Context, streamID id.ID) (*checkpoint.Checkpoint, error)
LatestCheckpoint returns the checkpoint with the highest ToSeq for a stream.
func (*Store) ListArchives ¶
func (s *Store) ListArchives(ctx context.Context, opts retention.ListOpts) ([]*retention.Archive, error)
ListArchives returns archive records matching opts, scoped before pagination.
func (*Store) ListCheckpoints ¶
func (s *Store) ListCheckpoints( ctx context.Context, streamID id.ID, opts checkpoint.ListOpts, ) ([]*checkpoint.Checkpoint, error)
ListCheckpoints returns a stream's checkpoints, newest first.
func (*Store) ListErasures ¶
func (s *Store) ListErasures(ctx context.Context, opts erasure.ListOpts) ([]*erasure.Erasure, error)
ListErasures returns erasure records matching opts, scoped before pagination.
func (*Store) ListPolicies ¶
func (s *Store) ListPolicies( ctx context.Context, opts retention.ListPoliciesOpts, ) ([]*retention.Policy, error)
ListPolicies returns retention policies matching opts, scoped before pagination so a caller's own rows are never hidden behind another tenant's.
func (*Store) ListReports ¶
func (s *Store) ListReports(ctx context.Context, opts compliance.ListOpts) ([]*compliance.Report, error)
ListReports returns reports matching opts, scoped before pagination.
func (*Store) ListStreams ¶
ListStreams returns all streams with pagination.
func (*Store) MarkErased ¶
func (s *Store) MarkErased( ctx context.Context, sq erasure.SubjectQuery, erasureID id.ID, ) (int64, error)
MarkErased flags a subject's events as erased within the query's scope.
Security-critical: without the scope filter any caller could flag every tenant's events for a guessed subject ID.
func (*Store) PurgeEvents ¶
PurgeEvents permanently deletes events by IDs.
func (*Store) RecordArchive ¶
RecordArchive records that a batch of events was archived.
func (*Store) RecordErasure ¶
RecordErasure persists an erasure event.
func (*Store) SavePolicy ¶
SavePolicy persists a retention policy (INSERT or UPDATE on conflict).
The conflict target is the owning scope plus the category, not the category alone: a policy must never overwrite, or take ownership of, another app or tenant's policy for the same category.
func (*Store) SaveReport ¶
SaveReport persists a generated compliance report.
func (*Store) SetHasher ¶
SetHasher replaces the chain the store re-links with after construction.
This exists for a store built directly (e.g. passed to extension.WithStore) rather than through New with WithHasher: it lets a caller who already holds the Store give it the chain later, which is what lets that path be brought up to the configured digest scheme instead of silently staying on the default plain one.
func (*Store) SubjectKeyUsage ¶
SubjectKeyUsage groups a subject's events across every scope by app, tenant, encryption key ID and erased flag.
Security-critical in the other direction: it is unscoped on purpose, so the erasure service can see every scope sharing a legacy key. It must never back a response to a caller; see erasure.Store.
type StreamModel ¶
type StreamModel struct {
grove.BaseModel `grove:"table:chronicle_streams,alias:s"`
ID string `grove:"id,pk"`
AppID string `grove:"app_id"`
TenantID string `grove:"tenant_id"`
HeadHash string `grove:"head_hash"`
HeadSeq uint64 `grove:"head_seq"`
CreatedAt string `grove:"created_at"` // TEXT, written with timeLayout
UpdatedAt string `grove:"updated_at"` // TEXT, written with timeLayout
Scheme string `grove:"scheme"`
SchemeSince uint64 `grove:"scheme_since"`
}
StreamModel is the grove ORM model for the chronicle_streams table.