StateStore

Struct StateStore 

Source
pub struct StateStore { /* private fields */ }
Expand description

SQLite state store with an in-memory hot cache.

Hot path: StateStore::lookup_credential reads the cache under an RwLock read and performs ZERO SQLite work after warmup (provable via disk_reads). Cold paths (writes, quota, list) take the connection mutex; see the module docs for the threading model.

Implementations§

Source§

impl StateStore

Source

pub fn open(path: &Path) -> Result<Self, StoreError>

Open (or create) a store at path, applying pending migrations.

§Backup before migrate (DW-115)

If the database’s schema version is behind this build, a consistent snapshot is taken via VACUUM INTO to <path>.bak-<old-version>-<unix-seconds>-<millis> (millisecond component disambiguates same-second migrations) BEFORE any migration runs. Failure to create the backup ABORTS the open: no migration proceeds without a safety copy. (A brand-new, empty database and in-memory stores skip this — there is no data to lose.)

§Refusal rules

A database whose user_version is NEWER than this build supports is refused with a clear error (downgrade attempts; the migration set is forward-only — see crate::state::migrations for the rebuild path).

§Permissions

On unix the database file is tightened to mode 0600 after create: it stores credential hashes/selectors, so group/other read access granted by the process umask default (0644) is removed. Existing files are also tightened; a failure to set permissions is a hard error rather than silently shipping a world-readable secrets file.

Source

pub fn open_in_memory() -> Result<Self, StoreError>

Open an in-memory store (tests and tooling).

Source

pub fn schema_version(&self) -> Result<u32, StoreError>

Current schema version of the open database (PRAGMA user_version). After Self::open this always equals crate::state::migrations::LATEST_SCHEMA_VERSION.

Source

pub fn schema_info(&self) -> Result<SchemaInfo, StoreError>

Current-vs-latest schema info. Seam for the admin API (DW-022): current is read live, latest is what this build can migrate to.

Source

pub fn disk_reads(&self) -> u64

Number of disk (SQLite) reads performed by cache-miss lookups. Test/ops observability for the “no disk on the hot path” contract.

Source

pub fn cache_hits(&self) -> u64

Number of cache hits served without touching SQLite.

Source

pub fn upsert_consumer( &self, name: &str, priority: Option<u8>, groups: &[String], ) -> Result<ConsumerRecord, StoreError>

Insert or update a consumer by name; returns the resulting record. Updating preserves the row id and creation time; priority and groups change (#124). The consumer cache entry is refreshed.

Source

pub fn list_consumers(&self) -> Result<Vec<ConsumerRecord>, StoreError>

List all consumers (disk read; cold path).

Source

pub fn add_credential( &self, consumer_id: i64, kind: CredentialKind, hash: String, salt: Option<String>, selector: String, ) -> Result<CredentialRecord, StoreError>

Add a credential with a PRE-HASHED value. The store never sees plaintext secrets: hashing and selector derivation are the authenticator’s job (DW-019). The selector’s cache entry is invalidated so the next lookup reflects the new credential.

Source

pub fn add_credential_from_reference( &self, consumer_id: i64, kind: CredentialKind, hash: String, selector: String, source_ref: &str, ) -> Result<CredentialRecord, StoreError>

Self::add_credential for a config credential seeded from a ${...} REFERENCE (DW-045, #46): records the reference text in credentials.source_ref (schema v4) — the linkage Self::revoke_credentials_by_source uses to retire THIS row when a later seed of the same reference can no longer resolve it. The reference text is config, not secret bytes (the resolved value is hashed before it ever reaches the store).

Source

pub fn revoke_credential(&self, credential_id: i64) -> Result<bool, StoreError>

Revoke a credential by id; returns whether a row was revoked. Idempotent (revoking a revoked credential returns false). The selector’s cache entry is invalidated so the next lookup reflects the revocation.

Source

pub fn retire_credential( &self, credential_id: i64, at_epoch_secs: i64, ) -> Result<bool, StoreError>

Schedule a credential’s RETIREMENT (DW-046): the row keeps authenticating until at_epoch_secs, then stops — the dual-validity window of key rotation. at in the past or now is an immediate retirement (equivalent to revocation for lookup purposes, but recorded as a scheduled stop, not an operator revocation). Returns whether an active row was updated. Idempotent: re-retiring moves an existing future retirement EARLIER only (never later — a scheduled stop cannot be postponed through this call; issue a new credential instead).

Source

pub fn list_credentials_for_consumer( &self, consumer_name: &str, ) -> Result<Vec<CredentialRecord>, StoreError>

Every credential row of a named consumer (including revoked and retired ones), newest first — the admin rotation surface’s list view. Secret material (hash/salt/selector) never leaves this module; the admin layer projects only the safe fields.

Source

pub fn revoke_credentials_by_source( &self, consumer_id: i64, kind: CredentialKind, source_ref: &str, ) -> Result<u64, StoreError>

Revoke every ACTIVE credential of consumer_id/kind seeded from the config reference source_ref (#46): the seed path’s fail-closed skip — a re-seed whose reference no longer resolves — calls this so the row a PREVIOUS generation of the same reference seeded stops authenticating through CredentialRegistry::Store (the store-backed twin of the config-registry build, which simply does not install the credential). Uses the store’s own revocation semantics (revoked_at, per-selector cache invalidation), so a re-seed after the source heals inserts a fresh row — revocation never blocks config re-seeding. Rows WITHOUT the linkage (pre-v4 databases, inline keys, operator-managed rows) are untouched: they keep the documented upsert-only posture. Returns the number of rows revoked.

Source

pub fn rehash_credential( &self, credential_id: i64, selector: &str, hash: &str, ) -> Result<(), StoreError>

Replace a credential’s stored hash in place (#124 pepper transition): used by the authenticator after a SUCCESSFUL legacy (sha256:) verification when a pepper is configured, upgrading the row to hmac-sha256:<hex> so the transition completes lazily — no credential re-issue, and the next lookup of the same selector serves the peppered hash. The UPDATE is guarded by BOTH the row id and the selector the credential was looked up under, so a row rebound to a different selector between the lookup and this write is left alone instead of being re-hashed. The selector’s cache entry is invalidated; nothing else about the row (kind, revocation, timestamps) changes.

Source

pub fn lookup_credentials_by_selector( &self, selector: &str, ) -> Result<Vec<CredentialRecord>, StoreError>

Disk lookup: all ACTIVE credentials for a selector (revoked rows are ignored). Cold path; feeds the cache.

Source

pub fn lookup_credential( &self, selector: &str, ) -> Result<Option<Arc<Vec<Arc<CredentialRecord>>>>, StoreError>

HOT PATH: look up the active credentials for a selector. Cache hit performs ZERO SQLite work (an RwLock read and an Arc clone). Miss reads disk once and fills the cache, including a cached NEGATIVE result (empty list) for unknown selectors.

Source

pub fn lookup_consumer( &self, name: &str, ) -> Result<Option<Arc<ConsumerRecord>>, StoreError>

Consumer lookup by name. Cached on hit (same counter rules as Self::lookup_credential); unlike credentials there is no negative caching — consumer-by-name lookups are a control-plane path, so an unknown name re-reads disk.

Source

pub fn delete_legacy_config_placeholder_credentials( &self, ) -> Result<u64, StoreError>

Legacy-cleanup rule (DW-019 review): databases seeded by the pre-DW-019 build hold api_key rows whose SELECTOR is the plaintext config key and whose hash is the placeholder config:api_key:<key> — both embed the secret verbatim, and without this cleanup they would sit in the store forever (the post-DW-019 seeding writes a sha256 selector/hash and never matches them, so the idempotence check skips them). This build never writes that format, so every matching row is transitional and is deleted here. A schema migration is overkill for one build’s placeholder rows: a code-level cleanup at sync time is sufficient, runs everywhere sync runs (deployments with or without a state DB), and needs no version bump. JWT/mTLS binding rows (config:jwt: / config:mtls:) are NOT deleted — those are the current, non-secret binding-marker format. Returns the number of rows deleted (0 on a clean or fresh database).

Source

pub fn invalidate(&self, selector: &str)

Invalidate one selector’s cache entry (next lookup re-reads disk).

Source

pub fn invalidate_all(&self)

Invalidate every cache entry (next lookup of anything re-reads disk).

Source

pub fn incr_quota( &self, consumer_id: i64, counter_key: &str, window_start: i64, amount: u64, limit: Option<u64>, ) -> Result<u64, StoreError>

Atomically add amount to a quota counter, refusing the increment (writing nothing) when it would exceed limit. Returns the used-after value. Window semantics are caller-driven: a new window_start is a NEW counter (rollover = call with the next window’s start).

Source

pub fn get_quota( &self, consumer_id: i64, counter_key: &str, window_start: i64, ) -> Result<u64, StoreError>

Read the current usage of a quota counter (0 when absent).

Source

pub fn set_prompt_override( &self, prompt_name: &str, version: &str, ) -> Result<(), StoreError>

Set (or replace) the active-version override for a prompt (DW-086). Idempotent: re-setting the same prompt moves the override to the new version. The caller validates that the prompt name and version exist in the CURRENT config before calling (the store does not know the config).

Source

pub fn clear_prompt_override(&self, prompt_name: &str) -> Result<(), StoreError>

Clear the active-version override for a prompt (DW-086): revert to the config-declared active version. Idempotent (clearing a prompt with no override is a no-op).

Source

pub fn get_prompt_override( &self, prompt_name: &str, ) -> Result<Option<String>, StoreError>

Read the active-version override for a prompt (DW-086): Some(version) when an override exists, None when the config-declared active version governs.

Source

pub fn list_prompt_overrides(&self) -> Result<Vec<(String, String)>, StoreError>

Every prompt override row (prompt_name, version), for the admin surface’s list view and for the compile-time override merge (DW-086).

Source

pub fn create_mcp_session( &self, session_id: &str, consumer: &str, ttl_secs: u64, client_info: Option<&str>, ) -> Result<(), StoreError>

Create a new MCP agent session (DW-087). Inserts a row with the given session id, consumer, TTL, and optional client info JSON. created_at, last_used_at, and expires_at are Unix-epoch SECONDS.

Source

pub fn touch_mcp_session(&self, session_id: &str) -> Result<bool, StoreError>

Touch an MCP session: update last_used_at to now. Returns true if the session exists and is not expired, false otherwise. An expired session is deleted on touch.

Source

pub fn delete_mcp_session(&self, session_id: &str) -> Result<(), StoreError>

Delete an MCP session (DW-087 shutdown). Returns Ok regardless of whether a row was deleted.

Source

pub fn count_active_mcp_sessions(&self) -> Result<usize, StoreError>

Count active (non-expired) MCP sessions (DW-087).

Source

pub fn cleanup_expired_mcp_sessions(&self) -> Result<usize, StoreError>

Delete expired MCP sessions (DW-087). Returns the number of rows deleted.

Source

pub fn list_active_mcp_sessions( &self, ) -> Result<Vec<(String, String, i64, i64, i64, Option<String>)>, StoreError>

List active MCP sessions (DW-087): the admin surface’s GET /mcp/sessions view. Returns (session_id, consumer, created_at, last_used_at, expires_at, client_info) for non-expired sessions, ordered by created_at descending.

Source

pub fn upsert_workspace( &self, name: &str, description: &str, active: bool, ) -> Result<(), StoreError>

Insert or replace a workspace row. The default workspace is seeded by the manager on first open; this method is the manager’s persistence path for create/update.

Source

pub fn delete_workspace_row(&self, name: &str) -> Result<bool, StoreError>

Delete a workspace row. Returns true if a row was deleted.

Source

pub fn list_workspace_rows(&self) -> Result<Vec<WorkspaceRow>, StoreError>

List all workspace rows.

Source

pub fn get_workspace_row( &self, name: &str, ) -> Result<Option<WorkspaceRow>, StoreError>

Get a single workspace row by name.

Source

pub fn upsert_role( &self, name: &str, permissions_json: &str, ) -> Result<(), StoreError>

Insert or replace an RBAC role row. The permissions are stored as a JSON array of {"action":"...","workspace":"..."}.

Source

pub fn delete_role_row(&self, name: &str) -> Result<bool, StoreError>

Delete an RBAC role row. Returns true if a row was deleted.

Source

pub fn list_role_rows(&self) -> Result<Vec<RoleRow>, StoreError>

List all RBAC role rows.

Source

pub fn upsert_principal( &self, identity: &str, roles_json: &str, ) -> Result<(), StoreError>

Insert or replace a principal row. The roles are stored as a JSON array of role-name strings.

Source

pub fn get_principal_row( &self, identity: &str, ) -> Result<Option<PrincipalRow>, StoreError>

Get a single principal row by identity.

Source

pub fn list_principal_rows(&self) -> Result<Vec<PrincipalRow>, StoreError>

List all principal rows.

Source

pub fn append_workspace_audit( &self, timestamp_ms: i64, principal: &str, action: &str, workspace: &str, before: Option<&str>, after: Option<&str>, request_id: &str, ) -> Result<i64, StoreError>

Append an audit entry. Returns the assigned sequence number.

Source

pub fn query_workspace_audit( &self, workspace: Option<&str>, since_ms: Option<i64>, until_ms: Option<i64>, limit: i64, ) -> Result<Vec<AuditRow>, StoreError>

Query audit entries, optionally filtered by workspace and bounded by a time range. Returns entries ordered by seq descending (newest first), capped at limit.

Source

pub fn try_acquire_leader( &self, instance_id: &str, ttl_ms: i64, ) -> Result<LeaderRow, StoreError>

Try to acquire leadership. Returns Ok(LeaderRow) if this instance now holds the lease; returns Err(String) with “instance_id:expires_at_ms” of the current holder if another instance holds a valid (non-expired) lease.

Source

pub fn renew_leader_lease( &self, instance_id: &str, ttl_ms: i64, ) -> Result<LeaderRow, StoreError>

Renew the leader lease. Returns Ok(LeaderRow) with the new expiry if this instance still holds the lease; Err if the lease was lost (expired or stolen).

Source

pub fn step_down_leader(&self, instance_id: &str) -> Result<(), StoreError>

Voluntarily release the lease (graceful shutdown). Returns Ok(()) if the lease was released, Err if this instance did not hold the lease.

Source

pub fn current_leader(&self) -> Result<Option<LeaderRow>, StoreError>

Get the current leader (if any), or None if no valid (non-expired) lease exists.

Source

pub fn save_generation_counter(&self, generation: u64) -> Result<(), StoreError>

Persist the current config generation counter (SCALE-06, #185). This survives controller restarts so the generation counter does not reset to 1.

Source

pub fn load_generation_counter(&self) -> Result<Option<u64>, StoreError>

Load the persisted config generation counter (SCALE-06, #185). Returns None if no counter has been persisted (fresh store).

Source

pub fn append_event_wal( &self, id: &str, kind: &str, gateway: &str, timestamp_ms: u64, payload_json: &str, ) -> Result<(), StoreError>

Append a critical event to the WAL before emission. The event is persisted as un-acked (acked = 0); the deliverer marks it acked after dispatch. payload_json is the JSON-serialized EventPayload.

Source

pub fn ack_event_wal(&self, id: &str) -> Result<usize, StoreError>

Mark an event as acked (delivered). Called by the webhook deliverer after the event has been dispatched to matching targets. Returns the number of rows updated (0 if the event was already acked or doesn’t exist).

Source

pub fn unacked_events_wal(&self) -> Result<Vec<EventWalRow>, StoreError>

Load all un-acked events from the WAL, ordered by timestamp (oldest first). Called on startup to replay events that were not delivered before the previous process exited.

Source

pub fn purge_acked_events_wal( &self, older_than_ms: u64, ) -> Result<usize, StoreError>

Purge acked events older than the given timestamp. Called periodically to keep the WAL from growing unbounded. Returns the number of rows purged.

Trait Implementations§

Source§

impl Debug for StateStore

Manual Debug: the connection and cache have no meaningful debug output; tests use it only for unwrap_err on open (error-side Debug).

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<T> AnyExt for T
where T: Any + ?Sized,

§

fn downcast_ref<T>(this: &Self) -> Option<&T>
where T: Any,

Attempts to downcast this to T behind reference
§

fn downcast_mut<T>(this: &mut Self) -> Option<&mut T>
where T: Any,

Attempts to downcast this to T behind mutable reference
§

fn downcast_rc<T>(this: Rc<Self>) -> Result<Rc<T>, Rc<Self>>
where T: Any,

Attempts to downcast this to T behind Rc pointer
§

fn downcast_arc<T>(this: Arc<Self>) -> Result<Arc<T>, Arc<Self>>
where T: Any,

Attempts to downcast this to T behind Arc pointer
§

fn downcast_box<T>(this: Box<Self>) -> Result<Box<T>, Box<Self>>
where T: Any,

Attempts to downcast this to T behind Box pointer
§

fn downcast_move<T>(this: Self) -> Option<T>
where T: Any, Self: Sized,

Attempts to downcast owned Self to T, useful only in generic context as a workaround for specialization
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T, X> CoerceTo<T> for X
where T: CoerceFrom<X> + ?Sized,

§

fn coerce_rc_to(self: Rc<X>) -> Rc<T>

§

fn coerce_box_to(self: Box<X>) -> Box<T>

§

fn coerce_ref_to(&self) -> &T

§

fn coerce_mut_to(&mut self) -> &mut T

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
§

impl<T> ServiceExt for T

§

fn map_response_body<F>(self, f: F) -> MapResponseBody<Self, F>
where Self: Sized,

Apply a transformation to the response body. Read more
§

fn trace_for_http(self) -> Trace<Self, SharedClassifier<ServerErrorsAsFailures>>
where Self: Sized,

High level tracing that classifies responses using HTTP status codes. Read more
§

fn trace_for_grpc(self) -> Trace<Self, SharedClassifier<GrpcErrorsAsFailures>>
where Self: Sized,

High level tracing that classifies responses using gRPC headers. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,