Skip to main content

tako_rs_plugins/
stores.rs

1//! Pluggable backend traits for stateful middleware.
2//!
3//! Built-in middleware (sessions, rate limiting, idempotency, JWKS, CSRF) all
4//! ship with an in-memory `scc::HashMap` store. Production deployments often
5//! want to swap that out for Redis, Postgres, or another shared backend so a
6//! cluster of replicas can share state. The traits here define the minimum
7//! surface needed by each middleware.
8//!
9//! Concrete `Memory*` implementations live in submodules under this module.
10//! Crates that want to provide a Redis or Postgres backend can implement the
11//! traits in their own crate and pass the resulting type into the matching
12//! middleware builder.
13//!
14//! # TODO — Redis / Postgres backend crates (tracked for v2.0)
15//!
16//! Companion crates `tako-stores-redis` and `tako-stores-postgres` are
17//! planned but **not yet shipped**. Until they land, multi-replica
18//! deployments must implement these traits themselves (or accept the
19//! per-process state silos of the in-memory defaults). See `V2_ROADMAP.md`
20//! § 4.1 for the linked follow-up checklist — do not let this slip.
21
22use std::time::Duration;
23
24use async_trait::async_trait;
25
26pub mod memory;
27
28/// Persistent session storage.
29///
30/// Implementations must be safe to clone cheaply — sessions are accessed on
31/// every request, so the trait is invoked from inside hot middleware paths.
32#[async_trait]
33pub trait SessionStore: Send + Sync + 'static {
34  /// Reads a session blob keyed by `id`. Returns `None` if the session does
35  /// not exist or has expired.
36  async fn load(&self, id: &str) -> Option<Vec<u8>>;
37
38  /// Inserts or replaces the session blob for `id` with the configured TTL.
39  async fn store(&self, id: &str, data: Vec<u8>, ttl: Duration);
40
41  /// Removes the session, returning whether the key existed.
42  async fn remove(&self, id: &str) -> bool;
43
44  /// Optional sweep hook. The default in-memory store schedules its own
45  /// janitor; remote backends typically rely on TTL expiry inside the
46  /// underlying database (e.g. Redis `EXPIRE`).
47  async fn sweep(&self) {}
48}
49
50/// Token-bucket / GCRA rate-limit storage.
51///
52/// `consume` atomically reduces the bucket for `key` by one request and
53/// returns the post-consumption snapshot. Implementations are responsible for
54/// refilling the bucket — token-bucket tickers run on a per-store schedule,
55/// GCRA computes the new state on read.
56#[async_trait]
57pub trait RateLimitStore: Send + Sync + 'static {
58  /// Atomically attempts to take one permit from `key`'s bucket. Returns
59  /// `Ok(snapshot)` when the request is allowed, `Err(snapshot)` when the
60  /// caller exceeded the limit. The returned snapshot is what the caller
61  /// emits in the `RateLimit-*` response headers.
62  async fn consume(&self, key: &str, cost: u32) -> Result<RateLimitSnapshot, RateLimitSnapshot>;
63}
64
65/// Public snapshot of a rate-limit decision suitable for response headers.
66#[derive(Debug, Clone)]
67pub struct RateLimitSnapshot {
68  /// Configured maximum (`RateLimit-Limit` value).
69  pub limit: u32,
70  /// Remaining quota after the current request, never below zero.
71  pub remaining: u32,
72  /// Seconds until the next refill arrives (`RateLimit-Reset`).
73  pub reset_secs: u64,
74  /// Suggested `Retry-After` (only meaningful when the request was rejected).
75  pub retry_after_secs: u64,
76}
77
78/// Idempotency-key cache.
79#[async_trait]
80pub trait IdempotencyStore: Send + Sync + 'static {
81  /// Reads an existing entry for `key`.
82  async fn get(&self, key: &str) -> Option<IdempotencyEntry>;
83
84  /// Marks `key` as in-flight; returns the freshly inserted record, or the
85  /// existing one if another request arrived first.
86  async fn begin(&self, key: &str, payload_sig: [u8; 20]) -> IdempotencyEntry;
87
88  /// Persists a completed entry with the configured TTL.
89  async fn complete(&self, key: &str, entry: IdempotencyEntry, ttl: Duration);
90
91  /// Removes the entry — typically invoked when the handler decided not to
92  /// cache the result (e.g. opt-out via response header).
93  async fn remove(&self, key: &str);
94}
95
96/// Idempotency cache record. The body / headers are stored as opaque bytes so
97/// remote backends don't need to understand HTTP serialization.
98#[derive(Debug, Clone)]
99pub struct IdempotencyEntry {
100  pub status: u16,
101  pub headers: Vec<(String, Vec<u8>)>,
102  pub body: Vec<u8>,
103  pub payload_sig: [u8; 20],
104  pub completed: bool,
105}
106
107/// JSON Web Key Set provider.
108///
109/// `keys_for(kid)` returns the candidate verification keys for a given key
110/// id. JWKS rotation is implementation-specific: the in-memory provider
111/// caches a fixed snapshot, while remote providers typically fetch from a
112/// well-known URL with their own background refresh cadence.
113#[async_trait]
114pub trait JwksProvider: Send + Sync + 'static {
115  /// Returns matching key bytes for `kid`. Multiple matches are allowed
116  /// (handlers verify against each in order); `None` means "no rotation
117  /// match — fall back to the configured default key, if any".
118  async fn keys_for(&self, kid: &str) -> Vec<Vec<u8>>;
119}
120
121/// CSRF token storage. Used by token-store CSRF middleware (as opposed to the
122/// stateless double-submit-cookie variant).
123#[async_trait]
124pub trait CsrfTokenStore: Send + Sync + 'static {
125  /// Issues a token bound to the given session id with the configured TTL.
126  async fn issue(&self, session_id: &str, ttl: Duration) -> String;
127
128  /// Validates a candidate token against the session id, consuming it on
129  /// success when `single_use` is true.
130  async fn validate(&self, session_id: &str, token: &str, single_use: bool) -> bool;
131}