storage

package
v0.0.0-...-918c0af Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package storage exposes a request-scoped DB query counter so HTTP middleware can observe the number of database round-trips a single panel request fires. Closes audit P-02: the audit suspected N+1 patterns in `clients_flow.go` / `agent_flow.go` but had no way to confirm without SQL tracing. This file plus instrumentedExecutor.go in each backend provides that confirmation surface.

Index

Constants

View Source
const (
	ConfigScopeGroup = "group"
	ConfigScopeAgent = "agent"
)

Config target scope kinds.

View Source
const (
	ConfigApplyBatchModeAllAtOnce = "all_at_once"
	ConfigApplyBatchModeRolling   = "rolling"
)

Config-apply batch rollout modes.

View Source
const (
	ConfigApplyBatchStatusRunning   = "running"
	ConfigApplyBatchStatusSucceeded = "succeeded"
	ConfigApplyBatchStatusFailed    = "failed"
	ConfigApplyBatchStatusHalted    = "halted"
)

Config-apply batch lifecycle statuses.

View Source
const (
	ConfigApplyTargetStatusPending   = "pending"
	ConfigApplyTargetStatusRunning   = "running"
	ConfigApplyTargetStatusSucceeded = "succeeded"
	ConfigApplyTargetStatusFailed    = "failed"
	ConfigApplyTargetStatusSkipped   = "skipped"
)

Config-apply batch per-target delivery statuses.

View Source
const (
	DefaultCursorPageSize = 100
	MaxCursorPageSize     = 500
)

Cursor pagination defaults (S25 T1). Callers omit Limit (== 0) to get DefaultCursorPageSize; values above MaxCursorPageSize are clamped down so an operator cannot ask for an unbounded page.

View Source
const DefaultListLimit = 1000

DefaultListLimit caps unbounded list queries so a long-lived control plane can never stream millions of rows back to the caller (P-7). Queries that already accept an explicit limit override this value; queries that have no natural ceiling (per-client IP history, raw timeseries) apply this value silently as a safety floor. 1000 is large enough that the dashboard page- sized views (typically <= 500 rows) are unaffected, while bounding the worst case at a few hundred kilobytes of result data.

Variables

View Source
var (
	// ErrNotFound reports a missing persisted record.
	ErrNotFound = errors.New("storage record not found")
	// ErrConflict reports a uniqueness or state-transition conflict during persistence.
	ErrConflict = errors.New("storage conflict")
	// ErrNestedTransact is returned when a TxFn calls Transact on the tx
	// argument. Transactions are not reentrant — see P2-ARCH-01.
	ErrNestedTransact = errors.New("storage: nested Transact call not allowed")
)

Functions

func DBQueryCount

func DBQueryCount(ctx context.Context) int64

DBQueryCount returns the number of queries seen since WithDBQueryCounter was called on ctx. Returns 0 when no counter is attached.

func DecodeKeysetCursor

func DecodeKeysetCursor(encoded string) (time.Time, string, error)

DecodeKeysetCursor parses an opaque cursor produced by EncodeKeysetCursor. An empty input returns the zero cursor (first page). Malformed input returns an error so the HTTP layer can respond 400 — silently treating garbage as "first page" would let a stale-but-valid-looking cursor produce wrong results without the client noticing.

func EncodeKeysetCursor

func EncodeKeysetCursor(createdAt time.Time, id string) string

EncodeKeysetCursor returns the base64-url JSON encoding of the (createdAt, id) keyset position. Empty id == sentinel "first page" — callers should pass "" for both arguments to mean that.

func IncrementDBQuery

func IncrementDBQuery(ctx context.Context)

IncrementDBQuery is called by instrumented dbExecutor wrappers on every query (Exec/Query/QueryRow). When ctx carries no counter (operations outside a tracked HTTP request — startup, batch writer, background jobs), this is a cheap no-op.

func NormalizeCursorLimit

func NormalizeCursorLimit(limit int) int

NormalizeCursorLimit applies the DefaultCursorPageSize/MaxCursorPageSize envelope. Exposed so storage backends and HTTP handlers stay consistent.

func WithDBQueryCounter

func WithDBQueryCounter(ctx context.Context) context.Context

WithDBQueryCounter installs a fresh atomic counter onto ctx. Call once at the start of every operation whose query count you want to measure (typically the HTTP middleware on each inbound request). Returns a derived ctx that downstream storage calls can read.

Types

type AgentCertPins

type AgentCertPins struct {
	Serial       string
	SPKI         []byte
	PrevSerial   string
	PrevSPKI     []byte
	OverlapUntil *time.Time
}

AgentRecord stores one enrolled host agent snapshot. AgentCertPins is the set of agent credentials the panel accepts at a given moment: the current one, and — while a rotation overlap window is open — the previous one. OverlapUntil is nil when no rotation is in flight.

The pair is deliberately small and time-bounded: it exists so an interrupted renewal cannot strand a node on a certificate the panel has already forgotten (R11), not to soften the fail-closed pinning.

type AgentCertificateRecoveryGrantRecord

type AgentCertificateRecoveryGrantRecord struct {
	AgentID   string
	IssuedBy  string
	IssuedAt  time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
	RevokedAt *time.Time
}

AgentCertificateRecoveryGrantRecord stores one administrator-approved certificate recovery window.

type AgentCertificateRecoveryGrantStore

type AgentCertificateRecoveryGrantStore interface {
	PutAgentCertificateRecoveryGrant(ctx context.Context, grant AgentCertificateRecoveryGrantRecord) error
	ListAgentCertificateRecoveryGrants(ctx context.Context) ([]AgentCertificateRecoveryGrantRecord, error)
	GetAgentCertificateRecoveryGrant(ctx context.Context, agentID string) (AgentCertificateRecoveryGrantRecord, error)
	UseAgentCertificateRecoveryGrant(ctx context.Context, agentID string, usedAt time.Time) (AgentCertificateRecoveryGrantRecord, error)
	RevokeAgentCertificateRecoveryGrant(ctx context.Context, agentID string, revokedAt time.Time) (AgentCertificateRecoveryGrantRecord, error)
}

AgentCertificateRecoveryGrantStore persists administrator-approved certificate recovery windows.

type AgentConfigTargetRecord

type AgentConfigTargetRecord struct {
	ScopeType    string
	ScopeID      string
	SectionsJSON string
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

AgentConfigTargetRecord is the operator's desired Telemt config for one scope. ScopeType is ConfigScopeGroup (ScopeID = fleet group id) or ConfigScopeAgent (ScopeID = agent id). SectionsJSON is a sparse JSON object of editable config sections (general/timeouts/censorship/upstreams/dc_overrides).

type AgentFallbackStateRecord

type AgentFallbackStateRecord struct {
	AgentID   string
	EnteredAt time.Time
}

AgentFallbackStateRecord persists when an agent first entered ME→Direct fallback. Cleared when MERuntimeReady returns to true. EnteredAt is the stable origin used to compute fallback duration for severity bucketing even across control-plane restarts.

type AgentFallbackStateStore

type AgentFallbackStateStore interface {
	PutAgentFallbackState(ctx context.Context, rec AgentFallbackStateRecord) error
	DeleteAgentFallbackState(ctx context.Context, agentID string) error
	GetAgentFallbackState(ctx context.Context, agentID string) (AgentFallbackStateRecord, error)
	// TODO(cursor): add ListAgentFallbackStateCursor next sprint. This is the
	// boot-time restore path; deferred from S25 T1 because the bound is
	// "agents in fallback simultaneously" which is small in practice.
	ListAgentFallbackState(ctx context.Context) ([]AgentFallbackStateRecord, error)
}

AgentFallbackStateStore persists per-agent ME→Direct fallback windows. A row exists exactly while the agent is currently in fallback: it is inserted when MERuntimeReady first flips to false and deleted when it returns to true. The persisted EnteredAt is the source of truth for fallback-duration severity classification across panel restarts. See AgentFallbackStateRecord in models.go.

type AgentRecord

type AgentRecord struct {
	ID            string
	NodeName      string
	FleetGroupID  string
	Version       string
	ReadOnly      bool
	LastSeenAt    time.Time
	CertIssuedAt  *time.Time
	CertExpiresAt *time.Time
	// CertSerial pins the latest issued certificate's serial number so a
	// previously-issued cert (e.g. a not-yet-expired old one harvested
	// from a backup or rotation log) cannot impersonate the agent
	// (Q4.U-S-04). Hex-encoded big-endian serial.
	CertSerial string
	// CertSPKISHA256 is the SHA-256 hash of the agent's serving cert SPKI,
	// set on first successful enroll (S-02). Empty bytes mean "not yet
	// pinned"; subsequent dials must verify the served cert hashes to this
	// value via storage.UpdateAgentCertPin.
	CertSPKISHA256 []byte
	// BootstrapState is the enrollment lifecycle state ("pending",
	// "expired", "active"; empty for legacy inbound rows). Written by the
	// bootstrap token/enroll paths; surfaced so a half-added node restores
	// and renders as pending rather than offline (R9a).
	BootstrapState string
	// TransportMode is the persisted dial mode ("inbound"/"outbound").
	TransportMode string
	// DialAddress is the panel-dials-agent target for outbound rows.
	DialAddress string
}

type AgentRevocationRecord

type AgentRevocationRecord struct {
	AgentID       string
	RevokedAt     time.Time
	CertExpiresAt time.Time
}

AgentRevocationRecord tracks one deregistered agent whose mTLS client certificate may still be cryptographically valid. The record survives control-plane restart so a revoked agent cannot silently reconnect. CertExpiresAt is the cert validity cut-off; once the cert has expired the row is eligible for pruning because the cert can no longer authenticate regardless of the revocation list.

type AgentRevocationStore

type AgentRevocationStore interface {
	PutAgentRevocation(ctx context.Context, revocation AgentRevocationRecord) error
	ListAgentRevocations(ctx context.Context) ([]AgentRevocationRecord, error)
	DeleteExpiredAgentRevocations(ctx context.Context, before time.Time) (int64, error)
}

AgentRevocationStore persists deregistered-agent IDs so the revocation set survives control-plane restart. See AgentRevocationRecord in models.go.

type AgentUpdateStrategyRecord

type AgentUpdateStrategyRecord struct {
	AgentID     string
	Mode        string
	RestartSpec string
	BinaryPath  string
	AssetFlavor string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

AgentUpdateStrategyRecord is the persisted Telemt update strategy for one agent (Telemt Update v1, Task 8): how `telemt.update` jobs should be applied on that agent. Mode is one of "binary" | "docker" | "none". RestartSpec / BinaryPath carry the resolved telemtrestart.Spec fields; AssetFlavor distinguishes release-asset variants (e.g. "" | "v3").

type AuditEventRecord

type AuditEventRecord struct {
	ID        string
	ActorID   string
	Action    string
	TargetID  string
	CreatedAt time.Time
	Details   map[string]any
	PrevHash  string
	EventHash string
}

AuditEventRecord stores one immutable control-plane audit event.

PrevHash and EventHash form the tamper-evident chain (migration 0038). PrevHash is the EventHash of the previous row in (created_at, id) ascending order; EventHash is sha256(prev_hash || canonical(record)) computed by the producer side. Both fields are empty on rows written before the migration and on the synthetic genesis position. The verify-audit-chain subcommand walks the chain and recomputes; any admin who silently rewrites a row will fail the verifier.

type AuditStore

type AuditStore interface {
	AppendAuditEvent(ctx context.Context, event AuditEventRecord) error
	// AppendAuditEventsBulk inserts a batch of audit events in one
	// transaction (P6-6.1b, finding #10). Rows are inserted in slice
	// order; hash-chain fields (PrevHash/EventHash) are computed by the
	// producer BEFORE buffering, so bulk insertion does not affect chain
	// integrity. IDs are unique — no conflict clause.
	AppendAuditEventsBulk(ctx context.Context, events []AuditEventRecord) error
	// ListAuditEvents returns the most recent audit events in ascending
	// chronological order. limit caps the number of rows returned; values
	// <= 0 fall back to a hard maximum of 1024.
	ListAuditEvents(ctx context.Context, limit int) ([]AuditEventRecord, error)
	// ListAuditEventsCursor returns one page in (created_at DESC, id DESC)
	// order — newest first — for keyset pagination. The returned next cursor
	// is non-empty iff a further page may exist. Limit follows
	// DefaultCursorPageSize / MaxCursorPageSize. Note this differs from
	// ListAuditEvents which returns ASCENDING for legacy timeline replay;
	// the cursor variant goes newest-first because that's the operator's
	// audit-page reading order.
	ListAuditEventsCursor(ctx context.Context, params ListAuditEventsCursorParams) ([]AuditEventRecord, ListAuditEventsCursorParams, error)
	// PruneAuditEvents deletes audit_events rows with created_at strictly
	// before the cutoff and returns the number of deleted rows. Used by the
	// retention worker (P2-REL-04 / finding M-R2) to keep audit_events from
	// growing unbounded.
	PruneAuditEvents(ctx context.Context, before time.Time) (int64, error)
	// LatestAuditChainHash returns the event_hash of the most recently
	// persisted audit row, or "" when the table is empty (no chain yet).
	// Producers read this once before each batch flush so each new row's
	// PrevHash is the previous row's EventHash. Migration 0038 added the
	// underlying columns.
	LatestAuditChainHash(ctx context.Context) (string, error)
}

AuditStore persists immutable operator and security events.

type CPSecretRecord

type CPSecretRecord struct {
	Key       string
	Value     []byte
	UpdatedAt time.Time
}

CPSecretRecord is one row of the cp_secrets key/value table. Value is raw byte material (e.g. the CSRF HMAC seed) stored verbatim. Used only by the offline migrate tooling to enumerate every secret for a table-complete sqlite→postgres copy; the runtime path reads/writes a single key at a time via Get/PutCPSecret.

type CPSecretStore

type CPSecretStore interface {
	GetCPSecret(ctx context.Context, key string) ([]byte, error)
	PutCPSecret(ctx context.Context, key string, value []byte) error
	// ListCPSecrets returns every cp_secrets row. cp_secrets has no
	// natural listing method on the runtime path (callers always know
	// the key); this exists for the offline migrate tooling so the
	// table-complete copy can enumerate and re-Put every secret
	// verbatim. Values are raw bytes — never re-encoded.
	ListCPSecrets(ctx context.Context) ([]CPSecretRecord, error)
}

CPSecretStore persists small opaque per-cluster secrets like the CSRF HMAC seed (Q2.U-S-24). Values are bytes so callers can store raw key material without an extra encoding hop.

type CertificateAuthorityRecord

type CertificateAuthorityRecord struct {
	CAPEM         string
	PrivateKeyPEM string
	UpdatedAt     time.Time
}

CertificateAuthorityRecord stores the persisted control-plane root CA material.

type CertificateAuthorityStore

type CertificateAuthorityStore interface {
	PutCertificateAuthority(ctx context.Context, authority CertificateAuthorityRecord) error
	GetCertificateAuthority(ctx context.Context) (CertificateAuthorityRecord, error)
}

CertificateAuthorityStore persists the control-plane root CA required for agent mTLS continuity.

type ClientAssignmentRecord

type ClientAssignmentRecord struct {
	ID           string
	ClientID     string
	TargetType   string
	FleetGroupID string
	AgentID      string
	CreatedAt    time.Time
}

ClientAssignmentRecord stores one desired rollout target for a managed client.

type ClientDeploymentRecord

type ClientDeploymentRecord struct {
	ClientID           string
	AgentID            string
	DesiredOperation   string
	Status             string
	LastError          string
	ConnectionLinks    []string
	LinkDiagnostic     string
	LastAppliedAt      *time.Time
	UpdatedAt          time.Time
	LastResetEpochSecs uint64
}

ClientDeploymentRecord stores the current rollout state for one client on one agent. ConnectionLinks holds every Telemt-reported link for this user (one per tls_domain × host combination). Stored as a JSON array on disk. LastResetEpochSecs is the unix-seconds value Telemt returned the last time the panel completed a client.reset_quota job here — zero when the panel has never reset this pair. Used for drift detection against Telemt's currently-reported quota_last_reset_unix. LinkDiagnostic is an operator-facing warning persisted alongside an otherwise-successful apply (IN-M2). Empty means "no issue". Non-empty when the node reported no connection links on a non-delete success, so ConnectionLinks may be stale.

type ClientIPAggregateRecord

type ClientIPAggregateRecord struct {
	IPAddress string
	FirstSeen time.Time
	LastSeen  time.Time
}

ClientIPAggregateRecord captures the per-IP aggregate across nodes: the earliest first-seen and the latest last-seen for a single IP address, regardless of which agent reported each end of the window. Used by the dashboard's IP-history endpoint so the heavy aggregation runs in SQL instead of in CP memory.

type ClientIPHistoryRecord

type ClientIPHistoryRecord struct {
	AgentID   string
	ClientID  string
	IPAddress string
	FirstSeen time.Time
	LastSeen  time.Time
}

ClientIPHistoryRecord stores one unique IP seen for a client on an agent.

type ClientRecord

type ClientRecord struct {
	ID                string
	Name              string
	SecretCiphertext  string
	UserADTag         string
	Enabled           bool
	MaxTCPConns       int
	MaxUniqueIPs      int
	DataQuotaBytes    int64
	ExpirationRFC3339 string
	SubscriptionToken string // opaque /sub/<token> handle; "" means not yet generated
	CreatedAt         time.Time
	UpdatedAt         time.Time
	DeletedAt         *time.Time
}

ClientRecord stores one centrally managed Telemt client definition.

type ClientStore deprecated

type ClientStore interface {
	PutClient(ctx context.Context, client ClientRecord) error
	ListClients(ctx context.Context) ([]ClientRecord, error)
	PutClientAssignment(ctx context.Context, assignment ClientAssignmentRecord) error
	ListClientAssignments(ctx context.Context, clientID string) ([]ClientAssignmentRecord, error)
	PutClientDeployment(ctx context.Context, deployment ClientDeploymentRecord) error
	ListClientDeployments(ctx context.Context, clientID string) ([]ClientDeploymentRecord, error)
	// Per-(client, agent) usage counters. Persisted so the in-memory
	// server.clientUsage map can rehydrate across restarts without losing
	// accumulated traffic totals.
	//
	// UpsertClientUsage is the single-row variant. The live agent-flow
	// telemetry hot path has since moved to the clients_repository-backed
	// domain service (clientsSvc.UpsertUsage / UpsertUsageBulk — see
	// server/agent_flow.go persistClientUsageRecords), not this Store-level
	// method. UpsertClientUsage itself is still a genuine production caller
	// via storage.MigrationStore (migrate-schema CLI, copy_extra.go
	// copyTierOneEntities) plus the storagetest contract suite — do not
	// remove.
	UpsertClientUsage(ctx context.Context, record ClientUsageRecord) error
	ListClientUsage(ctx context.Context) ([]ClientUsageRecord, error)
	DeleteClientUsageByClient(ctx context.Context, clientID string) error
}

ClientStore persists centrally managed Telemt clients, rollout assignments, and per-node deployment state.

Deprecated: Wave 4.2 replaced direct Store access with clients.Repository + UnitOfWork. No longer embedded in the Store aggregate (AC#10). Retained as a standalone interface for MigrationStore (migrate-schema CLI) and storagetest Transact contract tests.

type ClientUsageRecord

type ClientUsageRecord struct {
	ClientID           string
	AgentID            string
	TrafficUsedBytes   uint64
	UniqueIPsUsed      int
	ActiveTCPConns     int
	ActiveUniqueIPs    int
	QuotaUsedBytes     uint64
	QuotaLastResetUnix uint64
	AgentBootID        string
	LastTotalBytes     uint64
	ObservedAt         time.Time
}

ClientUsageRecord stores the lifetime traffic + live-gauge counters for one (client, agent) pair. Persisted so the usage mirror rehydrates across panel restarts without losing accumulated totals. AgentBootID + LastTotalBytes are the P4 cumulative-counter watermark: the reporting agent process epoch and the last cumulative total seen for it; TrafficUsedBytes is the panel-accumulated absolute.

type ConfigApplyBatchRecord

type ConfigApplyBatchRecord struct {
	ID               string
	FleetGroupID     string
	Mode             string
	WaveSize         int
	ExpectedRevision string
	Status           string
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

ConfigApplyBatchRecord tracks one group-wide config-apply rollout: the operator triggers a config push to every agent in FleetGroupID, and the batch coordinates delivery across one or more waves (see ConfigApplyBatchTargetRecord.WaveIndex) depending on Mode. ExpectedRevision pins the agent_config_targets revision this batch is rolling out, so a concurrent edit to the group's desired config cannot silently change what an in-flight batch delivers.

type ConfigApplyBatchStore

type ConfigApplyBatchStore interface {
	// CreateConfigApplyBatch inserts a batch row and its full target set
	// atomically: either every row lands or none does. Implementations
	// MUST run this inside a single transaction.
	CreateConfigApplyBatch(ctx context.Context, b ConfigApplyBatchRecord, targets []ConfigApplyBatchTargetRecord) error
	// GetConfigApplyBatch returns the batch plus every target row, ordered
	// by wave_index then agent_id. Returns ErrNotFound when no batch with
	// the given id exists.
	GetConfigApplyBatch(ctx context.Context, id string) (ConfigApplyBatchRecord, []ConfigApplyBatchTargetRecord, error)
	// ListRunningConfigApplyBatches returns every batch whose status is
	// ConfigApplyBatchStatusRunning. Used by the coordinator to resume
	// in-flight rollouts after a restart.
	ListRunningConfigApplyBatches(ctx context.Context) ([]ConfigApplyBatchRecord, error)
	// ActiveConfigApplyBatchForGroup returns the running batch for a fleet
	// group, if any. The bool is false (with a zero-value record) when the
	// group has no batch in ConfigApplyBatchStatusRunning.
	ActiveConfigApplyBatchForGroup(ctx context.Context, fleetGroupID string) (ConfigApplyBatchRecord, bool, error)
	// UpdateConfigApplyBatchStatus transitions a batch's status and bumps
	// updated_at. Returns ErrNotFound when no batch with the given id exists.
	UpdateConfigApplyBatchStatus(ctx context.Context, id, status string, now time.Time) error
	// SetConfigApplyBatchTargetJob records the job enqueued for one target
	// (wave enqueue) and updates its status in the same write.
	SetConfigApplyBatchTargetJob(ctx context.Context, batchID, agentID, jobID, status string) error
	// UpdateConfigApplyBatchTargetStatus updates one target's delivery
	// status and message without touching its job id. message is typically
	// the agent's own failure text (empty for succeeded/skipped targets) and
	// is persisted so it survives eviction of the underlying job from the
	// in-memory jobs store, keeping the batch-status view resumable.
	UpdateConfigApplyBatchTargetStatus(ctx context.Context, batchID, agentID, status, message string) error
	// PruneConfigApplyBatches deletes batches in a terminal status
	// (succeeded/failed/halted) whose updated_at predates the cutoff.
	// Targets are removed via ON DELETE CASCADE. Returns the number of
	// batches deleted.
	PruneConfigApplyBatches(ctx context.Context, before time.Time) (int64, error)
}

ConfigApplyBatchStore persists group-wide config-apply rollout batches and their per-agent target rows (see ConfigApplyBatchRecord / ConfigApplyBatchTargetRecord).

type ConfigApplyBatchTargetRecord

type ConfigApplyBatchTargetRecord struct {
	BatchID   string
	AgentID   string
	WaveIndex int
	JobID     string
	Status    string
	Message   string
}

ConfigApplyBatchTargetRecord is one agent's delivery record within a ConfigApplyBatchRecord. JobID is "" until the target's wave is enqueued (SetConfigApplyBatchTargetJob populates it alongside the job-queued status transition). Message carries the terminal outcome's human-readable reason (e.g. the agent's own failure text) and is persisted so it survives eviction of the underlying job from the in-memory jobs store — see UpdateConfigApplyBatchTargetStatus. Empty for non-terminal targets and for successful ones.

type ConsumedTotpRecord

type ConsumedTotpRecord struct {
	UserID string
	Code   string
	UsedAt time.Time
}

ConsumedTotpRecord stores one already-consumed TOTP code for replay prevention (Q2.U-S-17). The persistence layer keeps the code only long enough to bridge the verifier acceptance window (90s) so a CP restart cannot let an in-flight code be re-used.

type ConsumedTotpStore

type ConsumedTotpStore interface {
	UpsertConsumedTotp(ctx context.Context, record ConsumedTotpRecord) error
	ListConsumedTotp(ctx context.Context) ([]ConsumedTotpRecord, error)
	DeleteExpiredConsumedTotp(ctx context.Context, before time.Time) error
}

ConsumedTotpStore persists already-consumed TOTP codes for replay prevention across restarts (Q2.U-S-17). Implementations are expected to GC rows older than the verifier acceptance window via DeleteExpiredConsumedTotp; the auth service runs that GC alongside session cleanup.

type DCHealthPointRecord

type DCHealthPointRecord struct {
	AgentID         string
	CapturedAt      time.Time
	DC              int
	CoveragePctAvg  float64
	CoveragePctMin  float64
	RTTMsAvg        float64
	RTTMsMax        float64
	AliveWritersMin int
	RequiredWriters int
	LoadMax         int
	SampleCount     int
}

DCHealthPointRecord stores one aggregated DC health snapshot.

type DiscoveredClientRecord

type DiscoveredClientRecord struct {
	ID                 string
	AgentID            string
	ClientName         string
	Secret             string
	Status             string
	TotalOctets        uint64
	CurrentConnections int
	ActiveUniqueIPs    int
	ConnectionLinks    []string
	MaxTCPConns        int
	MaxUniqueIPs       int
	DataQuotaBytes     int64
	Expiration         string
	DiscoveredAt       time.Time
	UpdatedAt          time.Time
}

DiscoveredClientRecord stores one Telemt user found on an agent that is not managed by the panel.

type EnrollmentStore

type EnrollmentStore interface {
	PutEnrollmentToken(ctx context.Context, token EnrollmentTokenRecord) error
	ListEnrollmentTokens(ctx context.Context) ([]EnrollmentTokenRecord, error)
	GetEnrollmentToken(ctx context.Context, value string) (EnrollmentTokenRecord, error)
	ConsumeEnrollmentToken(ctx context.Context, value string, consumedAt time.Time) (EnrollmentTokenRecord, error)
	RevokeEnrollmentToken(ctx context.Context, value string, revokedAt time.Time) (EnrollmentTokenRecord, error)
	// PruneEnrollmentTokens deletes tokens that can never be consumed
	// again: consumed or revoked before the cutoff, or expired before
	// the cutoff while never consumed. Live tokens are never touched.
	// Returns the number of deleted rows (C4).
	PruneEnrollmentTokens(ctx context.Context, before time.Time) (int64, error)
}

EnrollmentStore persists one-time agent enrollment tokens.

type EnrollmentTokenRecord

type EnrollmentTokenRecord struct {
	Value        string
	FleetGroupID string
	IssuedAt     time.Time
	ExpiresAt    time.Time
	ConsumedAt   *time.Time
	RevokedAt    *time.Time
}

EnrollmentTokenRecord stores one enrollment token and its consumption state.

type FleetGroupIntegrationRecord

type FleetGroupIntegrationRecord struct {
	ID           string
	FleetGroupID string
	Kind         string
	ProviderID   *string
	Config       []byte
	Enabled      bool
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

FleetGroupIntegrationRecord attaches one integration install to a fleet group. At most one row per (fleet_group_id, kind). ProviderID is nullable: some integrations embed their entire config inline and do not reference a shared provider.

type FleetGroupRecord

type FleetGroupRecord struct {
	ID          string
	Name        string
	Label       string
	Description string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

FleetGroupRecord stores one fleet group in the global control-plane namespace.

ID is a UUID assigned at creation and never changes. Name is an immutable human-readable slug (unique, used in URLs / CLI / logs). Label is a free-form display name the operator can edit. Description is free text — rendered on the detail page.

type FleetStore

type FleetStore interface {
	// PutFleetGroup upserts a fleet group by id. Used by migration/copy
	// helpers and tests. HTTP-layer CRUD calls the explicit
	// Create/Update/Delete methods below, which enforce uniqueness and
	// bump updated_at.
	PutFleetGroup(ctx context.Context, group FleetGroupRecord) error
	CreateFleetGroup(ctx context.Context, group FleetGroupRecord) error
	UpdateFleetGroup(ctx context.Context, group FleetGroupRecord) error
	GetFleetGroup(ctx context.Context, id string) (FleetGroupRecord, error)
	GetFleetGroupByName(ctx context.Context, name string) (FleetGroupRecord, error)
	ListFleetGroups(ctx context.Context) ([]FleetGroupRecord, error)
	// DeleteFleetGroup removes the row. Callers are responsible for
	// reassigning or detaching dependents first (agents, enrollment
	// tokens, client_assignments); the DB enforces FK integrity so a
	// non-reassigned delete fails with a constraint error.
	DeleteFleetGroup(ctx context.Context, id string) error
	// ReassignFleetGroupMembers moves every FK reference to `fromID`
	// (agents.fleet_group_id, enrollment_tokens.fleet_group_id,
	// client_assignments.fleet_group_id) to `toID` in one transaction.
	// Returns the number of rows touched per table for audit logging.
	ReassignFleetGroupMembers(ctx context.Context, fromID, toID string) (ReassignCounts, error)
	// CountFleetGroupMembers returns how many rows in each dependent
	// table reference `fleetGroupID`. Powers the deletion-preview HTTP
	// endpoint so the operator sees the blast radius before confirming.
	CountFleetGroupMembers(ctx context.Context, fleetGroupID string) (ReassignCounts, error)
	PutAgent(ctx context.Context, agent AgentRecord) error
	// PutAgentsBulk upserts a batch of agents in a single transaction. Semantics
	// match PutAgent per row (UPSERT on id); when the same ID appears twice the
	// last occurrence wins. A nil/empty slice is a no-op that returns nil. Used
	// by the control-plane batch writer (P3-PERF-01a) to avoid N individual
	// INSERTs per flush. See also storage/postgres and storage/sqlite
	// implementations which chunk large batches.
	PutAgentsBulk(ctx context.Context, agents []AgentRecord) error
	// TODO(cursor): add ListAgentsCursor next sprint. ListAgents is currently
	// only called at boot and from the operator agents-page; the boot path is
	// safe because the table is small, but the operator path can grow without
	// bound on large fleets. S25 deferred this to keep T1 scope to the two
	// worst offenders (jobs + audit) — see commit feat(server): cursor
	// pagination for ListJobs and ListAuditEvents.
	ListAgents(ctx context.Context) ([]AgentRecord, error)
	// EarliestAgentCertExpiry returns the minimum cert_expires_at across
	// all enrolled agents, or nil when no agent has one recorded
	// (P6-6.3f, finding #14 — replaces the metrics poller's full
	// ListAgents scan with a single-scalar MIN query).
	EarliestAgentCertExpiry(ctx context.Context) (*time.Time, error)
	DeleteAgent(ctx context.Context, agentID string) error
	UpdateAgentNodeName(ctx context.Context, agentID string, nodeName string) error
	// UpdateAgentFleetGroup reassigns the agent to a different fleet
	// group. Empty fleetGroupID detaches the agent (NULL on the column).
	// Returns ErrNotFound when the agent doesn't exist; FK-constraint
	// errors surface verbatim so the HTTP layer can map them to 4xx.
	UpdateAgentFleetGroup(ctx context.Context, agentID, fleetGroupID string) error
	// UpdateAgentCertSerial pins the latest-issued client cert serial
	// for the agent so the gRPC connect path can reject any cert that
	// does not match (Q4.U-S-04).
	UpdateAgentCertSerial(ctx context.Context, agentID string, serial string) error
	// GetAgentCertSerial returns the pinned serial; "" means unpinned
	// (legacy agent whose cert pre-dates the schema migration).
	GetAgentCertSerial(ctx context.Context, agentID string) (string, error)
	// UpdateAgentCertPin persists the SPKI SHA-256 hash for an agent. Set
	// after first successful enroll; subsequent dials verify against this
	// value. Empty pin means "not yet pinned" (S-02). Returns ErrNotFound
	// if no agent with the given ID exists, matching the convention of
	// every other Update* method on this interface.
	UpdateAgentCertPin(ctx context.Context, agentID string, pin []byte) error
	// GetAgentCertPin returns the SPKI pin previously stored via
	// UpdateAgentCertPin. Returns ErrNotFound if no agent with the given
	// ID exists; returns empty bytes (no error) if the agent exists but
	// is not yet pinned.
	GetAgentCertPin(ctx context.Context, agentID string) ([]byte, error)
	// RotateAgentCert records a freshly issued agent credential (serial +
	// SPKI pin) and keeps the PREVIOUS one valid until overlapUntil.
	//
	// Issuance used to overwrite the pin outright, but the agent only learns
	// about its new certificate when the renewal response reaches it. An
	// interrupted exchange therefore left the panel expecting the new
	// credential while the agent still held the old one — and the fail-closed
	// verifier refused it. An inbound agent could ask for a new signature and
	// self-heal; a listen-mode node could not, and stayed stranded until an
	// operator issued a recovery grant (R11 / R-1).
	//
	// The overlap is bounded in time and in size: at most two credentials are
	// accepted at once, and the first connection presenting the new one closes
	// the window (CloseAgentCertOverlap). Outside that pair the verifier is
	// fail-closed exactly as before.
	//
	// presentedSerial is the serial of the certificate the agent authenticated
	// with on the connection that carried this renewal ("" when no presented
	// credential exists — enrollment, recovery). When an overlap window is
	// already OPEN and the presenter is the PREVIOUS credential, the agent has
	// just proven it never took delivery of the current one: shifting
	// prev := current would evict the only certificate the agent holds from
	// the accepted set (two consecutive lost renewal responses then strand a
	// listen-mode node — R11-1). In that case only the current credential
	// moves; prev and its overlap deadline stay untouched, so the credential
	// the agent demonstrably holds stays accepted until the original window
	// closes.
	RotateAgentCert(ctx context.Context, agentID string, serial string, spki []byte, overlapUntil time.Time, presentedSerial string) error
	// GetAgentCertPins returns the credentials the panel accepts for the agent:
	// the current one, plus the previous one while the overlap window is open.
	GetAgentCertPins(ctx context.Context, agentID string) (AgentCertPins, error)
	// CloseAgentCertOverlap drops the previous credential. Called the first
	// time an agent connects presenting the CURRENT one — proof that it has
	// taken delivery of the new certificate.
	CloseAgentCertOverlap(ctx context.Context, agentID string) error
	// UpdateAgentTransportMode changes the agent's transport_mode and
	// dial_address. dialAddress is empty when switching to inbound mode.
	// Returns ErrNotFound when the agent doesn't exist.
	UpdateAgentTransportMode(ctx context.Context, agentID, transportMode, dialAddress string) error
	PutInstance(ctx context.Context, instance InstanceRecord) error
	// PutInstancesBulk upserts a batch of Telemt instances in a single
	// transaction. Same semantics as PutInstance per row; empty slice is a
	// no-op. See P3-PERF-01a.
	PutInstancesBulk(ctx context.Context, instances []InstanceRecord) error
	// TODO(cursor): add ListInstancesCursor next sprint. Same rationale as
	// ListAgents above — deferred from S25 T1 to bound the change footprint.
	ListInstances(ctx context.Context) ([]InstanceRecord, error)
	DeleteInstancesByAgent(ctx context.Context, agentID string) error
	// GetAgentConfigTarget returns the desired config for one scope, or
	// storage.ErrNotFound when none is set.
	GetAgentConfigTarget(ctx context.Context, scopeType, scopeID string) (AgentConfigTargetRecord, error)
	// ListAgentConfigTargets returns all config targets (group + agent scopes).
	ListAgentConfigTargets(ctx context.Context) ([]AgentConfigTargetRecord, error)
	// UpsertAgentConfigTarget creates or replaces the config target for a scope.
	UpsertAgentConfigTarget(ctx context.Context, rec AgentConfigTargetRecord) error
	// DeleteAgentConfigTarget removes the config target for a scope; returns rows deleted.
	DeleteAgentConfigTarget(ctx context.Context, scopeType, scopeID string) (int64, error)
	// GetAgentUpdateStrategy returns the persisted Telemt update strategy for
	// one agent, or storage.ErrNotFound when none is set.
	GetAgentUpdateStrategy(ctx context.Context, agentID string) (AgentUpdateStrategyRecord, error)
	// UpsertAgentUpdateStrategy creates or replaces the update strategy for an
	// agent. CreatedAt is preserved across replaces of an existing row.
	UpsertAgentUpdateStrategy(ctx context.Context, rec AgentUpdateStrategyRecord) error
	// DeleteAgentUpdateStrategy removes the update strategy for an agent.
	// Idempotent: deleting an absent row is not an error.
	DeleteAgentUpdateStrategy(ctx context.Context, agentID string) error
}

FleetStore persists fleet topology and discovered Telemt runtime state.

type InstanceRecord

type InstanceRecord struct {
	ID                string
	AgentID           string
	Name              string
	Version           string
	ConfigFingerprint string
	Connections       int
	ReadOnly          bool
	UpdatedAt         time.Time
}

InstanceRecord stores one Telemt runtime observed through an agent.

type IntegrationProviderRecord

type IntegrationProviderRecord struct {
	ID        string
	Kind      string
	Label     string
	Config    []byte
	CreatedAt time.Time
	UpdatedAt time.Time
}

IntegrationProviderRecord stores credentials for an external integration backend (e.g. a Cloudflare account). A single provider can back FleetGroupIntegrationRecord rows across many groups. Config is opaque JSON — the shape is owned by the integration implementation and validated at install time.

type IntegrationStore

type IntegrationStore interface {
	CreateIntegrationProvider(ctx context.Context, provider IntegrationProviderRecord) error
	UpdateIntegrationProvider(ctx context.Context, provider IntegrationProviderRecord) error
	GetIntegrationProvider(ctx context.Context, id string) (IntegrationProviderRecord, error)
	ListIntegrationProviders(ctx context.Context) ([]IntegrationProviderRecord, error)
	ListIntegrationProvidersByKind(ctx context.Context, kind string) ([]IntegrationProviderRecord, error)
	DeleteIntegrationProvider(ctx context.Context, id string) error

	CreateFleetGroupIntegration(ctx context.Context, integration FleetGroupIntegrationRecord) error
	UpdateFleetGroupIntegration(ctx context.Context, integration FleetGroupIntegrationRecord) error
	GetFleetGroupIntegration(ctx context.Context, id string) (FleetGroupIntegrationRecord, error)
	ListFleetGroupIntegrations(ctx context.Context, fleetGroupID string) ([]FleetGroupIntegrationRecord, error)
	DeleteFleetGroupIntegration(ctx context.Context, id string) error
}

IntegrationStore persists the integration-scaffolding entities: shared provider credentials and per-fleet-group integration installs. The store itself is kind-agnostic — config is an opaque JSON blob. Validation, reconciliation, and kind-specific semantics live in the fleet/integrations registry.

type JobRecord

type JobRecord struct {
	ID             string
	Action         string
	ActorID        string
	Status         string
	CreatedAt      time.Time
	TTL            time.Duration
	IdempotencyKey string
	PayloadJSON    string
}

JobRecord stores one orchestration job.

type JobStore

type JobStore interface {
	PutJob(ctx context.Context, job JobRecord) error
	// GetJob returns a single job row by primary key, or ErrNotFound.
	// P8.1 (audit #24): backs jobs.Service.GetWithContext — terminal jobs
	// evicted from the in-memory maps are read back from here until the
	// retention worker deletes them (PruneTerminalJobs).
	GetJob(ctx context.Context, id string) (JobRecord, error)
	// ListJobs returns every job. S25 T1: capped defensively at
	// DefaultListLimit by the SQL backends so a long-lived store cannot
	// stream millions of rows even if a caller forgets to paginate. New
	// callers should use ListJobsCursor (keyset pagination) instead.
	ListJobs(ctx context.Context) ([]JobRecord, error)
	// ListJobsCursor returns one page of jobs in (created_at DESC, id DESC)
	// order. The returned next cursor is non-empty iff a further page may
	// exist; callers thread it back through params.AfterCreatedAt /
	// params.AfterID. Limit follows DefaultCursorPageSize / MaxCursorPageSize.
	ListJobsCursor(ctx context.Context, params ListJobsCursorParams) ([]JobRecord, ListJobsCursorParams, error)
	PutJobTarget(ctx context.Context, target JobTargetRecord) error
	ListJobTargets(ctx context.Context, jobID string) ([]JobTargetRecord, error)
	// ListAllJobTargets returns every job_targets row in one round-trip.
	// Used by Service.restore() to avoid the per-job N+1 SELECT pattern.
	// Empty result is fine — callers must not assume the slice is non-nil.
	ListAllJobTargets(ctx context.Context) ([]JobTargetRecord, error)
	// PruneTerminalJobs deletes jobs in succeeded/failed/expired status
	// whose created_at predates the cutoff (Q2.U-P-02). Returns the
	// number of rows deleted; the bound also cascades to job_targets via
	// ON DELETE CASCADE in the schema.
	PruneTerminalJobs(ctx context.Context, before time.Time) (int64, error)
}

JobStore persists orchestration jobs and per-target result state.

type JobTargetRecord

type JobTargetRecord struct {
	JobID      string
	AgentID    string
	Status     string
	ResultText string
	ResultJSON string
	UpdatedAt  time.Time
}

JobTargetRecord stores delivery and result state for one job target.

type ListAuditEventsCursorParams

type ListAuditEventsCursorParams struct {
	Limit          int
	AfterCreatedAt time.Time
	AfterID        string
}

ListAuditEventsCursorParams selects a single page of audit events in (created_at DESC, id DESC) order. Same cursor + limit semantics as ListJobsCursorParams.

type ListJobsCursorParams

type ListJobsCursorParams struct {
	Limit          int
	AfterCreatedAt time.Time
	AfterID        string
}

ListJobsCursorParams selects a single page of jobs in (created_at DESC, id DESC) order. AfterCreatedAt + AfterID is the keyset cursor — leave both zero for the first page. Limit <= 0 falls back to DefaultCursorPageSize and values above MaxCursorPageSize are clamped.

type LoginLockoutRecord

type LoginLockoutRecord struct {
	Username  string
	Failures  int
	LockedAt  *time.Time
	UpdatedAt time.Time
}

LoginLockoutRecord stores the persistent login-failure state for one account (S7). Failures accumulates until the lockout threshold is reached; at that point LockedAt is set to the wall-clock time the lockout began. A nil LockedAt means "not currently locked". Username is the raw account name as submitted to /auth/login so the auth service can still match it after a restart — the service normalises to lower-case before lookup.

type LoginLockoutStore

type LoginLockoutStore interface {
	UpsertLoginLockout(ctx context.Context, record LoginLockoutRecord) error
	GetLoginLockout(ctx context.Context, username string) (LoginLockoutRecord, error)
	DeleteLoginLockout(ctx context.Context, username string) error
	ListLoginLockouts(ctx context.Context) ([]LoginLockoutRecord, error)
	DeleteExpiredLoginLockouts(ctx context.Context, before time.Time) (int64, error)
}

LoginLockoutStore persists per-account login-failure state so a control-plane restart or fail-over cannot reset the lockout counter (S7). See LoginLockoutRecord.

type MetricSnapshotRecord

type MetricSnapshotRecord struct {
	ID         string
	AgentID    string
	InstanceID string
	CapturedAt time.Time
	Values     map[string]uint64
}

MetricSnapshotRecord stores one aggregated metric capture.

type MetricStore

type MetricStore interface {
	AppendMetricSnapshot(ctx context.Context, snapshot MetricSnapshotRecord) error
	// AppendMetricSnapshotsBulk inserts a batch of metric snapshots in a
	// single transaction. Empty slice is a no-op. See P3-PERF-01a.
	AppendMetricSnapshotsBulk(ctx context.Context, snapshots []MetricSnapshotRecord) error
	// TODO(cursor): add ListMetricSnapshotsCursor next sprint. The current
	// implementations cap the result at 512 rows internally so this is not
	// catastrophically unbounded, but operators viewing more than 512 most-
	// recent snapshots silently lose history. Deferred from S25 T1.
	ListMetricSnapshots(ctx context.Context) ([]MetricSnapshotRecord, error)
	// PruneMetricSnapshots deletes metric_snapshots rows with captured_at
	// strictly before the cutoff and returns the number of deleted rows.
	// Used by the retention worker (P2-REL-05).
	PruneMetricSnapshots(ctx context.Context, before time.Time) (int64, error)
}

MetricStore persists aggregated control-plane metric snapshots.

type MigrationStore

type MigrationStore interface {
	Store
	ClientStore
}

MigrationStore is the full storage surface required by the migrate-schema CLI subcommand and storagetest Transact contract tests. It composes Store with the legacy row-level client interface so migration code can iterate every table without needing to assemble per-domain Repositories. discovered_clients is copied as raw rows (no typed method), so no discovered-client interface is embedded here.

Production code MUST NOT accept or return MigrationStore — it exists only for offline migration tooling and low-level storage contract tests. Both SQLite and Postgres concrete stores satisfy this interface.

type PanelSettingsRecord

type PanelSettingsRecord struct {
	HTTPPublicURL      string
	GRPCPublicEndpoint string
	// PasswordMinLength is the operator-configured minimum password length.
	// Zero is sentinel for "not configured" — callers should treat it as
	// the compiled-in default (auth.DefaultPasswordMinLength).
	PasswordMinLength int32
	UpdatedAt         time.Time
}

PanelSettingsRecord stores operator-managed public access settings for the panel.

type PanelSettingsStore

type PanelSettingsStore interface {
	PutPanelSettings(ctx context.Context, settings PanelSettingsRecord) error
	GetPanelSettings(ctx context.Context) (PanelSettingsRecord, error)
}

PanelSettingsStore persists operator-managed panel network and TLS settings.

type ReassignCounts

type ReassignCounts struct {
	Agents            int64
	EnrollmentTokens  int64
	ClientAssignments int64
}

ReassignCounts summarises how many FK references to a fleet group exist (or were moved, depending on the method). Used by the deletion-preview endpoint and the reassignment audit entry.

type RetentionSettings

type RetentionSettings = RetentionSettingsRecord

RetentionSettings is the storage-layer alias used across the Store interface. Callers in the control-plane server wrap it with their own typed RetentionSettings struct; at the storage boundary this alias keeps the interface decoupled from server internals while reusing the same field layout (see RetentionSettingsRecord).

type RetentionSettingsRecord

type RetentionSettingsRecord struct {
	TSRawSeconds          int `json:"ts_raw_seconds"`
	TSHourlySeconds       int `json:"ts_hourly_seconds"`
	TSDCSeconds           int `json:"ts_dc_seconds"`
	IPHistorySeconds      int `json:"ip_history_seconds"`
	EventSeconds          int `json:"event_history_seconds"`
	AuditEventSeconds     int `json:"audit_event_seconds"`
	MetricSnapshotSeconds int `json:"metric_snapshot_seconds"`
	// JobsSeconds bounds how long terminal jobs (succeeded/failed/
	// expired) live in the jobs table before the rollup loop deletes
	// them via PruneTerminalJobs (Q2.U-P-02). Zero disables job
	// pruning so existing dev fixtures keep their full history.
	JobsSeconds int `json:"jobs_seconds"`
	// WebhookOutboxSeconds bounds how long terminal webhook_outbox rows
	// (delivered or dead) are kept for operator audit before the rollup
	// loop prunes them via webhooks.Storage.PruneOutbox (C4).
	WebhookOutboxSeconds int `json:"webhook_outbox_seconds"`
	// EnrollmentTokenSeconds bounds how long dead enrollment tokens
	// (consumed, revoked, or expired-unconsumed) are kept for operator
	// forensics before the rollup loop prunes them via
	// PruneEnrollmentTokens (C4).
	EnrollmentTokenSeconds int `json:"enrollment_token_seconds"`
	// ConfigApplyBatchSeconds bounds how long terminal group config-apply
	// batches (succeeded/failed/halted) and their targets live before the
	// rollup loop deletes them via PruneConfigApplyBatches (Phase A / A5).
	// Zero disables the prune, matching JobsSeconds's zero-disables
	// convention, so existing dev fixtures keep their full history.
	ConfigApplyBatchSeconds int `json:"config_apply_batch_seconds"`
}

RetentionSettingsRecord stores operator-managed timeseries/event retention windows. Persisted as an opaque JSON blob in panel_settings.retention_json so adding new retention knobs never needs another migration.

type RetentionSettingsStore

type RetentionSettingsStore interface {
	GetRetentionSettings(ctx context.Context) (RetentionSettings, error)
	PutRetentionSettings(ctx context.Context, settings RetentionSettings) error
}

RetentionSettingsStore persists operator-managed retention windows for timeseries data, runtime events, and client IP history. Returns ErrNotFound when no row has been written yet so the caller can fall back to its own defaults.

type ServerLoadHourlyRecord

type ServerLoadHourlyRecord struct {
	AgentID        string
	BucketHour     time.Time
	CPUPctAvg      float64
	CPUPctMax      float64
	MemPctAvg      float64
	MemPctMax      float64
	ConnectionsAvg float64
	ConnectionsMax int
	ActiveUsersAvg float64
	ActiveUsersMax int
	DCCoverageMin  float64
	DCCoverageAvg  float64
	SampleCount    int
}

ServerLoadHourlyRecord stores one hourly rollup of server load metrics.

type ServerLoadPointRecord

type ServerLoadPointRecord struct {
	AgentID                string
	CapturedAt             time.Time
	CPUPctAvg              float64
	CPUPctMax              float64
	MemPctAvg              float64
	MemPctMax              float64
	DiskPctAvg             float64
	DiskPctMax             float64
	Load1M                 float64
	Load5M                 float64
	Load15M                float64
	ConnectionsAvg         int
	ConnectionsMax         int
	ConnectionsMEAvg       int
	ConnectionsDirectAvg   int
	ActiveUsersAvg         int
	ActiveUsersMax         int
	ConnectionsTotal       uint64
	ConnectionsBadTotal    uint64
	HandshakeTimeoutsTotal uint64
	DCCoverageMinPct       float64
	DCCoverageAvgPct       float64
	HealthyUpstreams       int
	TotalUpstreams         int
	NetBytesSent           uint64
	NetBytesRecv           uint64
	SampleCount            int
}

ServerLoadPointRecord stores one aggregated runtime snapshot for timeseries.

type SessionRecord

type SessionRecord struct {
	ID        string
	UserID    string
	CreatedAt time.Time
	// LastSeenAt is the persisted sliding-refresh timestamp (Q2.U-S-12).
	// Updated by SessionStore.TouchSession at most every
	// sessionTouchThrottle so the idle-timeout survives a restart
	// without thrashing the store on every authenticated request.
	LastSeenAt time.Time
}

SessionRecord stores one authenticated user session.

type SessionStore

type SessionStore interface {
	PutSession(ctx context.Context, session SessionRecord) error
	GetSession(ctx context.Context, sessionID string) (SessionRecord, error)
	DeleteSession(ctx context.Context, sessionID string) error
	ListSessions(ctx context.Context) ([]SessionRecord, error)
	DeleteExpiredSessions(ctx context.Context, before time.Time) error
	// TouchSession persists a refreshed LastSeenAt so the sliding idle
	// timeout survives a control-plane restart (Q2.U-S-12). Implementations
	// must update only the last_seen_at column to avoid contention on
	// the rest of the row.
	TouchSession(ctx context.Context, sessionID string, lastSeenAt time.Time) error
}

SessionStore persists authenticated user sessions.

type Store

type Store interface {
	UserStore
	UserFleetGroupScopeStore
	UserAppearanceStore
	SessionStore
	CPSecretStore
	ConsumedTotpStore
	LoginLockoutStore
	AgentRevocationStore
	AgentFallbackStateStore
	FleetStore
	JobStore
	AuditStore
	MetricStore
	TelemetryStore
	EnrollmentStore
	AgentCertificateRecoveryGrantStore
	PanelSettingsStore
	RetentionSettingsStore
	UpdateConfigStore
	CertificateAuthorityStore
	TimeseriesStore
	IntegrationStore
	ConfigApplyBatchStore

	// Transact runs fn inside a single database transaction. The tx
	// argument is a Store implementation bound to the transaction:
	// all mutations performed through it either commit as a unit or
	// roll back together.
	//
	// Contract:
	//   - On fn returning nil, the transaction commits.
	//   - On fn returning a non-nil error, the transaction rolls back
	//     and the error is returned to the caller.
	//   - On panic inside fn, the transaction rolls back and the panic
	//     is re-raised.
	//   - Context cancellation during fn aborts the transaction.
	//   - PostgreSQL: serialization failures (SQLSTATE 40001) are
	//     retried up to 3 times automatically. Default isolation is
	//     read-committed.
	//   - SQLite: uses BEGIN IMMEDIATE so the writer lock is acquired
	//     up front. No retry loop (single-writer semantics).
	//   - TxFn MUST NOT call tx.Transact; nested calls return
	//     ErrNestedTransact immediately.
	Transact(ctx context.Context, fn TxFn) error

	// Ping verifies that the database connection is alive.
	Ping(ctx context.Context) error
	Close() error
}

Store aggregates the persistence capabilities required by the control-plane.

type TelemetryDiagnosticsCurrentRecord

type TelemetryDiagnosticsCurrentRecord struct {
	AgentID             string
	ObservedAt          time.Time
	State               string
	StateReason         string
	SystemInfoJSON      string
	EffectiveLimitsJSON string
	SecurityPostureJSON string
	MinimalAllJSON      string
	MEPoolJSON          string
	DcsJSON             string
}

TelemetryDiagnosticsCurrentRecord stores the latest slower diagnostics payloads for one node.

type TelemetryRuntimeCurrentRecord

type TelemetryRuntimeCurrentRecord struct {
	AgentID     string
	ObservedAt  time.Time
	RuntimeJSON string
}

TelemetryRuntimeCurrentRecord stores one node's latest Telemt runtime summary as a canonical JSON blob (P3-3.1, audit #3). RuntimeJSON is the whole json.Marshal(server.AgentRuntime); the storage layer treats it as an opaque string and must return it byte-for-byte. ObservedAt is stored in a separate column ONLY for ORDER BY in List and for diagnostics; on read the server overwrites the runtime's updated_at from this column (the single source of truth for the clock — see P3-3.2).

type TelemetryRuntimeDCRecord

type TelemetryRuntimeDCRecord struct {
	AgentID            string
	DC                 int
	ObservedAt         time.Time
	AvailableEndpoints int
	AvailablePct       float64
	RequiredWriters    int
	AliveWriters       int
	CoveragePct        float64
	RTTMs              float64
	Load               float64
}

TelemetryRuntimeDCRecord stores one node's latest DC health row.

type TelemetryRuntimeEventRecord

type TelemetryRuntimeEventRecord struct {
	AgentID    string
	Sequence   int64
	ObservedAt time.Time
	Timestamp  time.Time
	EventType  string
	Context    string
	Severity   string
}

TelemetryRuntimeEventRecord stores one recent runtime event observed for a node.

type TelemetryRuntimeUpstreamRecord

type TelemetryRuntimeUpstreamRecord struct {
	AgentID            string
	UpstreamID         int
	ObservedAt         time.Time
	RouteKind          string
	Address            string
	Healthy            bool
	Fails              int
	EffectiveLatencyMs float64
}

TelemetryRuntimeUpstreamRecord stores one node's latest upstream health row.

type TelemetrySecurityInventoryCurrentRecord

type TelemetrySecurityInventoryCurrentRecord struct {
	AgentID      string
	ObservedAt   time.Time
	State        string
	StateReason  string
	Enabled      bool
	EntriesTotal int
	EntriesJSON  string
}

TelemetrySecurityInventoryCurrentRecord stores the latest security inventory payload for one node.

type TelemetryStore

type TelemetryStore interface {
	PutTelemetryRuntimeCurrent(ctx context.Context, record TelemetryRuntimeCurrentRecord) error
	GetTelemetryRuntimeCurrent(ctx context.Context, agentID string) (TelemetryRuntimeCurrentRecord, error)
	ListTelemetryRuntimeCurrent(ctx context.Context) ([]TelemetryRuntimeCurrentRecord, error)
	ReplaceTelemetryRuntimeDCs(ctx context.Context, agentID string, records []TelemetryRuntimeDCRecord) error
	ListTelemetryRuntimeDCs(ctx context.Context, agentID string) ([]TelemetryRuntimeDCRecord, error)
	// ListAllTelemetryRuntimeDCs returns DC rows for every agent in one
	// query. Cold-start rehydration groups the result by agent_id in
	// memory instead of issuing one query per agent (A2).
	ListAllTelemetryRuntimeDCs(ctx context.Context) ([]TelemetryRuntimeDCRecord, error)
	ReplaceTelemetryRuntimeUpstreams(ctx context.Context, agentID string, records []TelemetryRuntimeUpstreamRecord) error
	ListTelemetryRuntimeUpstreams(ctx context.Context, agentID string) ([]TelemetryRuntimeUpstreamRecord, error)
	// ListAllTelemetryRuntimeUpstreams returns upstream rows for every
	// agent in one query (A2 cold-start rehydration).
	ListAllTelemetryRuntimeUpstreams(ctx context.Context) ([]TelemetryRuntimeUpstreamRecord, error)
	AppendTelemetryRuntimeEvents(ctx context.Context, agentID string, records []TelemetryRuntimeEventRecord) error
	ListTelemetryRuntimeEvents(ctx context.Context, agentID string, limit int) ([]TelemetryRuntimeEventRecord, error)
	// ListAllTelemetryRuntimeEventsPerAgent returns the most recent
	// perAgentLimit events PER agent (a windowed query, NOT a global
	// limit) for every agent in one round-trip (A2 cold-start
	// rehydration). When perAgentLimit <= 0 all events are returned.
	ListAllTelemetryRuntimeEventsPerAgent(ctx context.Context, perAgentLimit int) ([]TelemetryRuntimeEventRecord, error)
	PruneTelemetryRuntimeEvents(ctx context.Context, olderThan time.Time) (int64, error)
	PutTelemetryDiagnosticsCurrent(ctx context.Context, record TelemetryDiagnosticsCurrentRecord) error
	GetTelemetryDiagnosticsCurrent(ctx context.Context, agentID string) (TelemetryDiagnosticsCurrentRecord, error)
	PutTelemetrySecurityInventoryCurrent(ctx context.Context, record TelemetrySecurityInventoryCurrentRecord) error
	GetTelemetrySecurityInventoryCurrent(ctx context.Context, agentID string) (TelemetrySecurityInventoryCurrentRecord, error)

	// PutTelemetryRuntimeCurrentBulk upserts a batch of runtime JSON blobs
	// in a single transaction (P6-6.1a, finding #10). Per-row semantics match
	// PutTelemetryRuntimeCurrent; duplicate AgentIDs inside one batch
	// collapse to last-wins on BOTH backends (Postgres cannot upsert the
	// same conflict key twice in one statement, so implementations dedup
	// before inserting).
	PutTelemetryRuntimeCurrentBulk(ctx context.Context, records []TelemetryRuntimeCurrentRecord) error
	// ReplaceTelemetryRuntimeDCsBulk applies the ReplaceTelemetryRuntimeDCs
	// semantics for many agents in ONE transaction: every agent key present
	// in byAgent has its DC rows deleted and re-inserted from the mapped
	// slice (an empty slice clears the agent's rows). Agents absent from the
	// map are untouched.
	ReplaceTelemetryRuntimeDCsBulk(ctx context.Context, byAgent map[string][]TelemetryRuntimeDCRecord) error
	// ReplaceTelemetryRuntimeUpstreamsBulk: see ReplaceTelemetryRuntimeDCsBulk.
	ReplaceTelemetryRuntimeUpstreamsBulk(ctx context.Context, byAgent map[string][]TelemetryRuntimeUpstreamRecord) error
	// AppendTelemetryRuntimeEventsBulk inserts/upserts runtime events for
	// MANY agents in one transaction. Records carry their own AgentID.
	// Conflict target (agent_id, sequence) matches the single-agent
	// AppendTelemetryRuntimeEvents; duplicates within one batch collapse
	// to last-wins.
	AppendTelemetryRuntimeEventsBulk(ctx context.Context, records []TelemetryRuntimeEventRecord) error
	// PutTelemetryDiagnosticsCurrentBulk upserts a batch of diagnostics
	// snapshots in one transaction. Duplicate AgentIDs collapse last-wins.
	PutTelemetryDiagnosticsCurrentBulk(ctx context.Context, records []TelemetryDiagnosticsCurrentRecord) error
	// PutTelemetrySecurityInventoryCurrentBulk upserts a batch of security
	// inventory snapshots in one transaction. Duplicate AgentIDs collapse
	// last-wins.
	PutTelemetrySecurityInventoryCurrentBulk(ctx context.Context, records []TelemetrySecurityInventoryCurrentRecord) error
}

TelemetryStore persists current Telemt telemetry projections and recent runtime events.

type TimeseriesStore

type TimeseriesStore interface {
	AppendServerLoadPoint(ctx context.Context, record ServerLoadPointRecord) error
	// AppendServerLoadPointsBulk inserts a batch of server-load points in a
	// single transaction. Same ON-CONFLICT DO NOTHING semantics as the
	// single-row variant. Empty slice is a no-op. See P3-PERF-01a.
	AppendServerLoadPointsBulk(ctx context.Context, records []ServerLoadPointRecord) error
	ListServerLoadPoints(ctx context.Context, agentID string, from time.Time, to time.Time) ([]ServerLoadPointRecord, error)
	PruneServerLoadPoints(ctx context.Context, olderThan time.Time) (int64, error)
	AppendDCHealthPoint(ctx context.Context, record DCHealthPointRecord) error
	// AppendDCHealthPointsBulk inserts a batch of DC-health points in a
	// single transaction. Same ON-CONFLICT DO NOTHING semantics. Empty
	// slice is a no-op. See P3-PERF-01a.
	AppendDCHealthPointsBulk(ctx context.Context, records []DCHealthPointRecord) error
	ListDCHealthPoints(ctx context.Context, agentID string, from time.Time, to time.Time) ([]DCHealthPointRecord, error)
	PruneDCHealthPoints(ctx context.Context, olderThan time.Time) (int64, error)
	UpsertClientIPHistory(ctx context.Context, record ClientIPHistoryRecord) error
	// UpsertClientIPHistoryBulk upserts a batch of client-ip history rows in
	// a single transaction. Same semantics as the single-row UPSERT
	// (last_seen is updated on conflict). Empty slice is a no-op. See
	// P3-PERF-01a.
	UpsertClientIPHistoryBulk(ctx context.Context, records []ClientIPHistoryRecord) error
	ListClientIPHistory(ctx context.Context, clientID string, from time.Time, to time.Time) ([]ClientIPHistoryRecord, error)
	// AggregateClientIPHistory folds the per-(agent, ip) rows into one
	// per IP using SQL GROUP BY: MIN(first_seen) and MAX(last_seen).
	// Sorted last_seen DESC. Pushes the work into the database so the
	// CP no longer holds the full raw set in memory just to collapse
	// duplicates.
	AggregateClientIPHistory(ctx context.Context, clientID string, from time.Time, to time.Time, limit int) ([]ClientIPAggregateRecord, error)
	CountUniqueClientIPs(ctx context.Context, clientID string) (int, error)
	// CountUniqueClientIPsForClients returns the unique-IP count for
	// each client ID in a single SQL round-trip (Q2.U-P-03). The
	// returned map only carries entries for client IDs with at least
	// one history row; missing entries should be treated as zero.
	// Empty input is a no-op that returns an empty map.
	CountUniqueClientIPsForClients(ctx context.Context, clientIDs []string) (map[string]int, error)
	// ListServerLoadPointsForAgents fetches the raw load points for a
	// batch of agents in a single SQL round-trip (Q2.U-P-01). The
	// per-agent slices stay sorted by captured_at ascending to match
	// the single-agent shape. Missing agents return no key.
	ListServerLoadPointsForAgents(ctx context.Context, agentIDs []string, from time.Time, to time.Time) (map[string][]ServerLoadPointRecord, error)
	PruneClientIPHistory(ctx context.Context, olderThan time.Time) (int64, error)
	RollupServerLoadHourly(ctx context.Context, bucketHour time.Time) error
	ListServerLoadHourly(ctx context.Context, agentID string, from time.Time, to time.Time) ([]ServerLoadHourlyRecord, error)
	PruneServerLoadHourly(ctx context.Context, olderThan time.Time) (int64, error)
}

TimeseriesStore persists historical metric points for server load, DC health, and client IPs.

type TxFn

type TxFn func(tx Store) error

TxFn is the callback invoked by Store.Transact. The tx argument implements the full Store interface so that existing methods compose without duplication — see P2-ARCH-01.

NOTE: TxFn MUST NOT call tx.Transact recursively. Nested Transact calls on the same connection would deadlock (SQLite) or escalate isolation requirements unpredictably (PostgreSQL). Both backends detect the nested call and return ErrNestedTransact.

type UpdateConfigStore

type UpdateConfigStore interface {
	PutUpdateSettings(ctx context.Context, settings json.RawMessage) error
	GetUpdateSettings(ctx context.Context) (json.RawMessage, error)
	PutUpdateState(ctx context.Context, state json.RawMessage) error
	GetUpdateState(ctx context.Context) (json.RawMessage, error)
	// PutPanelSelfUpdate and GetPanelSelfUpdate persist the panel
	// self-update phase (updates.SelfUpdateState) under its own
	// update_config key, independent of the settings/state keys above.
	PutPanelSelfUpdate(ctx context.Context, raw json.RawMessage) error
	GetPanelSelfUpdate(ctx context.Context) (json.RawMessage, error)
	// PutPendingAgentUpdates and GetPendingAgentUpdates persist the
	// agent-ID -> requested-version map behind the reconcile-on-reconnect
	// of operator-requested agent self-updates, under its own
	// update_config key.
	PutPendingAgentUpdates(ctx context.Context, raw json.RawMessage) error
	GetPendingAgentUpdates(ctx context.Context) (json.RawMessage, error)
	// PutPendingTelemtUpdates and GetPendingTelemtUpdates mirror
	// PutPendingAgentUpdates/GetPendingAgentUpdates for telemt.update jobs
	// (Task 11), under their own update_config key.
	PutPendingTelemtUpdates(ctx context.Context, raw json.RawMessage) error
	GetPendingTelemtUpdates(ctx context.Context) (json.RawMessage, error)
	PutGeoIPSettings(ctx context.Context, settings json.RawMessage) error
	GetGeoIPSettings(ctx context.Context) (json.RawMessage, error)
	PutGeoIPState(ctx context.Context, state json.RawMessage) error
	GetGeoIPState(ctx context.Context) (json.RawMessage, error)
}

UpdateConfigStore persists update settings and cached version state as opaque JSON blobs.

type UserAppearanceRecord

type UserAppearanceRecord struct {
	UserID    string
	Theme     string
	Density   string
	HelpMode  string
	UpdatedAt time.Time
}

UserAppearanceRecord stores one user's persisted appearance preferences.

type UserAppearanceStore

type UserAppearanceStore interface {
	PutUserAppearance(ctx context.Context, appearance UserAppearanceRecord) error
	GetUserAppearance(ctx context.Context, userID string) (UserAppearanceRecord, error)
	ListUserAppearances(ctx context.Context) ([]UserAppearanceRecord, error)
}

UserAppearanceStore persists per-user appearance preferences.

type UserFleetGroupScopeRecord

type UserFleetGroupScopeRecord struct {
	UserID       string
	FleetGroupID string
	GrantedBy    string
	GrantedAt    time.Time
}

UserFleetGroupScopeRecord is one (user, fleet_group) scope grant with its full provenance. The runtime ListUserFleetGroupScopes returns only the fleet-group ids; this richer record exists for the offline migrate tooling so granted_by / granted_at survive the copy (audit fidelity).

type UserFleetGroupScopeStore

type UserFleetGroupScopeStore interface {
	// ListUserFleetGroupScopes returns every fleet-group id the given
	// user is scoped to. An empty slice means "global" — the user sees
	// every fleet group.
	ListUserFleetGroupScopes(ctx context.Context, userID string) ([]string, error)
	// SetUserFleetGroupScopes replaces the user's scope set with the
	// supplied list. An empty input clears the scope (back to global).
	// Caller is expected to pre-validate that every fleet group id
	// exists; the store relies on FK integrity for the runtime guarantee.
	SetUserFleetGroupScopes(ctx context.Context, userID string, fleetGroupIDs []string, grantedBy string, grantedAt time.Time) error
	// ListAllUserFleetGroupScopes returns every scope grant across all
	// users with full provenance (granted_by / granted_at). Exists for
	// the offline migrate tooling so the copy preserves audit fields the
	// per-user ListUserFleetGroupScopes drops. No runtime caller.
	ListAllUserFleetGroupScopes(ctx context.Context) ([]UserFleetGroupScopeRecord, error)
}

UserFleetGroupScopeStore persists the per-operator fleet-group scope mapping introduced by R-S-14. An empty list for a user means "global" (legacy single-tenant behaviour); admins are global regardless of stored rows.

type UserRecord

type UserRecord struct {
	ID           string
	Username     string
	PasswordHash string
	Role         string
	TotpEnabled  bool
	TotpSecret   string
	CreatedAt    time.Time
}

UserRecord stores one local control-plane account.

type UserStore

type UserStore interface {
	PutUser(ctx context.Context, user UserRecord) error
	DeleteUser(ctx context.Context, userID string) error
	GetUserByID(ctx context.Context, userID string) (UserRecord, error)
	GetUserByUsername(ctx context.Context, username string) (UserRecord, error)
	ListUsers(ctx context.Context) ([]UserRecord, error)
}

UserStore persists local control-plane user records.

Directories

Path Synopsis
Package migrateguard refuses to apply migrations that would silently destroy production data unless the operator has explicitly opted in.
Package migrateguard refuses to apply migrations that would silently destroy production data unless the operator has explicitly opted in.
Package postgres bulk insert helpers (P3-PERF-01a).
Package postgres bulk insert helpers (P3-PERF-01a).
Package sqlite bulk insert helpers (P3-PERF-01a).
Package sqlite bulk insert helpers (P3-PERF-01a).
Package sqlshared holds the pieces of the SQLite and PostgreSQL stores that are genuinely dialect-independent.
Package sqlshared holds the pieces of the SQLite and PostgreSQL stores that are genuinely dialect-independent.
internal/controlplane/storage/uow/uow.go
internal/controlplane/storage/uow/uow.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL