Skip to main content

stateset_http/
state.rs

1//! Application state shared across all route handlers.
2
3use std::{
4    collections::{HashMap, HashSet},
5    fmt,
6    net::IpAddr,
7    path::{Path, PathBuf},
8    str::FromStr,
9    sync::{
10        Arc, RwLock,
11        atomic::{AtomicU64, Ordering},
12    },
13};
14
15use axum::http::HeaderMap;
16use stateset_embedded::Commerce;
17
18use crate::{
19    error::HttpError,
20    events_replay::{DEFAULT_REPLAY_CAPACITY, EventReplayBuffer, EventReplayRegistry},
21    middleware::X_TENANT_ID,
22};
23
24/// Default cap for lazily created per-tenant databases.
25pub(crate) const DEFAULT_MAX_TENANT_DBS: usize = 256;
26
27#[derive(Debug)]
28struct TenantCacheEntry {
29    commerce: Arc<Commerce>,
30    last_access_tick: u64,
31}
32
33/// Snapshot of per-tenant cache behavior used by health endpoints.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct TenantCacheMetrics {
36    /// Whether per-tenant routing is enabled.
37    pub enabled: bool,
38    /// Maximum active tenant databases kept in the cache.
39    pub max_cached_dbs: usize,
40    /// Number of tenant databases currently cached.
41    pub cached_dbs: usize,
42    /// Number of cached tenant databases currently in use by requests.
43    pub in_use_cached_dbs: usize,
44    /// Number of cache hits for tenant resolution.
45    pub hits: u64,
46    /// Number of cache misses for tenant resolution.
47    pub misses: u64,
48    /// Number of idle tenant cache entries evicted due to capacity.
49    pub evictions: u64,
50    /// Number of rejections when all cache entries were in use at capacity.
51    pub rejections: u64,
52}
53
54/// Snapshot of `/metrics` access outcomes for observability.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) struct MetricsAccessMetrics {
57    pub(crate) requests_total: u64,
58    pub(crate) allowed_total: u64,
59    pub(crate) allowed_peer_total: u64,
60    pub(crate) allowed_forwarded_trusted_proxy_total: u64,
61    pub(crate) allowed_forwarded_without_peer_total: u64,
62    pub(crate) allowed_unavailable_total: u64,
63    pub(crate) denied_ip_total: u64,
64    pub(crate) denied_ip_not_allowed_total: u64,
65    pub(crate) denied_missing_peer_ip_with_trusted_proxies_total: u64,
66    pub(crate) denied_auth_total: u64,
67    pub(crate) denied_auth_header_missing_total: u64,
68    pub(crate) denied_auth_header_invalid_total: u64,
69    pub(crate) denied_auth_header_invalid_encoding_total: u64,
70    pub(crate) denied_auth_header_invalid_scheme_total: u64,
71    pub(crate) denied_auth_header_malformed_total: u64,
72    pub(crate) denied_auth_header_multiple_total: u64,
73    pub(crate) denied_auth_header_oversized_total: u64,
74    pub(crate) denied_auth_token_mismatch_total: u64,
75    pub(crate) denied_forwarded_missing_total: u64,
76    pub(crate) denied_forwarded_invalid_total: u64,
77    pub(crate) denied_forwarded_oversized_total: u64,
78}
79
80/// Maximum accepted byte lengths for metrics-related forwarding headers.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct MetricsHeaderLimits {
83    forwarded_header_value_bytes: usize,
84    x_forwarded_for_header_value_bytes: usize,
85    x_real_ip_header_value_bytes: usize,
86    authorization_header_value_bytes: usize,
87}
88
89impl MetricsHeaderLimits {
90    /// Default max byte length for the standard `Forwarded` header.
91    pub const DEFAULT_FORWARDED_HEADER_VALUE_BYTES: usize = 2048;
92    /// Default max byte length for `X-Forwarded-For`.
93    pub const DEFAULT_X_FORWARDED_FOR_HEADER_VALUE_BYTES: usize = 2048;
94    /// Default max byte length for `X-Real-IP`.
95    pub const DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES: usize = 512;
96    /// Default max byte length for `Authorization`.
97    pub const DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES: usize = 2048;
98
99    /// Create a custom header limit set. All values must be greater than zero.
100    pub fn new(
101        forwarded_header_value_bytes: usize,
102        x_forwarded_for_header_value_bytes: usize,
103        x_real_ip_header_value_bytes: usize,
104    ) -> Result<Self, HttpError> {
105        Self::new_with_authorization(
106            forwarded_header_value_bytes,
107            x_forwarded_for_header_value_bytes,
108            x_real_ip_header_value_bytes,
109            Self::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES,
110        )
111    }
112
113    /// Create a custom header limit set including Authorization.
114    ///
115    /// All values must be greater than zero.
116    pub fn new_with_authorization(
117        forwarded_header_value_bytes: usize,
118        x_forwarded_for_header_value_bytes: usize,
119        x_real_ip_header_value_bytes: usize,
120        authorization_header_value_bytes: usize,
121    ) -> Result<Self, HttpError> {
122        if forwarded_header_value_bytes == 0 {
123            return Err(HttpError::BadRequest(
124                "metrics forwarded header limit must be greater than zero".to_string(),
125            ));
126        }
127        if x_forwarded_for_header_value_bytes == 0 {
128            return Err(HttpError::BadRequest(
129                "metrics x-forwarded-for header limit must be greater than zero".to_string(),
130            ));
131        }
132        if x_real_ip_header_value_bytes == 0 {
133            return Err(HttpError::BadRequest(
134                "metrics x-real-ip header limit must be greater than zero".to_string(),
135            ));
136        }
137        if authorization_header_value_bytes == 0 {
138            return Err(HttpError::BadRequest(
139                "metrics authorization header limit must be greater than zero".to_string(),
140            ));
141        }
142
143        Ok(Self {
144            forwarded_header_value_bytes,
145            x_forwarded_for_header_value_bytes,
146            x_real_ip_header_value_bytes,
147            authorization_header_value_bytes,
148        })
149    }
150
151    /// Max byte length for `Forwarded`.
152    #[must_use]
153    pub const fn forwarded_header_value_bytes(self) -> usize {
154        self.forwarded_header_value_bytes
155    }
156
157    /// Max byte length for `X-Forwarded-For`.
158    #[must_use]
159    pub const fn x_forwarded_for_header_value_bytes(self) -> usize {
160        self.x_forwarded_for_header_value_bytes
161    }
162
163    /// Max byte length for `X-Real-IP`.
164    #[must_use]
165    pub const fn x_real_ip_header_value_bytes(self) -> usize {
166        self.x_real_ip_header_value_bytes
167    }
168
169    /// Max byte length for `Authorization`.
170    #[must_use]
171    pub const fn authorization_header_value_bytes(self) -> usize {
172        self.authorization_header_value_bytes
173    }
174}
175
176impl Default for MetricsHeaderLimits {
177    fn default() -> Self {
178        Self {
179            forwarded_header_value_bytes: Self::DEFAULT_FORWARDED_HEADER_VALUE_BYTES,
180            x_forwarded_for_header_value_bytes: Self::DEFAULT_X_FORWARDED_FOR_HEADER_VALUE_BYTES,
181            x_real_ip_header_value_bytes: Self::DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES,
182            authorization_header_value_bytes: Self::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES,
183        }
184    }
185}
186
187/// CIDR block used for trusted proxy checks.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
189pub struct IpCidr {
190    network: IpAddr,
191    prefix_len: u8,
192}
193
194impl IpCidr {
195    /// Creates a CIDR network from an IP and prefix length.
196    pub fn new(network: IpAddr, prefix_len: u8) -> Result<Self, HttpError> {
197        let max_prefix = match network {
198            IpAddr::V4(_) => 32,
199            IpAddr::V6(_) => 128,
200        };
201        if prefix_len > max_prefix {
202            return Err(HttpError::BadRequest(format!(
203                "invalid CIDR prefix length {prefix_len} for {network}"
204            )));
205        }
206        Ok(Self { network, prefix_len })
207    }
208
209    /// Returns true if `ip` is contained in this CIDR range.
210    #[must_use]
211    pub fn contains(self, ip: IpAddr) -> bool {
212        match (self.network, ip) {
213            (IpAddr::V4(network), IpAddr::V4(ip)) => {
214                let mask = if self.prefix_len == 0 {
215                    0
216                } else {
217                    u32::MAX << (32 - u32::from(self.prefix_len))
218                };
219                (u32::from(network) & mask) == (u32::from(ip) & mask)
220            }
221            (IpAddr::V6(network), IpAddr::V6(ip)) => {
222                let mask = if self.prefix_len == 0 {
223                    0
224                } else {
225                    u128::MAX << (128 - u32::from(self.prefix_len))
226                };
227                (u128::from(network) & mask) == (u128::from(ip) & mask)
228            }
229            _ => false,
230        }
231    }
232
233    /// Returns the CIDR prefix length.
234    #[must_use]
235    pub const fn prefix_len(self) -> u8 {
236        self.prefix_len
237    }
238
239    /// Returns the CIDR network base IP.
240    #[must_use]
241    pub const fn network(self) -> IpAddr {
242        self.network
243    }
244}
245
246impl fmt::Display for IpCidr {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        write!(f, "{}/{}", self.network, self.prefix_len)
249    }
250}
251
252impl FromStr for IpCidr {
253    type Err = HttpError;
254
255    fn from_str(value: &str) -> Result<Self, Self::Err> {
256        let value = value.trim();
257        if value.is_empty() {
258            return Err(HttpError::BadRequest("CIDR cannot be empty".to_string()));
259        }
260
261        if let Some((ip, prefix)) = value.split_once('/') {
262            let network = ip.trim().parse::<IpAddr>().map_err(|error| {
263                HttpError::BadRequest(format!("invalid CIDR address '{}': {error}", ip.trim()))
264            })?;
265            let prefix_len = prefix.trim().parse::<u8>().map_err(|error| {
266                HttpError::BadRequest(format!(
267                    "invalid CIDR prefix '{}' in '{}': {error}",
268                    prefix.trim(),
269                    value
270                ))
271            })?;
272            Self::new(network, prefix_len)
273        } else {
274            let ip = value.parse::<IpAddr>().map_err(|error| {
275                HttpError::BadRequest(format!("invalid IP address '{value}': {error}"))
276            })?;
277            let prefix = match ip {
278                IpAddr::V4(_) => 32,
279                IpAddr::V6(_) => 128,
280            };
281            Self::new(ip, prefix)
282        }
283    }
284}
285
286/// Shared application state injected into every route handler via
287/// [`axum::extract::State`].
288#[derive(Clone)]
289pub struct AppState {
290    commerce: Arc<Commerce>,
291    tenant_db_dir: Option<Arc<PathBuf>>,
292    ignore_tenant_header: bool,
293    tenant_cache: Arc<RwLock<HashMap<String, TenantCacheEntry>>>,
294    tenant_access_clock: Arc<AtomicU64>,
295    tenant_cache_hits: Arc<AtomicU64>,
296    tenant_cache_misses: Arc<AtomicU64>,
297    tenant_cache_evictions: Arc<AtomicU64>,
298    tenant_cache_rejections: Arc<AtomicU64>,
299    metrics_scrape_requests: Arc<AtomicU64>,
300    metrics_scrape_allowed: Arc<AtomicU64>,
301    metrics_scrape_allowed_peer: Arc<AtomicU64>,
302    metrics_scrape_allowed_forwarded_trusted_proxy: Arc<AtomicU64>,
303    metrics_scrape_allowed_forwarded_without_peer: Arc<AtomicU64>,
304    metrics_scrape_allowed_unavailable: Arc<AtomicU64>,
305    metrics_scrape_denied_ip: Arc<AtomicU64>,
306    metrics_scrape_denied_ip_not_allowed: Arc<AtomicU64>,
307    metrics_scrape_denied_missing_peer_ip_with_trusted_proxies: Arc<AtomicU64>,
308    metrics_scrape_denied_auth: Arc<AtomicU64>,
309    metrics_scrape_denied_auth_header_missing: Arc<AtomicU64>,
310    metrics_scrape_denied_auth_header_invalid: Arc<AtomicU64>,
311    metrics_scrape_denied_auth_header_invalid_encoding: Arc<AtomicU64>,
312    metrics_scrape_denied_auth_header_invalid_scheme: Arc<AtomicU64>,
313    metrics_scrape_denied_auth_header_malformed: Arc<AtomicU64>,
314    metrics_scrape_denied_auth_header_multiple: Arc<AtomicU64>,
315    metrics_scrape_denied_auth_header_oversized: Arc<AtomicU64>,
316    metrics_scrape_denied_auth_token_mismatch: Arc<AtomicU64>,
317    metrics_scrape_denied_forwarded_missing: Arc<AtomicU64>,
318    metrics_scrape_denied_forwarded_invalid: Arc<AtomicU64>,
319    metrics_scrape_denied_forwarded_oversized: Arc<AtomicU64>,
320    metrics_bearer_token: Option<Arc<str>>,
321    metrics_ip_allowlist: Option<Arc<HashSet<IpAddr>>>,
322    metrics_ip_cidr_allowlist: Option<Arc<Vec<IpCidr>>>,
323    metrics_trusted_proxies: Option<Arc<Vec<IpCidr>>>,
324    metrics_header_limits: MetricsHeaderLimits,
325    max_tenant_dbs: usize,
326    event_replay: EventReplayRegistry,
327}
328
329impl fmt::Debug for AppState {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        f.debug_struct("AppState")
332            .field("commerce", &"Commerce { .. }")
333            .field(
334                "tenant_db_dir",
335                &self.tenant_db_dir.as_deref().map(|path| path.display().to_string()),
336            )
337            .field("ignore_tenant_header", &self.ignore_tenant_header)
338            .field(
339                "metrics_bearer_token",
340                &self.metrics_bearer_token.as_ref().map(|_| "<redacted>"),
341            )
342            .field(
343                "metrics_ip_allowlist_size",
344                &self.metrics_ip_allowlist.as_ref().map(|allowlist| allowlist.len()),
345            )
346            .field(
347                "metrics_ip_cidr_allowlist_size",
348                &self.metrics_ip_cidr_allowlist.as_ref().map(|allowlist| allowlist.len()),
349            )
350            .field(
351                "metrics_trusted_proxies_size",
352                &self.metrics_trusted_proxies.as_ref().map(|proxies| proxies.len()),
353            )
354            .field("metrics_scrape_requests", &self.metrics_scrape_requests.load(Ordering::Relaxed))
355            .field("metrics_scrape_allowed", &self.metrics_scrape_allowed.load(Ordering::Relaxed))
356            .field(
357                "metrics_scrape_allowed_peer",
358                &self.metrics_scrape_allowed_peer.load(Ordering::Relaxed),
359            )
360            .field(
361                "metrics_scrape_allowed_forwarded_trusted_proxy",
362                &self.metrics_scrape_allowed_forwarded_trusted_proxy.load(Ordering::Relaxed),
363            )
364            .field(
365                "metrics_scrape_allowed_forwarded_without_peer",
366                &self.metrics_scrape_allowed_forwarded_without_peer.load(Ordering::Relaxed),
367            )
368            .field(
369                "metrics_scrape_allowed_unavailable",
370                &self.metrics_scrape_allowed_unavailable.load(Ordering::Relaxed),
371            )
372            .field(
373                "metrics_scrape_denied_ip",
374                &self.metrics_scrape_denied_ip.load(Ordering::Relaxed),
375            )
376            .field(
377                "metrics_scrape_denied_ip_not_allowed",
378                &self.metrics_scrape_denied_ip_not_allowed.load(Ordering::Relaxed),
379            )
380            .field(
381                "metrics_scrape_denied_missing_peer_ip_with_trusted_proxies",
382                &self
383                    .metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
384                    .load(Ordering::Relaxed),
385            )
386            .field(
387                "metrics_scrape_denied_auth",
388                &self.metrics_scrape_denied_auth.load(Ordering::Relaxed),
389            )
390            .field(
391                "metrics_scrape_denied_auth_header_missing",
392                &self.metrics_scrape_denied_auth_header_missing.load(Ordering::Relaxed),
393            )
394            .field(
395                "metrics_scrape_denied_auth_header_invalid",
396                &self.metrics_scrape_denied_auth_header_invalid.load(Ordering::Relaxed),
397            )
398            .field(
399                "metrics_scrape_denied_auth_header_invalid_encoding",
400                &self.metrics_scrape_denied_auth_header_invalid_encoding.load(Ordering::Relaxed),
401            )
402            .field(
403                "metrics_scrape_denied_auth_header_invalid_scheme",
404                &self.metrics_scrape_denied_auth_header_invalid_scheme.load(Ordering::Relaxed),
405            )
406            .field(
407                "metrics_scrape_denied_auth_header_malformed",
408                &self.metrics_scrape_denied_auth_header_malformed.load(Ordering::Relaxed),
409            )
410            .field(
411                "metrics_scrape_denied_auth_header_multiple",
412                &self.metrics_scrape_denied_auth_header_multiple.load(Ordering::Relaxed),
413            )
414            .field(
415                "metrics_scrape_denied_auth_header_oversized",
416                &self.metrics_scrape_denied_auth_header_oversized.load(Ordering::Relaxed),
417            )
418            .field(
419                "metrics_scrape_denied_auth_token_mismatch",
420                &self.metrics_scrape_denied_auth_token_mismatch.load(Ordering::Relaxed),
421            )
422            .field(
423                "metrics_scrape_denied_forwarded_missing",
424                &self.metrics_scrape_denied_forwarded_missing.load(Ordering::Relaxed),
425            )
426            .field(
427                "metrics_scrape_denied_forwarded_invalid",
428                &self.metrics_scrape_denied_forwarded_invalid.load(Ordering::Relaxed),
429            )
430            .field(
431                "metrics_scrape_denied_forwarded_oversized",
432                &self.metrics_scrape_denied_forwarded_oversized.load(Ordering::Relaxed),
433            )
434            .field("metrics_header_limits", &self.metrics_header_limits)
435            .field("max_tenant_dbs", &self.max_tenant_dbs)
436            .field("event_replay", &self.event_replay)
437            .finish()
438    }
439}
440
441fn is_valid_tenant_id(value: &str) -> bool {
442    let trimmed = value.trim();
443    if trimmed.is_empty() || trimmed.len() > 64 {
444        return false;
445    }
446    // Reject anything with path semantics outright: `.` / `..` components,
447    // separators, and dots in general (a tenant id is an identifier, not a
448    // file name fragment).
449    trimmed.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
450}
451
452/// Parse `x-tenant-id` from request headers.
453#[must_use]
454pub(crate) fn tenant_id_from_headers(headers: &HeaderMap) -> Option<String> {
455    headers
456        .get(&X_TENANT_ID)
457        .and_then(|value| value.to_str().ok())
458        .map(str::trim)
459        .filter(|value| !value.is_empty())
460        .map(ToOwned::to_owned)
461}
462
463impl AppState {
464    /// Create a new `AppState` wrapping a [`Commerce`] instance.
465    #[must_use]
466    pub fn new(commerce: Commerce) -> Self {
467        Self::new_with_tenant_db_dir(commerce, None)
468    }
469
470    /// Create a new `AppState` with optional per-tenant database directory.
471    #[must_use]
472    pub fn new_with_tenant_db_dir(commerce: Commerce, tenant_db_dir: Option<PathBuf>) -> Self {
473        Self {
474            commerce: Arc::new(commerce),
475            tenant_db_dir: tenant_db_dir.map(Arc::new),
476            ignore_tenant_header: false,
477            tenant_cache: Arc::new(RwLock::new(HashMap::new())),
478            tenant_access_clock: Arc::new(AtomicU64::new(0)),
479            tenant_cache_hits: Arc::new(AtomicU64::new(0)),
480            tenant_cache_misses: Arc::new(AtomicU64::new(0)),
481            tenant_cache_evictions: Arc::new(AtomicU64::new(0)),
482            tenant_cache_rejections: Arc::new(AtomicU64::new(0)),
483            metrics_scrape_requests: Arc::new(AtomicU64::new(0)),
484            metrics_scrape_allowed: Arc::new(AtomicU64::new(0)),
485            metrics_scrape_allowed_peer: Arc::new(AtomicU64::new(0)),
486            metrics_scrape_allowed_forwarded_trusted_proxy: Arc::new(AtomicU64::new(0)),
487            metrics_scrape_allowed_forwarded_without_peer: Arc::new(AtomicU64::new(0)),
488            metrics_scrape_allowed_unavailable: Arc::new(AtomicU64::new(0)),
489            metrics_scrape_denied_ip: Arc::new(AtomicU64::new(0)),
490            metrics_scrape_denied_ip_not_allowed: Arc::new(AtomicU64::new(0)),
491            metrics_scrape_denied_missing_peer_ip_with_trusted_proxies: Arc::new(AtomicU64::new(0)),
492            metrics_scrape_denied_auth: Arc::new(AtomicU64::new(0)),
493            metrics_scrape_denied_auth_header_missing: Arc::new(AtomicU64::new(0)),
494            metrics_scrape_denied_auth_header_invalid: Arc::new(AtomicU64::new(0)),
495            metrics_scrape_denied_auth_header_invalid_encoding: Arc::new(AtomicU64::new(0)),
496            metrics_scrape_denied_auth_header_invalid_scheme: Arc::new(AtomicU64::new(0)),
497            metrics_scrape_denied_auth_header_malformed: Arc::new(AtomicU64::new(0)),
498            metrics_scrape_denied_auth_header_multiple: Arc::new(AtomicU64::new(0)),
499            metrics_scrape_denied_auth_header_oversized: Arc::new(AtomicU64::new(0)),
500            metrics_scrape_denied_auth_token_mismatch: Arc::new(AtomicU64::new(0)),
501            metrics_scrape_denied_forwarded_missing: Arc::new(AtomicU64::new(0)),
502            metrics_scrape_denied_forwarded_invalid: Arc::new(AtomicU64::new(0)),
503            metrics_scrape_denied_forwarded_oversized: Arc::new(AtomicU64::new(0)),
504            metrics_bearer_token: None,
505            metrics_ip_allowlist: None,
506            metrics_ip_cidr_allowlist: None,
507            metrics_trusted_proxies: None,
508            metrics_header_limits: MetricsHeaderLimits::default(),
509            max_tenant_dbs: DEFAULT_MAX_TENANT_DBS,
510            event_replay: EventReplayRegistry::new(DEFAULT_REPLAY_CAPACITY),
511        }
512    }
513
514    /// Enable per-tenant SQLite routing under a base directory.
515    #[must_use]
516    pub fn with_tenant_db_dir(mut self, tenant_db_dir: impl Into<PathBuf>) -> Self {
517        self.tenant_db_dir = Some(Arc::new(tenant_db_dir.into()));
518        self
519    }
520
521    /// Ignore `x-tenant-id` headers when per-tenant routing is disabled.
522    ///
523    /// By default, a request that carries `x-tenant-id` while no tenant
524    /// database directory is configured is rejected with a `400` so that
525    /// callers are never silently served shared data. Deployments that
526    /// intentionally sit behind a multi-tenant proxy (which forwards the
527    /// header but handles isolation upstream) can opt back into the old
528    /// silent fallthrough behavior with this escape hatch.
529    #[must_use]
530    pub const fn with_ignore_tenant_header(mut self) -> Self {
531        self.ignore_tenant_header = true;
532        self
533    }
534
535    /// Whether `x-tenant-id` headers are ignored when tenant routing is disabled.
536    #[must_use]
537    pub const fn ignores_tenant_header(&self) -> bool {
538        self.ignore_tenant_header
539    }
540
541    /// Set the maximum number of per-tenant databases kept in the active cache.
542    ///
543    /// When capacity is reached, the least-recently-used idle tenant engine is
544    /// evicted to make room for a new tenant. If every cached tenant engine is
545    /// currently in use, creation of an additional tenant database is rejected.
546    #[must_use]
547    pub fn with_max_tenant_dbs(mut self, max_tenant_dbs: usize) -> Self {
548        self.max_tenant_dbs = max_tenant_dbs.max(1);
549        self
550    }
551
552    /// Configure bearer authentication for the `/metrics` endpoint.
553    #[must_use]
554    pub fn with_metrics_bearer_auth(mut self, token: impl Into<String>) -> Self {
555        self.metrics_bearer_token = Some(Arc::<str>::from(token.into()));
556        self
557    }
558
559    /// Disable authentication for `/metrics`.
560    #[must_use]
561    pub fn without_metrics_auth(mut self) -> Self {
562        self.metrics_bearer_token = None;
563        self
564    }
565
566    /// Configure a client IP allowlist for `/metrics`.
567    ///
568    /// Empty lists disable allowlist enforcement.
569    #[must_use]
570    pub fn with_metrics_ip_allowlist<I>(mut self, ips: I) -> Self
571    where
572        I: IntoIterator<Item = IpAddr>,
573    {
574        let set: HashSet<IpAddr> = ips.into_iter().collect();
575        self.metrics_ip_allowlist = if set.is_empty() { None } else { Some(Arc::new(set)) };
576        self
577    }
578
579    /// Disable IP allowlist checks for `/metrics`.
580    #[must_use]
581    pub fn without_metrics_ip_allowlist(mut self) -> Self {
582        self.metrics_ip_allowlist = None;
583        self
584    }
585
586    /// Configure a CIDR-based client IP allowlist for `/metrics`.
587    ///
588    /// Empty lists disable CIDR allowlist enforcement.
589    #[must_use]
590    pub fn with_metrics_ip_cidr_allowlist<I>(mut self, cidrs: I) -> Self
591    where
592        I: IntoIterator<Item = IpCidr>,
593    {
594        let mut cidrs: Vec<IpCidr> = cidrs.into_iter().collect();
595        cidrs.sort_unstable();
596        cidrs.dedup();
597        self.metrics_ip_cidr_allowlist =
598            if cidrs.is_empty() { None } else { Some(Arc::new(cidrs)) };
599        self
600    }
601
602    /// Disable CIDR-based IP allowlist checks for `/metrics`.
603    #[must_use]
604    pub fn without_metrics_ip_cidr_allowlist(mut self) -> Self {
605        self.metrics_ip_cidr_allowlist = None;
606        self
607    }
608
609    /// Configure trusted proxy CIDRs for `/metrics`.
610    ///
611    /// When configured, forwarded headers are only trusted when the request
612    /// peer IP matches one of these networks.
613    #[must_use]
614    pub fn with_metrics_trusted_proxies<I>(mut self, cidrs: I) -> Self
615    where
616        I: IntoIterator<Item = IpCidr>,
617    {
618        let mut proxies: Vec<IpCidr> = cidrs.into_iter().collect();
619        proxies.sort_unstable();
620        proxies.dedup();
621        self.metrics_trusted_proxies =
622            if proxies.is_empty() { None } else { Some(Arc::new(proxies)) };
623        self
624    }
625
626    /// Disable trusted proxy checks for `/metrics`.
627    #[must_use]
628    pub fn without_metrics_trusted_proxies(mut self) -> Self {
629        self.metrics_trusted_proxies = None;
630        self
631    }
632
633    /// Configure max accepted forwarding header lengths for `/metrics`.
634    #[must_use]
635    pub const fn with_metrics_header_limits(mut self, limits: MetricsHeaderLimits) -> Self {
636        self.metrics_header_limits = limits;
637        self
638    }
639
640    /// Returns the configured tenant DB directory when per-tenant routing is enabled.
641    #[must_use]
642    pub fn tenant_db_dir(&self) -> Option<&Path> {
643        self.tenant_db_dir.as_deref().map(std::path::PathBuf::as_path)
644    }
645
646    /// Access the underlying default [`Commerce`] engine.
647    #[must_use]
648    pub fn commerce(&self) -> &Commerce {
649        &self.commerce
650    }
651
652    /// Get (or lazily create) the SSE replay buffer for a `Commerce` engine.
653    ///
654    /// The buffer assigns monotonic ids to events and retains a bounded window
655    /// for `Last-Event-ID` reconnection. Each engine (e.g. each tenant database)
656    /// has an independent buffer and background pump.
657    pub(crate) fn event_replay_buffer(&self, commerce: &Arc<Commerce>) -> Arc<EventReplayBuffer> {
658        self.event_replay.buffer_for(commerce)
659    }
660
661    /// Override the SSE replay ring capacity (used in tests to exercise overflow).
662    #[cfg(test)]
663    #[must_use]
664    pub(crate) fn with_event_replay_capacity(mut self, capacity: usize) -> Self {
665        self.event_replay = EventReplayRegistry::new(capacity);
666        self
667    }
668
669    /// Return the configured bearer token for `/metrics`, if set.
670    #[must_use]
671    pub fn metrics_bearer_auth_token(&self) -> Option<&str> {
672        self.metrics_bearer_token.as_deref()
673    }
674
675    /// Return configured metrics IP allowlist entries.
676    #[must_use]
677    pub fn metrics_ip_allowlist(&self) -> Option<Vec<IpAddr>> {
678        self.metrics_ip_allowlist.as_ref().map(|allowlist| {
679            let mut ips: Vec<IpAddr> = allowlist.iter().copied().collect();
680            ips.sort_unstable();
681            ips
682        })
683    }
684
685    /// Return configured CIDR-based metrics IP allowlist entries.
686    #[must_use]
687    pub fn metrics_ip_cidr_allowlist(&self) -> Option<Vec<IpCidr>> {
688        self.metrics_ip_cidr_allowlist.as_ref().map(|cidrs| cidrs.as_ref().clone())
689    }
690
691    /// Return number of configured exact-IP metrics allowlist entries.
692    #[must_use]
693    pub fn metrics_ip_allowlist_len(&self) -> usize {
694        self.metrics_ip_allowlist.as_ref().map_or(0, |allowlist| allowlist.len())
695    }
696
697    /// Return number of configured CIDR metrics allowlist entries.
698    #[must_use]
699    pub fn metrics_ip_cidr_allowlist_len(&self) -> usize {
700        self.metrics_ip_cidr_allowlist.as_ref().map_or(0, |cidrs| cidrs.len())
701    }
702
703    /// Returns whether `/metrics` IP allowlist checks are enabled.
704    #[must_use]
705    pub const fn has_metrics_ip_allowlist(&self) -> bool {
706        self.metrics_ip_allowlist.is_some() || self.metrics_ip_cidr_allowlist.is_some()
707    }
708
709    /// Returns configured trusted proxy CIDRs.
710    #[must_use]
711    pub fn metrics_trusted_proxies(&self) -> Option<Vec<IpCidr>> {
712        self.metrics_trusted_proxies.as_ref().map(|proxies| proxies.as_ref().clone())
713    }
714
715    /// Return number of configured trusted proxy CIDR entries.
716    #[must_use]
717    pub fn metrics_trusted_proxies_len(&self) -> usize {
718        self.metrics_trusted_proxies.as_ref().map_or(0, |proxies| proxies.len())
719    }
720
721    /// Record a `/metrics` scrape attempt.
722    pub(crate) fn record_metrics_scrape_attempt(&self) {
723        self.metrics_scrape_requests.fetch_add(1, Ordering::Relaxed);
724    }
725
726    /// Record a successful `/metrics` scrape.
727    pub(crate) fn record_metrics_scrape_allowed(&self) {
728        self.metrics_scrape_allowed.fetch_add(1, Ordering::Relaxed);
729    }
730
731    /// Record a successful `/metrics` scrape attributed to direct peer IP.
732    pub(crate) fn record_metrics_scrape_allowed_peer(&self) {
733        self.metrics_scrape_allowed_peer.fetch_add(1, Ordering::Relaxed);
734    }
735
736    /// Record a successful `/metrics` scrape attributed to forwarded IP from a trusted proxy.
737    pub(crate) fn record_metrics_scrape_allowed_forwarded_trusted_proxy(&self) {
738        self.metrics_scrape_allowed_forwarded_trusted_proxy.fetch_add(1, Ordering::Relaxed);
739    }
740
741    /// Record a successful `/metrics` scrape attributed to forwarded headers without a peer IP.
742    pub(crate) fn record_metrics_scrape_allowed_forwarded_without_peer(&self) {
743        self.metrics_scrape_allowed_forwarded_without_peer.fetch_add(1, Ordering::Relaxed);
744    }
745
746    /// Record a successful `/metrics` scrape where no client IP source was resolved.
747    pub(crate) fn record_metrics_scrape_allowed_unavailable(&self) {
748        self.metrics_scrape_allowed_unavailable.fetch_add(1, Ordering::Relaxed);
749    }
750
751    /// Record a `/metrics` scrape denied by network policy.
752    pub(crate) fn record_metrics_scrape_denied_ip(&self) {
753        self.metrics_scrape_denied_ip.fetch_add(1, Ordering::Relaxed);
754    }
755
756    /// Record a `/metrics` scrape denied because the resolved client IP was not allowed.
757    pub(crate) fn record_metrics_scrape_denied_ip_not_allowed(&self) {
758        self.metrics_scrape_denied_ip_not_allowed.fetch_add(1, Ordering::Relaxed);
759    }
760
761    /// Record a `/metrics` scrape denied because trusted proxy mode had no peer IP metadata.
762    pub(crate) fn record_metrics_scrape_denied_missing_peer_ip_with_trusted_proxies(&self) {
763        self.metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
764            .fetch_add(1, Ordering::Relaxed);
765    }
766
767    /// Record a `/metrics` scrape denied by authentication.
768    pub(crate) fn record_metrics_scrape_denied_auth(&self) {
769        self.metrics_scrape_denied_auth.fetch_add(1, Ordering::Relaxed);
770    }
771
772    /// Record a `/metrics` scrape denied due to a missing Authorization header.
773    pub(crate) fn record_metrics_scrape_denied_auth_header_missing(&self) {
774        self.metrics_scrape_denied_auth_header_missing.fetch_add(1, Ordering::Relaxed);
775    }
776
777    /// Record a `/metrics` scrape denied due to an invalid Authorization header format.
778    pub(crate) fn record_metrics_scrape_denied_auth_header_invalid(&self) {
779        self.metrics_scrape_denied_auth_header_invalid.fetch_add(1, Ordering::Relaxed);
780    }
781
782    /// Record a `/metrics` scrape denied due to non-UTF8 Authorization header bytes.
783    pub(crate) fn record_metrics_scrape_denied_auth_header_invalid_encoding(&self) {
784        self.metrics_scrape_denied_auth_header_invalid_encoding.fetch_add(1, Ordering::Relaxed);
785    }
786
787    /// Record a `/metrics` scrape denied due to non-Bearer Authorization scheme.
788    pub(crate) fn record_metrics_scrape_denied_auth_header_invalid_scheme(&self) {
789        self.metrics_scrape_denied_auth_header_invalid_scheme.fetch_add(1, Ordering::Relaxed);
790    }
791
792    /// Record a `/metrics` scrape denied due to malformed Authorization header structure.
793    pub(crate) fn record_metrics_scrape_denied_auth_header_malformed(&self) {
794        self.metrics_scrape_denied_auth_header_malformed.fetch_add(1, Ordering::Relaxed);
795    }
796
797    /// Record a `/metrics` scrape denied due to multiple Authorization headers.
798    pub(crate) fn record_metrics_scrape_denied_auth_header_multiple(&self) {
799        self.metrics_scrape_denied_auth_header_multiple.fetch_add(1, Ordering::Relaxed);
800    }
801
802    /// Record a `/metrics` scrape denied due to oversized Authorization header value.
803    pub(crate) fn record_metrics_scrape_denied_auth_header_oversized(&self) {
804        self.metrics_scrape_denied_auth_header_oversized.fetch_add(1, Ordering::Relaxed);
805    }
806
807    /// Record a `/metrics` scrape denied due to bearer token mismatch.
808    pub(crate) fn record_metrics_scrape_denied_auth_token_mismatch(&self) {
809        self.metrics_scrape_denied_auth_token_mismatch.fetch_add(1, Ordering::Relaxed);
810    }
811
812    /// Record a `/metrics` scrape denied because forwarded headers were missing.
813    pub(crate) fn record_metrics_scrape_denied_forwarded_missing(&self) {
814        self.metrics_scrape_denied_forwarded_missing.fetch_add(1, Ordering::Relaxed);
815    }
816
817    /// Record a `/metrics` scrape denied because forwarded headers were invalid.
818    pub(crate) fn record_metrics_scrape_denied_forwarded_invalid(&self) {
819        self.metrics_scrape_denied_forwarded_invalid.fetch_add(1, Ordering::Relaxed);
820    }
821
822    /// Record a `/metrics` scrape denied because forwarded headers were oversized.
823    pub(crate) fn record_metrics_scrape_denied_forwarded_oversized(&self) {
824        self.metrics_scrape_denied_forwarded_oversized.fetch_add(1, Ordering::Relaxed);
825    }
826
827    /// Snapshot `/metrics` access outcome counters.
828    #[must_use]
829    pub(crate) fn metrics_access_metrics(&self) -> MetricsAccessMetrics {
830        MetricsAccessMetrics {
831            requests_total: self.metrics_scrape_requests.load(Ordering::Relaxed),
832            allowed_total: self.metrics_scrape_allowed.load(Ordering::Relaxed),
833            allowed_peer_total: self.metrics_scrape_allowed_peer.load(Ordering::Relaxed),
834            allowed_forwarded_trusted_proxy_total: self
835                .metrics_scrape_allowed_forwarded_trusted_proxy
836                .load(Ordering::Relaxed),
837            allowed_forwarded_without_peer_total: self
838                .metrics_scrape_allowed_forwarded_without_peer
839                .load(Ordering::Relaxed),
840            allowed_unavailable_total: self
841                .metrics_scrape_allowed_unavailable
842                .load(Ordering::Relaxed),
843            denied_ip_total: self.metrics_scrape_denied_ip.load(Ordering::Relaxed),
844            denied_ip_not_allowed_total: self
845                .metrics_scrape_denied_ip_not_allowed
846                .load(Ordering::Relaxed),
847            denied_missing_peer_ip_with_trusted_proxies_total: self
848                .metrics_scrape_denied_missing_peer_ip_with_trusted_proxies
849                .load(Ordering::Relaxed),
850            denied_auth_total: self.metrics_scrape_denied_auth.load(Ordering::Relaxed),
851            denied_auth_header_missing_total: self
852                .metrics_scrape_denied_auth_header_missing
853                .load(Ordering::Relaxed),
854            denied_auth_header_invalid_total: self
855                .metrics_scrape_denied_auth_header_invalid
856                .load(Ordering::Relaxed),
857            denied_auth_header_invalid_encoding_total: self
858                .metrics_scrape_denied_auth_header_invalid_encoding
859                .load(Ordering::Relaxed),
860            denied_auth_header_invalid_scheme_total: self
861                .metrics_scrape_denied_auth_header_invalid_scheme
862                .load(Ordering::Relaxed),
863            denied_auth_header_malformed_total: self
864                .metrics_scrape_denied_auth_header_malformed
865                .load(Ordering::Relaxed),
866            denied_auth_header_multiple_total: self
867                .metrics_scrape_denied_auth_header_multiple
868                .load(Ordering::Relaxed),
869            denied_auth_header_oversized_total: self
870                .metrics_scrape_denied_auth_header_oversized
871                .load(Ordering::Relaxed),
872            denied_auth_token_mismatch_total: self
873                .metrics_scrape_denied_auth_token_mismatch
874                .load(Ordering::Relaxed),
875            denied_forwarded_missing_total: self
876                .metrics_scrape_denied_forwarded_missing
877                .load(Ordering::Relaxed),
878            denied_forwarded_invalid_total: self
879                .metrics_scrape_denied_forwarded_invalid
880                .load(Ordering::Relaxed),
881            denied_forwarded_oversized_total: self
882                .metrics_scrape_denied_forwarded_oversized
883                .load(Ordering::Relaxed),
884        }
885    }
886
887    /// Returns max accepted forwarding header lengths for `/metrics`.
888    #[must_use]
889    pub const fn metrics_header_limits(&self) -> MetricsHeaderLimits {
890        self.metrics_header_limits
891    }
892
893    /// Returns whether trusted proxy CIDRs are configured.
894    #[must_use]
895    pub const fn has_metrics_trusted_proxies(&self) -> bool {
896        self.metrics_trusted_proxies.is_some()
897    }
898
899    /// Check whether a peer IP is within configured trusted proxy CIDRs.
900    #[must_use]
901    pub fn is_metrics_trusted_proxy(&self, ip: IpAddr) -> bool {
902        self.metrics_trusted_proxies
903            .as_ref()
904            .is_some_and(|proxies| proxies.iter().any(|proxy| proxy.contains(ip)))
905    }
906
907    /// Check whether a metrics client IP is allowed by the configured allowlist.
908    #[must_use]
909    pub fn is_metrics_ip_allowed(&self, ip: IpAddr) -> bool {
910        if !self.has_metrics_ip_allowlist() {
911            return true;
912        }
913
914        self.metrics_ip_allowlist.as_ref().is_some_and(|allowlist| allowlist.contains(&ip))
915            || self
916                .metrics_ip_cidr_allowlist
917                .as_ref()
918                .is_some_and(|cidrs| cidrs.iter().any(|cidr| cidr.contains(ip)))
919    }
920
921    /// Snapshot tenant cache runtime metrics.
922    #[must_use]
923    pub fn tenant_cache_metrics(&self) -> TenantCacheMetrics {
924        let (cached_dbs, in_use_cached_dbs) = self
925            .tenant_cache
926            .read()
927            .map(|cache| {
928                let cached = cache.len();
929                let in_use =
930                    cache.values().filter(|entry| Arc::strong_count(&entry.commerce) > 1).count();
931                (cached, in_use)
932            })
933            .unwrap_or((0, 0));
934
935        TenantCacheMetrics {
936            enabled: self.tenant_db_dir.is_some(),
937            max_cached_dbs: self.max_tenant_dbs,
938            cached_dbs,
939            in_use_cached_dbs,
940            hits: self.tenant_cache_hits.load(Ordering::Relaxed),
941            misses: self.tenant_cache_misses.load(Ordering::Relaxed),
942            evictions: self.tenant_cache_evictions.load(Ordering::Relaxed),
943            rejections: self.tenant_cache_rejections.load(Ordering::Relaxed),
944        }
945    }
946
947    /// Resolve the [`Commerce`] engine for a tenant.
948    ///
949    /// When per-tenant routing is disabled, requests without a tenant id get
950    /// the default engine. Requests that *do* carry a tenant id are rejected
951    /// (the backend cannot isolate them), unless
952    /// [`Self::with_ignore_tenant_header`] was configured.
953    pub fn commerce_for_tenant(&self, tenant_id: Option<&str>) -> Result<Arc<Commerce>, HttpError> {
954        let Some(base_dir) = self.tenant_db_dir.as_deref() else {
955            let carries_tenant = tenant_id.map(str::trim).is_some_and(|value| !value.is_empty());
956            if carries_tenant && !self.ignore_tenant_header {
957                return Err(HttpError::BadRequest(
958                    "tenant routing is not enabled on this deployment; remove the x-tenant-id \
959                     header or configure a tenant database directory"
960                        .to_string(),
961                ));
962            }
963            return Ok(Arc::clone(&self.commerce));
964        };
965
966        let tenant_id = tenant_id
967            .map(str::trim)
968            .filter(|value| !value.is_empty())
969            .ok_or_else(|| HttpError::BadRequest("missing x-tenant-id header".to_string()))?;
970
971        if !is_valid_tenant_id(tenant_id) {
972            return Err(HttpError::BadRequest("invalid x-tenant-id header".to_string()));
973        }
974
975        let access_tick = self.next_tenant_access_tick();
976        let mut cache = self.tenant_cache.write().map_err(|_| {
977            HttpError::InternalError("tenant cache lock poisoned while writing".to_string())
978        })?;
979        if let Some(existing) = cache.get_mut(tenant_id) {
980            existing.last_access_tick = access_tick;
981            self.tenant_cache_hits.fetch_add(1, Ordering::Relaxed);
982            return Ok(Arc::clone(&existing.commerce));
983        }
984        self.tenant_cache_misses.fetch_add(1, Ordering::Relaxed);
985        drop(cache);
986
987        std::fs::create_dir_all(base_dir).map_err(|error| {
988            HttpError::InternalError(format!(
989                "failed to create tenant database directory {}: {error}",
990                base_dir.display()
991            ))
992        })?;
993
994        let canonical_base = base_dir.canonicalize().map_err(|error| {
995            HttpError::InternalError(format!(
996                "failed to canonicalize tenant database directory {}: {error}",
997                base_dir.display()
998            ))
999        })?;
1000        let db_path = canonical_base.join(format!("{tenant_id}.db"));
1001        // Defense in depth: the validated tenant id cannot contain path
1002        // separators or dot components, but verify the resolved path still
1003        // lives directly inside the tenant database directory.
1004        if db_path.parent() != Some(canonical_base.as_path())
1005            || !db_path.starts_with(&canonical_base)
1006        {
1007            return Err(HttpError::BadRequest("invalid x-tenant-id header".to_string()));
1008        }
1009        let db_path_str = db_path.to_string_lossy().into_owned();
1010        let created = Arc::new(Commerce::new(&db_path_str).map_err(|error| {
1011            HttpError::InternalError(format!(
1012                "failed to initialize tenant database for '{tenant_id}': {error}"
1013            ))
1014        })?);
1015
1016        let mut cache = self.tenant_cache.write().map_err(|_| {
1017            HttpError::InternalError("tenant cache lock poisoned while writing".to_string())
1018        })?;
1019        let access_tick = self.next_tenant_access_tick();
1020        if let Some(existing) = cache.get_mut(tenant_id) {
1021            existing.last_access_tick = access_tick;
1022            self.tenant_cache_hits.fetch_add(1, Ordering::Relaxed);
1023            return Ok(Arc::clone(&existing.commerce));
1024        }
1025        if cache.len() >= self.max_tenant_dbs {
1026            self.evict_lru_idle_tenant(&mut cache)?;
1027        }
1028        let entry = cache.entry(tenant_id.to_string()).or_insert_with(|| TenantCacheEntry {
1029            commerce: Arc::clone(&created),
1030            last_access_tick: access_tick,
1031        });
1032        Ok(Arc::clone(&entry.commerce))
1033    }
1034
1035    /// Resolve the tenant [`Commerce`] engine and run a synchronous closure
1036    /// against it on a blocking-friendly thread, off the async worker pool.
1037    ///
1038    /// The repository layer is synchronous: with the default SQLite backend the
1039    /// calls are CPU/disk-bound and would otherwise stall a Tokio worker thread,
1040    /// and the optional Postgres backend bridges to async via a blocking
1041    /// `block_on` shim that *cannot* run inside an async runtime. Routing the
1042    /// closure through [`tokio::task::spawn_blocking`] keeps the async executor
1043    /// responsive under load and keeps the Postgres path valid.
1044    ///
1045    /// Tenant resolution happens before the closure is dispatched so that
1046    /// routing errors (missing/invalid `x-tenant-id`) surface synchronously with
1047    /// their normal status codes.
1048    pub(crate) async fn run_blocking<F, T>(
1049        &self,
1050        tenant_id: Option<&str>,
1051        f: F,
1052    ) -> Result<T, HttpError>
1053    where
1054        F: FnOnce(&Commerce) -> Result<T, HttpError> + Send + 'static,
1055        T: Send + 'static,
1056    {
1057        let commerce = self.commerce_for_tenant(tenant_id)?;
1058        tokio::task::spawn_blocking(move || f(&commerce)).await.map_err(|join_error| {
1059            HttpError::InternalError(format!("blocking commerce task failed: {join_error}"))
1060        })?
1061    }
1062
1063    fn next_tenant_access_tick(&self) -> u64 {
1064        self.tenant_access_clock.fetch_add(1, Ordering::Relaxed)
1065    }
1066
1067    fn evict_lru_idle_tenant(
1068        &self,
1069        cache: &mut HashMap<String, TenantCacheEntry>,
1070    ) -> Result<(), HttpError> {
1071        let Some((tenant_id, _)) = cache
1072            .iter()
1073            .filter(|(_, entry)| Arc::strong_count(&entry.commerce) == 1)
1074            .min_by_key(|(_, entry)| entry.last_access_tick)
1075            .map(|(tenant_id, entry)| (tenant_id.clone(), entry.last_access_tick))
1076        else {
1077            self.tenant_cache_rejections.fetch_add(1, Ordering::Relaxed);
1078            return Err(HttpError::TooManyRequests(format!(
1079                "tenant database limit reached (max: {}); all cached tenant engines are currently in use",
1080                self.max_tenant_dbs
1081            )));
1082        };
1083
1084        cache.remove(&tenant_id);
1085        self.tenant_cache_evictions.fetch_add(1, Ordering::Relaxed);
1086        Ok(())
1087    }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092    use super::*;
1093    use stateset_core::CreateCustomer;
1094    use uuid::Uuid;
1095
1096    fn test_commerce() -> Commerce {
1097        Commerce::new(":memory:").expect("in-memory Commerce")
1098    }
1099
1100    #[test]
1101    fn tenant_id_validation_rejects_path_like_values() {
1102        assert!(is_valid_tenant_id("tenant-1"));
1103        assert!(is_valid_tenant_id("Tenant_2"));
1104        assert!(is_valid_tenant_id(&"a".repeat(64)));
1105
1106        assert!(!is_valid_tenant_id(""));
1107        assert!(!is_valid_tenant_id("   "));
1108        assert!(!is_valid_tenant_id(&"a".repeat(65)));
1109        assert!(!is_valid_tenant_id("."));
1110        assert!(!is_valid_tenant_id(".."));
1111        assert!(!is_valid_tenant_id("tenant.1"));
1112        assert!(!is_valid_tenant_id("../etc/passwd"));
1113        assert!(!is_valid_tenant_id("./tenant"));
1114        assert!(!is_valid_tenant_id("a/b"));
1115        assert!(!is_valid_tenant_id("a\\b"));
1116        assert!(!is_valid_tenant_id("/absolute"));
1117    }
1118
1119    #[test]
1120    fn tenant_header_without_tenant_routing_is_rejected() {
1121        let state = AppState::new(test_commerce());
1122        let err = state.commerce_for_tenant(Some("tenant-1")).unwrap_err();
1123        assert!(matches!(err, HttpError::BadRequest(_)));
1124        assert!(err.to_string().contains("tenant routing is not enabled"));
1125    }
1126
1127    #[test]
1128    fn missing_tenant_header_without_tenant_routing_uses_shared_engine() {
1129        let state = AppState::new(test_commerce());
1130        let shared = state.commerce_for_tenant(None).expect("shared engine");
1131        assert!(Arc::ptr_eq(&shared, &state.commerce));
1132        // A blank header value is treated as absent.
1133        let blank = state.commerce_for_tenant(Some("  ")).expect("shared engine");
1134        assert!(Arc::ptr_eq(&blank, &state.commerce));
1135    }
1136
1137    #[test]
1138    fn ignore_tenant_header_escape_hatch_restores_fallthrough() {
1139        let state = AppState::new(test_commerce()).with_ignore_tenant_header();
1140        assert!(state.ignores_tenant_header());
1141        let shared = state.commerce_for_tenant(Some("tenant-1")).expect("shared engine");
1142        assert!(Arc::ptr_eq(&shared, &state.commerce));
1143    }
1144
1145    #[test]
1146    fn tenant_routing_rejects_traversal_attempts() {
1147        let base = std::env::temp_dir().join(format!("stateset-state-tenant-{}", Uuid::new_v4()));
1148        let state = AppState::new(test_commerce()).with_tenant_db_dir(&base);
1149        for candidate in ["..", ".", "../other", "./x", "/etc/passwd", "a/b", "a\\b", "a.b"] {
1150            let err = state.commerce_for_tenant(Some(candidate)).unwrap_err();
1151            assert!(matches!(err, HttpError::BadRequest(_)), "{candidate} should be rejected");
1152        }
1153        // A valid tenant id still resolves and lands inside the base directory.
1154        let engine = state.commerce_for_tenant(Some("tenant-ok")).expect("tenant engine");
1155        assert!(!Arc::ptr_eq(&engine, &state.commerce));
1156        assert!(base.join("tenant-ok.db").exists());
1157        let _ = std::fs::remove_dir_all(&base);
1158    }
1159
1160    #[test]
1161    fn state_debug_impl() {
1162        let state = AppState::new(test_commerce());
1163        let dbg = format!("{state:?}");
1164        assert!(dbg.contains("AppState"));
1165    }
1166
1167    #[test]
1168    fn state_clone() {
1169        let state = AppState::new(test_commerce());
1170        let cloned = state.clone();
1171        // Both point to the same Arc
1172        assert!(Arc::ptr_eq(&state.commerce, &cloned.commerce));
1173    }
1174
1175    #[test]
1176    fn state_accessor() {
1177        let state = AppState::new(test_commerce());
1178        // Just verify it doesn't panic
1179        let _commerce = state.commerce();
1180    }
1181
1182    #[test]
1183    fn metrics_auth_configuration_round_trip() {
1184        let state = AppState::new(test_commerce()).with_metrics_bearer_auth("metrics-token");
1185        assert_eq!(state.metrics_bearer_auth_token(), Some("metrics-token"));
1186
1187        let disabled = state.without_metrics_auth();
1188        assert_eq!(disabled.metrics_bearer_auth_token(), None);
1189    }
1190
1191    #[test]
1192    fn metrics_ip_allowlist_configuration_round_trip() {
1193        let state = AppState::new(test_commerce())
1194            .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap(), "10.0.0.1".parse().unwrap()]);
1195        assert_eq!(
1196            state.metrics_ip_allowlist().unwrap(),
1197            vec!["10.0.0.1".parse::<IpAddr>().unwrap(), "127.0.0.1".parse::<IpAddr>().unwrap()]
1198        );
1199        assert_eq!(state.metrics_ip_allowlist_len(), 2);
1200        assert_eq!(state.metrics_ip_cidr_allowlist_len(), 0);
1201        assert!(state.metrics_ip_cidr_allowlist().is_none());
1202        assert!(state.has_metrics_ip_allowlist());
1203        assert!(state.is_metrics_ip_allowed("127.0.0.1".parse().unwrap()));
1204        assert!(!state.is_metrics_ip_allowed("192.168.1.10".parse().unwrap()));
1205
1206        let disabled = state.without_metrics_ip_allowlist();
1207        assert!(disabled.metrics_ip_allowlist().is_none());
1208        assert_eq!(disabled.metrics_ip_allowlist_len(), 0);
1209        assert!(!disabled.has_metrics_ip_allowlist());
1210        assert!(disabled.is_metrics_ip_allowed("192.168.1.10".parse().unwrap()));
1211    }
1212
1213    #[test]
1214    fn metrics_ip_cidr_allowlist_configuration_round_trip() {
1215        let state = AppState::new(test_commerce()).with_metrics_ip_cidr_allowlist([
1216            "10.0.0.0/8".parse().unwrap(),
1217            "127.0.0.1".parse().unwrap(),
1218        ]);
1219        assert_eq!(
1220            state.metrics_ip_cidr_allowlist().unwrap(),
1221            vec!["10.0.0.0/8".parse().unwrap(), "127.0.0.1/32".parse().unwrap()]
1222        );
1223        assert_eq!(state.metrics_ip_allowlist_len(), 0);
1224        assert_eq!(state.metrics_ip_cidr_allowlist_len(), 2);
1225        assert!(state.metrics_ip_allowlist().is_none());
1226        assert!(state.has_metrics_ip_allowlist());
1227        assert!(state.is_metrics_ip_allowed("10.5.6.7".parse().unwrap()));
1228        assert!(state.is_metrics_ip_allowed("127.0.0.1".parse().unwrap()));
1229        assert!(!state.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1230
1231        let disabled = state.without_metrics_ip_cidr_allowlist();
1232        assert!(disabled.metrics_ip_cidr_allowlist().is_none());
1233        assert_eq!(disabled.metrics_ip_cidr_allowlist_len(), 0);
1234        assert!(!disabled.has_metrics_ip_allowlist());
1235        assert!(disabled.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1236    }
1237
1238    #[test]
1239    fn metrics_ip_allowlists_are_union_when_both_configured() {
1240        let state = AppState::new(test_commerce())
1241            .with_metrics_ip_allowlist(["203.0.113.10".parse().unwrap()])
1242            .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()]);
1243
1244        assert!(state.is_metrics_ip_allowed("203.0.113.10".parse().unwrap()));
1245        assert!(state.is_metrics_ip_allowed("10.1.2.3".parse().unwrap()));
1246        assert!(!state.is_metrics_ip_allowed("192.168.1.1".parse().unwrap()));
1247    }
1248
1249    #[test]
1250    fn ip_cidr_parsing_and_contains() {
1251        let cidr = "10.0.0.0/8".parse::<IpCidr>().unwrap();
1252        assert!(cidr.contains("10.1.2.3".parse().unwrap()));
1253        assert!(!cidr.contains("11.1.2.3".parse().unwrap()));
1254
1255        let host = "127.0.0.1".parse::<IpCidr>().unwrap();
1256        assert_eq!(host.to_string(), "127.0.0.1/32");
1257        assert!(host.contains("127.0.0.1".parse().unwrap()));
1258        assert!(!host.contains("127.0.0.2".parse().unwrap()));
1259    }
1260
1261    #[test]
1262    fn metrics_trusted_proxies_configuration_round_trip() {
1263        let state = AppState::new(test_commerce()).with_metrics_trusted_proxies([
1264            "10.0.0.0/8".parse().unwrap(),
1265            "127.0.0.1".parse().unwrap(),
1266        ]);
1267
1268        let proxies = state.metrics_trusted_proxies().unwrap();
1269        assert_eq!(proxies.len(), 2);
1270        assert_eq!(state.metrics_trusted_proxies_len(), 2);
1271        assert!(state.has_metrics_trusted_proxies());
1272        assert!(state.is_metrics_trusted_proxy("10.9.8.7".parse().unwrap()));
1273        assert!(state.is_metrics_trusted_proxy("127.0.0.1".parse().unwrap()));
1274        assert!(!state.is_metrics_trusted_proxy("192.168.1.10".parse().unwrap()));
1275
1276        let disabled = state.without_metrics_trusted_proxies();
1277        assert!(disabled.metrics_trusted_proxies().is_none());
1278        assert_eq!(disabled.metrics_trusted_proxies_len(), 0);
1279        assert!(!disabled.has_metrics_trusted_proxies());
1280        assert!(!disabled.is_metrics_trusted_proxy("127.0.0.1".parse().unwrap()));
1281    }
1282
1283    #[test]
1284    fn metrics_header_limits_configuration_round_trip() {
1285        let limits = MetricsHeaderLimits::new_with_authorization(1024, 1536, 256, 768).unwrap();
1286        let state = AppState::new(test_commerce()).with_metrics_header_limits(limits);
1287        assert_eq!(state.metrics_header_limits(), limits);
1288        assert_eq!(state.metrics_header_limits().authorization_header_value_bytes(), 768);
1289    }
1290
1291    #[test]
1292    fn metrics_header_limits_reject_zero_values() {
1293        assert!(MetricsHeaderLimits::new(0, 1024, 256).is_err());
1294        assert!(MetricsHeaderLimits::new(1024, 0, 256).is_err());
1295        assert!(MetricsHeaderLimits::new(1024, 1024, 0).is_err());
1296        assert!(MetricsHeaderLimits::new_with_authorization(1024, 1024, 256, 0).is_err());
1297    }
1298
1299    #[test]
1300    fn metrics_access_metrics_track_counters() {
1301        let state = AppState::new(test_commerce());
1302        let initial = state.metrics_access_metrics();
1303        assert_eq!(initial.requests_total, 0);
1304        assert_eq!(initial.allowed_total, 0);
1305        assert_eq!(initial.allowed_peer_total, 0);
1306        assert_eq!(initial.allowed_forwarded_trusted_proxy_total, 0);
1307        assert_eq!(initial.allowed_forwarded_without_peer_total, 0);
1308        assert_eq!(initial.allowed_unavailable_total, 0);
1309        assert_eq!(initial.denied_ip_total, 0);
1310        assert_eq!(initial.denied_ip_not_allowed_total, 0);
1311        assert_eq!(initial.denied_missing_peer_ip_with_trusted_proxies_total, 0);
1312        assert_eq!(initial.denied_auth_total, 0);
1313        assert_eq!(initial.denied_auth_header_missing_total, 0);
1314        assert_eq!(initial.denied_auth_header_invalid_total, 0);
1315        assert_eq!(initial.denied_auth_header_invalid_encoding_total, 0);
1316        assert_eq!(initial.denied_auth_header_invalid_scheme_total, 0);
1317        assert_eq!(initial.denied_auth_header_malformed_total, 0);
1318        assert_eq!(initial.denied_auth_header_multiple_total, 0);
1319        assert_eq!(initial.denied_auth_header_oversized_total, 0);
1320        assert_eq!(initial.denied_auth_token_mismatch_total, 0);
1321        assert_eq!(initial.denied_forwarded_missing_total, 0);
1322        assert_eq!(initial.denied_forwarded_invalid_total, 0);
1323        assert_eq!(initial.denied_forwarded_oversized_total, 0);
1324
1325        state.record_metrics_scrape_attempt();
1326        state.record_metrics_scrape_denied_auth();
1327        state.record_metrics_scrape_denied_auth_header_missing();
1328        state.record_metrics_scrape_attempt();
1329        state.record_metrics_scrape_denied_ip();
1330        state.record_metrics_scrape_denied_ip_not_allowed();
1331        state.record_metrics_scrape_denied_forwarded_missing();
1332        state.record_metrics_scrape_attempt();
1333        state.record_metrics_scrape_denied_auth();
1334        state.record_metrics_scrape_denied_auth_header_invalid();
1335        state.record_metrics_scrape_denied_auth_header_invalid_encoding();
1336        state.record_metrics_scrape_denied_auth_header_invalid();
1337        state.record_metrics_scrape_denied_auth_header_invalid_scheme();
1338        state.record_metrics_scrape_denied_auth_header_invalid();
1339        state.record_metrics_scrape_denied_auth_header_malformed();
1340        state.record_metrics_scrape_attempt();
1341        state.record_metrics_scrape_denied_auth();
1342        state.record_metrics_scrape_denied_auth_header_multiple();
1343        state.record_metrics_scrape_attempt();
1344        state.record_metrics_scrape_denied_auth();
1345        state.record_metrics_scrape_denied_auth_header_oversized();
1346        state.record_metrics_scrape_attempt();
1347        state.record_metrics_scrape_denied_auth();
1348        state.record_metrics_scrape_denied_auth_token_mismatch();
1349        state.record_metrics_scrape_attempt();
1350        state.record_metrics_scrape_denied_ip();
1351        state.record_metrics_scrape_denied_forwarded_invalid();
1352        state.record_metrics_scrape_attempt();
1353        state.record_metrics_scrape_denied_ip();
1354        state.record_metrics_scrape_denied_missing_peer_ip_with_trusted_proxies();
1355        state.record_metrics_scrape_denied_forwarded_oversized();
1356        state.record_metrics_scrape_attempt();
1357        state.record_metrics_scrape_allowed();
1358        state.record_metrics_scrape_allowed_peer();
1359        state.record_metrics_scrape_allowed_forwarded_trusted_proxy();
1360        state.record_metrics_scrape_allowed_forwarded_without_peer();
1361        state.record_metrics_scrape_allowed_unavailable();
1362
1363        let snapshot = state.metrics_access_metrics();
1364        assert_eq!(snapshot.requests_total, 9);
1365        assert_eq!(snapshot.allowed_total, 1);
1366        assert_eq!(snapshot.allowed_peer_total, 1);
1367        assert_eq!(snapshot.allowed_forwarded_trusted_proxy_total, 1);
1368        assert_eq!(snapshot.allowed_forwarded_without_peer_total, 1);
1369        assert_eq!(snapshot.allowed_unavailable_total, 1);
1370        assert_eq!(snapshot.denied_ip_total, 3);
1371        assert_eq!(snapshot.denied_ip_not_allowed_total, 1);
1372        assert_eq!(snapshot.denied_missing_peer_ip_with_trusted_proxies_total, 1);
1373        assert_eq!(snapshot.denied_auth_total, 5);
1374        assert_eq!(snapshot.denied_auth_header_missing_total, 1);
1375        assert_eq!(snapshot.denied_auth_header_invalid_total, 3);
1376        assert_eq!(snapshot.denied_auth_header_invalid_encoding_total, 1);
1377        assert_eq!(snapshot.denied_auth_header_invalid_scheme_total, 1);
1378        assert_eq!(snapshot.denied_auth_header_malformed_total, 1);
1379        assert_eq!(snapshot.denied_auth_header_multiple_total, 1);
1380        assert_eq!(snapshot.denied_auth_header_oversized_total, 1);
1381        assert_eq!(snapshot.denied_auth_token_mismatch_total, 1);
1382        assert_eq!(snapshot.denied_forwarded_missing_total, 1);
1383        assert_eq!(snapshot.denied_forwarded_invalid_total, 1);
1384        assert_eq!(snapshot.denied_forwarded_oversized_total, 1);
1385    }
1386
1387    #[test]
1388    fn tenant_header_parser_reads_valid_value() {
1389        let mut headers = HeaderMap::new();
1390        headers.insert(X_TENANT_ID.clone(), "tenant-1".parse().unwrap());
1391        assert_eq!(tenant_id_from_headers(&headers).as_deref(), Some("tenant-1"));
1392    }
1393
1394    #[test]
1395    fn tenant_header_parser_rejects_empty_values() {
1396        let mut headers = HeaderMap::new();
1397        headers.insert(X_TENANT_ID.clone(), " ".parse().unwrap());
1398        assert!(tenant_id_from_headers(&headers).is_none());
1399    }
1400
1401    #[test]
1402    fn tenant_routing_isolated_when_configured() {
1403        let tenant_dir =
1404            std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1405        let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()));
1406
1407        let tenant_a = state.commerce_for_tenant(Some("tenant-a")).expect("tenant-a commerce");
1408        let tenant_b = state.commerce_for_tenant(Some("tenant-b")).expect("tenant-b commerce");
1409
1410        tenant_a
1411            .customers()
1412            .create(CreateCustomer {
1413                email: "tenant-a@example.com".into(),
1414                first_name: "Tenant".into(),
1415                last_name: "A".into(),
1416                ..Default::default()
1417            })
1418            .unwrap();
1419
1420        let a_count = tenant_a.customers().list(Default::default()).unwrap().len();
1421        let b_count = tenant_b.customers().list(Default::default()).unwrap().len();
1422        assert_eq!(a_count, 1);
1423        assert_eq!(b_count, 0);
1424
1425        let _ = std::fs::remove_dir_all(tenant_dir);
1426    }
1427
1428    #[test]
1429    fn tenant_routing_requires_header_when_enabled() {
1430        let tenant_dir =
1431            std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1432        let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()));
1433
1434        let err = state.commerce_for_tenant(None).expect_err("expected missing tenant error");
1435        assert!(matches!(err, HttpError::BadRequest(_)));
1436
1437        let _ = std::fs::remove_dir_all(tenant_dir);
1438    }
1439
1440    #[test]
1441    fn tenant_routing_enforces_database_limit() {
1442        let tenant_dir =
1443            std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1444        let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1445            .with_max_tenant_dbs(1);
1446
1447        let first = state.commerce_for_tenant(Some("tenant-a"));
1448        assert!(first.is_ok());
1449
1450        let second = state.commerce_for_tenant(Some("tenant-b"));
1451        assert!(matches!(second, Err(HttpError::TooManyRequests(_))));
1452
1453        let _ = std::fs::remove_dir_all(tenant_dir);
1454    }
1455
1456    #[test]
1457    fn tenant_routing_evicts_idle_lru_database_and_reopens_from_disk() {
1458        let tenant_dir =
1459            std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1460        let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1461            .with_max_tenant_dbs(1);
1462
1463        let tenant_a = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1464        tenant_a
1465            .customers()
1466            .create(CreateCustomer {
1467                email: "tenant-a@example.com".into(),
1468                first_name: "Tenant".into(),
1469                last_name: "A".into(),
1470                ..Default::default()
1471            })
1472            .unwrap();
1473        drop(tenant_a);
1474
1475        assert!(state.commerce_for_tenant(Some("tenant-b")).is_ok());
1476
1477        let tenant_a_reloaded = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1478        let tenant_a_customer_count =
1479            tenant_a_reloaded.customers().list(Default::default()).unwrap().len();
1480        assert_eq!(tenant_a_customer_count, 1);
1481
1482        let _ = std::fs::remove_dir_all(tenant_dir);
1483    }
1484
1485    #[test]
1486    fn tenant_cache_metrics_track_hits_misses_evictions_and_rejections() {
1487        let tenant_dir =
1488            std::env::temp_dir().join(format!("stateset-http-state-{}", Uuid::new_v4()));
1489        let state = AppState::new_with_tenant_db_dir(test_commerce(), Some(tenant_dir.clone()))
1490            .with_max_tenant_dbs(1);
1491
1492        let tenant_a = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1493        let tenant_a_again = state.commerce_for_tenant(Some("tenant-a")).unwrap();
1494        drop(tenant_a_again);
1495
1496        let tenant_b_while_a_in_use = state.commerce_for_tenant(Some("tenant-b"));
1497        assert!(matches!(tenant_b_while_a_in_use, Err(HttpError::TooManyRequests(_))));
1498
1499        drop(tenant_a);
1500        let tenant_b_after_release = state.commerce_for_tenant(Some("tenant-b"));
1501        assert!(tenant_b_after_release.is_ok());
1502
1503        let metrics = state.tenant_cache_metrics();
1504        assert!(metrics.enabled);
1505        assert_eq!(metrics.max_cached_dbs, 1);
1506        assert_eq!(metrics.cached_dbs, 1);
1507        assert_eq!(metrics.hits, 1);
1508        assert_eq!(metrics.misses, 3);
1509        assert_eq!(metrics.evictions, 1);
1510        assert_eq!(metrics.rejections, 1);
1511
1512        let _ = std::fs::remove_dir_all(tenant_dir);
1513    }
1514}