Skip to main content

stateset_http/
server.rs

1//! Server builder for configuring and running the HTTP service.
2
3use std::{
4    collections::HashSet,
5    fmt,
6    net::{IpAddr, SocketAddr},
7    path::PathBuf,
8};
9
10use axum::Router;
11use stateset_authz::AuthzEngine;
12use stateset_embedded::Commerce;
13use uuid::Uuid;
14
15use crate::error::HttpError;
16use crate::middleware::{self, AuthzConfig, BearerAuthBinding, RateLimitConfig};
17use crate::routes::{self, DEFAULT_REQUEST_BODY_LIMIT_BYTES};
18use crate::state::{AppState, IpCidr, MetricsHeaderLimits};
19
20/// Default bind address.
21const DEFAULT_ADDR: ([u8; 4], u16) = ([127, 0, 0, 1], 3000);
22const METRICS_IP_ALLOWLIST_ENV: &str = "STATESET_HTTP_METRICS_IP_ALLOWLIST";
23const METRICS_IP_CIDR_ALLOWLIST_ENV: &str = "STATESET_HTTP_METRICS_IP_CIDR_ALLOWLIST";
24const METRICS_TRUSTED_PROXIES_ENV: &str = "STATESET_HTTP_METRICS_TRUSTED_PROXIES";
25const METRICS_FORWARDED_HEADER_MAX_BYTES_ENV: &str = "STATESET_HTTP_METRICS_FORWARDED_MAX_BYTES";
26const METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV: &str =
27    "STATESET_HTTP_METRICS_X_FORWARDED_FOR_MAX_BYTES";
28const METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV: &str = "STATESET_HTTP_METRICS_X_REAL_IP_MAX_BYTES";
29const METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV: &str =
30    "STATESET_HTTP_METRICS_AUTHORIZATION_MAX_BYTES";
31const REQUEST_BODY_MAX_BYTES_ENV: &str = "STATESET_HTTP_MAX_BODY_BYTES";
32const ALLOW_UNAUTHENTICATED_ENV: &str = "STATESET_HTTP_ALLOW_UNAUTHENTICATED";
33const REQUIRE_IDEMPOTENCY_KEYS_ENV: &str = "STATESET_HTTP_REQUIRE_IDEMPOTENCY_KEYS";
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36struct AdditionalApiBearerBinding {
37    token: String,
38    bound_tenant_id: Option<String>,
39    bound_actor_id: Option<String>,
40}
41
42impl AdditionalApiBearerBinding {
43    const fn new(
44        token: String,
45        bound_tenant_id: Option<String>,
46        bound_actor_id: Option<String>,
47    ) -> Self {
48        Self { token, bound_tenant_id, bound_actor_id }
49    }
50}
51
52fn parse_ip_allowlist_csv(env_var: &str, value: &str) -> Result<Vec<IpAddr>, HttpError> {
53    let mut ips = value
54        .split(',')
55        .map(str::trim)
56        .filter(|entry| !entry.is_empty())
57        .map(|entry| {
58            entry.parse::<IpAddr>().map_err(|error| {
59                HttpError::BadRequest(format!("invalid IP '{entry}' in {env_var}: {error}"))
60            })
61        })
62        .collect::<Result<Vec<_>, _>>()?;
63    ips.sort_unstable();
64    ips.dedup();
65    Ok(ips)
66}
67
68fn parse_ip_cidr_allowlist_csv(env_var: &str, value: &str) -> Result<Vec<IpCidr>, HttpError> {
69    let mut cidrs = value
70        .split(',')
71        .map(str::trim)
72        .filter(|entry| !entry.is_empty())
73        .map(|entry| {
74            entry.parse::<IpCidr>().map_err(|error| {
75                HttpError::BadRequest(format!("invalid CIDR '{entry}' in {env_var}: {error}"))
76            })
77        })
78        .collect::<Result<Vec<_>, _>>()?;
79    cidrs.sort_unstable();
80    cidrs.dedup();
81    Ok(cidrs)
82}
83
84fn parse_positive_usize_env(env_var: &str, value: &str) -> Result<usize, HttpError> {
85    let trimmed = value.trim();
86    if trimmed.is_empty() {
87        return Err(HttpError::BadRequest(format!(
88            "{env_var} must be a positive integer greater than zero"
89        )));
90    }
91    let parsed = trimmed.parse::<usize>().map_err(|error| {
92        HttpError::BadRequest(format!("invalid value '{trimmed}' in {env_var}: {error}"))
93    })?;
94    if parsed == 0 {
95        return Err(HttpError::BadRequest(format!("{env_var} must be greater than zero")));
96    }
97    Ok(parsed)
98}
99
100fn parse_bool_env(env_var: &str, value: &str) -> Result<bool, HttpError> {
101    match value.trim().to_ascii_lowercase().as_str() {
102        "1" | "true" | "yes" => Ok(true),
103        "0" | "false" | "no" | "" => Ok(false),
104        other => Err(HttpError::BadRequest(format!(
105            "invalid value '{other}' in {env_var}: expected true/false"
106        ))),
107    }
108}
109
110/// Builder for configuring and launching the HTTP server.
111///
112/// # Example
113///
114/// ```rust,ignore
115/// use stateset_embedded::Commerce;
116/// use stateset_http::ServerBuilder;
117///
118/// let commerce = Commerce::new(":memory:")?;
119///
120/// ServerBuilder::new_from_env(commerce)?
121///     .bind("0.0.0.0:8080".parse()?)
122///     .with_cors()
123///     .with_request_id()
124///     .with_bearer_auth("replace-me-with-a-secret")
125///     .serve()
126///     .await?;
127/// ```
128pub struct ServerBuilder {
129    state: AppState,
130    addr: SocketAddr,
131    enable_cors: bool,
132    enable_request_id: bool,
133    api_bearer_token: Option<String>,
134    bound_tenant_id: Option<String>,
135    bound_actor_id: Option<String>,
136    additional_api_bearer_bindings: Vec<AdditionalApiBearerBinding>,
137    generated_default_token: bool,
138    max_request_body_bytes: usize,
139    authz_config: Option<AuthzConfig>,
140    trust_actor_headers_for_authz: bool,
141    authz_strict: bool,
142    allow_unauthenticated: bool,
143    rate_limit: Option<RateLimitConfig>,
144    require_idempotency_keys: bool,
145}
146
147impl fmt::Debug for ServerBuilder {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.debug_struct("ServerBuilder")
150            .field("state", &"AppState { .. }")
151            .field("addr", &self.addr)
152            .field("enable_cors", &self.enable_cors)
153            .field("enable_request_id", &self.enable_request_id)
154            .field("api_bearer_token", &self.api_bearer_token.as_ref().map(|_| "<redacted>"))
155            .field(
156                "metrics_bearer_token",
157                &self.state.metrics_bearer_auth_token().map(|_| "<redacted>"),
158            )
159            .field("bound_tenant_id", &self.bound_tenant_id.as_ref().map(|_| "<redacted>"))
160            .field("bound_actor_id", &self.bound_actor_id.as_ref().map(|_| "<redacted>"))
161            .field("additional_api_bearer_bindings", &self.additional_api_bearer_bindings.len())
162            .field("generated_default_token", &self.generated_default_token)
163            .field("max_request_body_bytes", &self.max_request_body_bytes)
164            .field("authz_enabled", &self.authz_config.is_some())
165            .field("trust_actor_headers_for_authz", &self.trust_actor_headers_for_authz)
166            .field("authz_strict", &self.authz_strict)
167            .field("allow_unauthenticated", &self.allow_unauthenticated)
168            .field("rate_limit", &self.rate_limit)
169            .field("require_idempotency_keys", &self.require_idempotency_keys)
170            .finish()
171    }
172}
173
174impl ServerBuilder {
175    fn api_bearer_bindings(&self) -> Vec<AdditionalApiBearerBinding> {
176        let mut bindings = Vec::with_capacity(
177            usize::from(self.api_bearer_token.is_some())
178                + self.additional_api_bearer_bindings.len(),
179        );
180        if let Some(token) = self.api_bearer_token.clone() {
181            bindings.push(AdditionalApiBearerBinding::new(
182                token,
183                self.bound_tenant_id.clone(),
184                self.bound_actor_id.clone(),
185            ));
186        }
187        bindings.extend(self.additional_api_bearer_bindings.iter().cloned());
188        bindings
189    }
190
191    fn tenant_routing_auth_error(&self) -> Option<&'static str> {
192        self.state.tenant_db_dir()?;
193        let bindings = self.api_bearer_bindings();
194        if bindings.is_empty() {
195            return Some("per-tenant database routing requires API auth to remain enabled");
196        }
197        if bindings.iter().any(|binding| binding.bound_tenant_id.is_none()) {
198            return Some(
199                "per-tenant database routing requires binding every API bearer token to a single tenant",
200            );
201        }
202        None
203    }
204
205    fn api_auth_error(&self) -> Option<String> {
206        if let Some(message) = self.tenant_routing_auth_error() {
207            return Some(message.to_string());
208        }
209        let bindings = self.api_bearer_bindings();
210        if bindings.is_empty() {
211            if self.authz_config.is_some() && !self.trust_actor_headers_for_authz {
212                return Some(
213                    "request authorization requires actor-bound API authentication or explicitly trusted x-actor-id headers"
214                        .to_string(),
215                );
216            }
217            if self.bound_actor_id.is_some() {
218                return Some(
219                    "actor-bound API authentication requires API auth to remain enabled"
220                        .to_string(),
221                );
222            }
223            return None;
224        }
225
226        let mut seen_tokens = HashSet::with_capacity(bindings.len());
227        for binding in &bindings {
228            if !seen_tokens.insert(binding.token.clone()) {
229                return Some("duplicate API bearer tokens are not allowed".to_string());
230            }
231            if let Some(bound_actor_id) = binding.bound_actor_id.as_deref() {
232                if !middleware::is_valid_actor_id(bound_actor_id) {
233                    return Some("bound actor ID is invalid".to_string());
234                }
235            }
236        }
237        if self.authz_config.is_some()
238            && !self.trust_actor_headers_for_authz
239            && bindings.iter().any(|binding| binding.bound_actor_id.is_none())
240        {
241            return Some(
242                "request authorization requires binding every API bearer token to a single actor or explicitly trusting x-actor-id headers"
243                    .to_string(),
244            );
245        }
246        None
247    }
248
249    /// Create a new server builder wrapping a [`Commerce`] instance.
250    #[must_use]
251    pub fn new(commerce: Commerce) -> Self {
252        let generated_token = Uuid::new_v4().to_string();
253        Self {
254            state: AppState::new(commerce).with_metrics_bearer_auth(generated_token.clone()),
255            addr: SocketAddr::from(DEFAULT_ADDR),
256            enable_cors: false,
257            enable_request_id: false,
258            // Secure-by-default: API routes require an auth token unless
259            // explicitly disabled.
260            api_bearer_token: Some(generated_token),
261            bound_tenant_id: None,
262            bound_actor_id: None,
263            additional_api_bearer_bindings: Vec::new(),
264            generated_default_token: true,
265            max_request_body_bytes: DEFAULT_REQUEST_BODY_LIMIT_BYTES,
266            authz_config: None,
267            trust_actor_headers_for_authz: false,
268            authz_strict: false,
269            allow_unauthenticated: false,
270            rate_limit: None,
271            // Secure-by-default: money-moving create endpoints require an
272            // Idempotency-Key header (HTTP 428 when missing).
273            require_idempotency_keys: true,
274        }
275    }
276
277    /// Create a new server builder and apply `/metrics` network policy from environment.
278    ///
279    /// This is useful for startup wiring where operators configure network policy via:
280    /// - `STATESET_HTTP_METRICS_IP_ALLOWLIST`
281    /// - `STATESET_HTTP_METRICS_IP_CIDR_ALLOWLIST`
282    /// - `STATESET_HTTP_METRICS_TRUSTED_PROXIES`
283    /// - `STATESET_HTTP_METRICS_FORWARDED_MAX_BYTES`
284    /// - `STATESET_HTTP_METRICS_X_FORWARDED_FOR_MAX_BYTES`
285    /// - `STATESET_HTTP_METRICS_X_REAL_IP_MAX_BYTES`
286    /// - `STATESET_HTTP_METRICS_AUTHORIZATION_MAX_BYTES`
287    /// - `STATESET_HTTP_ALLOW_UNAUTHENTICATED`
288    pub fn new_from_env(commerce: Commerce) -> Result<Self, HttpError> {
289        Self::new(commerce)
290            .with_metrics_network_policy_from_env()?
291            .with_metrics_header_limits_from_env()?
292            .with_request_body_limit_from_env()?
293            .with_allow_unauthenticated_from_env()?
294            .with_require_idempotency_keys_from_env()
295    }
296
297    /// Apply the unauthenticated opt-out from `STATESET_HTTP_ALLOW_UNAUTHENTICATED`.
298    ///
299    /// Accepts `true`/`false` (also `1`/`0`, `yes`/`no`). Unset or empty leaves
300    /// the secure default (`false`) in place.
301    pub fn with_allow_unauthenticated_from_env(mut self) -> Result<Self, HttpError> {
302        if let Ok(raw) = std::env::var(ALLOW_UNAUTHENTICATED_ENV) {
303            self.allow_unauthenticated = parse_bool_env(ALLOW_UNAUTHENTICATED_ENV, &raw)?;
304        }
305        Ok(self)
306    }
307
308    /// Require an `Idempotency-Key` header on money-moving create endpoints
309    /// (`POST /orders`, `/payments`, `/payments/{id}/refund`, `/ap/payments`),
310    /// returning HTTP 428 (Precondition Required) when it is missing.
311    ///
312    /// Defaults to `true`; pass `false` to let existing deployments opt out.
313    #[must_use]
314    pub const fn require_idempotency_keys(mut self, require: bool) -> Self {
315        self.require_idempotency_keys = require;
316        self
317    }
318
319    /// Apply the required-idempotency-key opt-out from
320    /// `STATESET_HTTP_REQUIRE_IDEMPOTENCY_KEYS`.
321    ///
322    /// Accepts `true`/`false` (also `1`/`0`, `yes`/`no`). Unset or empty leaves
323    /// the secure default (`true`) in place.
324    pub fn with_require_idempotency_keys_from_env(self) -> Result<Self, HttpError> {
325        let raw = std::env::var(REQUIRE_IDEMPOTENCY_KEYS_ENV).ok();
326        self.with_require_idempotency_keys_from_value(raw.as_deref())
327    }
328
329    /// Apply the required-idempotency-key setting from an optional raw value.
330    fn with_require_idempotency_keys_from_value(
331        mut self,
332        raw: Option<&str>,
333    ) -> Result<Self, HttpError> {
334        if let Some(raw) = raw {
335            self.require_idempotency_keys = parse_bool_env(REQUIRE_IDEMPOTENCY_KEYS_ENV, raw)?;
336        }
337        Ok(self)
338    }
339
340    /// Set the bind address.
341    #[must_use]
342    pub const fn bind(mut self, addr: SocketAddr) -> Self {
343        self.addr = addr;
344        self
345    }
346
347    /// Enable CORS middleware with explicit defaults and optional env override.
348    ///
349    /// Set `STATESET_HTTP_ALLOWED_ORIGINS` to a comma-separated list of origins
350    /// to override the localhost-only defaults.
351    #[must_use]
352    pub const fn with_cors(mut self) -> Self {
353        self.enable_cors = true;
354        self
355    }
356
357    /// Enable request-ID middleware (generates UUID, propagates in headers).
358    #[must_use]
359    pub const fn with_request_id(mut self) -> Self {
360        self.enable_request_id = true;
361        self
362    }
363
364    /// Configure bearer authentication for `/api/v1/*` endpoints.
365    ///
366    /// Requests to API routes must include `Authorization: Bearer <token>`.
367    #[must_use]
368    pub fn with_bearer_auth(mut self, token: impl Into<String>) -> Self {
369        self.api_bearer_token = Some(token.into());
370        self.generated_default_token = false;
371        self
372    }
373
374    fn push_bearer_auth_binding(
375        &mut self,
376        token: impl Into<String>,
377        bound_tenant_id: Option<String>,
378        bound_actor_id: Option<String>,
379    ) {
380        self.additional_api_bearer_bindings.push(AdditionalApiBearerBinding::new(
381            token.into(),
382            bound_tenant_id,
383            bound_actor_id,
384        ));
385    }
386
387    /// Configure bearer authentication for `/metrics`.
388    #[must_use]
389    pub fn with_metrics_bearer_auth(mut self, token: impl Into<String>) -> Self {
390        self.state = self.state.with_metrics_bearer_auth(token);
391        self
392    }
393
394    /// Disable authentication for `/metrics`.
395    #[must_use]
396    pub fn without_metrics_auth(mut self) -> Self {
397        self.state = self.state.without_metrics_auth();
398        self
399    }
400
401    /// Configure a client IP allowlist for `/metrics`.
402    #[must_use]
403    pub fn with_metrics_ip_allowlist<I>(mut self, ips: I) -> Self
404    where
405        I: IntoIterator<Item = IpAddr>,
406    {
407        self.state = self.state.with_metrics_ip_allowlist(ips);
408        self
409    }
410
411    /// Disable `/metrics` IP allowlist checks.
412    #[must_use]
413    pub fn without_metrics_ip_allowlist(mut self) -> Self {
414        self.state = self.state.without_metrics_ip_allowlist();
415        self
416    }
417
418    /// Configure a CIDR-based client IP allowlist for `/metrics`.
419    #[must_use]
420    pub fn with_metrics_ip_cidr_allowlist<I>(mut self, cidrs: I) -> Self
421    where
422        I: IntoIterator<Item = IpCidr>,
423    {
424        self.state = self.state.with_metrics_ip_cidr_allowlist(cidrs);
425        self
426    }
427
428    /// Disable CIDR-based `/metrics` IP allowlist checks.
429    #[must_use]
430    pub fn without_metrics_ip_cidr_allowlist(mut self) -> Self {
431        self.state = self.state.without_metrics_ip_cidr_allowlist();
432        self
433    }
434
435    /// Configure trusted proxy CIDRs for `/metrics`.
436    #[must_use]
437    pub fn with_metrics_trusted_proxies<I>(mut self, cidrs: I) -> Self
438    where
439        I: IntoIterator<Item = IpCidr>,
440    {
441        self.state = self.state.with_metrics_trusted_proxies(cidrs);
442        self
443    }
444
445    /// Configure max accepted forwarding header lengths for `/metrics`.
446    #[must_use]
447    pub fn with_metrics_header_limits(mut self, limits: MetricsHeaderLimits) -> Self {
448        self.state = self.state.with_metrics_header_limits(limits);
449        self
450    }
451
452    /// Apply `/metrics` network policy from environment variables.
453    ///
454    /// Supported variables (comma-separated entries):
455    /// - `STATESET_HTTP_METRICS_IP_ALLOWLIST` (exact IPs)
456    /// - `STATESET_HTTP_METRICS_IP_CIDR_ALLOWLIST` (CIDRs and/or IPs)
457    /// - `STATESET_HTTP_METRICS_TRUSTED_PROXIES` (CIDRs and/or IPs)
458    ///
459    /// If a variable is present but empty, the corresponding config is disabled.
460    pub fn with_metrics_network_policy_from_env(self) -> Result<Self, HttpError> {
461        let ip_allowlist = std::env::var(METRICS_IP_ALLOWLIST_ENV).ok();
462        let ip_cidr_allowlist = std::env::var(METRICS_IP_CIDR_ALLOWLIST_ENV).ok();
463        let trusted_proxies = std::env::var(METRICS_TRUSTED_PROXIES_ENV).ok();
464        self.with_metrics_network_policy_from_values(
465            ip_allowlist.as_deref(),
466            ip_cidr_allowlist.as_deref(),
467            trusted_proxies.as_deref(),
468        )
469    }
470
471    /// Apply `/metrics` forwarding header limits from environment variables.
472    ///
473    /// Supported variables (positive integers in bytes):
474    /// - `STATESET_HTTP_METRICS_FORWARDED_MAX_BYTES`
475    /// - `STATESET_HTTP_METRICS_X_FORWARDED_FOR_MAX_BYTES`
476    /// - `STATESET_HTTP_METRICS_X_REAL_IP_MAX_BYTES`
477    /// - `STATESET_HTTP_METRICS_AUTHORIZATION_MAX_BYTES`
478    pub fn with_metrics_header_limits_from_env(self) -> Result<Self, HttpError> {
479        let forwarded = std::env::var(METRICS_FORWARDED_HEADER_MAX_BYTES_ENV).ok();
480        let x_forwarded_for = std::env::var(METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV).ok();
481        let x_real_ip = std::env::var(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV).ok();
482        let authorization = std::env::var(METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV).ok();
483        self.with_metrics_header_limits_from_values(
484            forwarded.as_deref(),
485            x_forwarded_for.as_deref(),
486            x_real_ip.as_deref(),
487            authorization.as_deref(),
488        )
489    }
490
491    fn with_metrics_network_policy_from_values(
492        mut self,
493        ip_allowlist: Option<&str>,
494        ip_cidr_allowlist: Option<&str>,
495        trusted_proxies: Option<&str>,
496    ) -> Result<Self, HttpError> {
497        if let Some(raw) = ip_allowlist {
498            let ips = parse_ip_allowlist_csv(METRICS_IP_ALLOWLIST_ENV, raw)?;
499            self = if ips.is_empty() {
500                self.without_metrics_ip_allowlist()
501            } else {
502                self.with_metrics_ip_allowlist(ips)
503            };
504        }
505
506        if let Some(raw) = ip_cidr_allowlist {
507            let cidrs = parse_ip_cidr_allowlist_csv(METRICS_IP_CIDR_ALLOWLIST_ENV, raw)?;
508            self = if cidrs.is_empty() {
509                self.without_metrics_ip_cidr_allowlist()
510            } else {
511                self.with_metrics_ip_cidr_allowlist(cidrs)
512            };
513        }
514
515        if let Some(raw) = trusted_proxies {
516            let cidrs = parse_ip_cidr_allowlist_csv(METRICS_TRUSTED_PROXIES_ENV, raw)?;
517            self = if cidrs.is_empty() {
518                self.without_metrics_trusted_proxies()
519            } else {
520                self.with_metrics_trusted_proxies(cidrs)
521            };
522        }
523
524        Ok(self)
525    }
526
527    fn with_metrics_header_limits_from_values(
528        self,
529        forwarded: Option<&str>,
530        x_forwarded_for: Option<&str>,
531        x_real_ip: Option<&str>,
532        authorization: Option<&str>,
533    ) -> Result<Self, HttpError> {
534        let current = self.metrics_header_limits();
535        let forwarded = if let Some(raw) = forwarded {
536            parse_positive_usize_env(METRICS_FORWARDED_HEADER_MAX_BYTES_ENV, raw)?
537        } else {
538            current.forwarded_header_value_bytes()
539        };
540        let x_forwarded_for = if let Some(raw) = x_forwarded_for {
541            parse_positive_usize_env(METRICS_X_FORWARDED_FOR_HEADER_MAX_BYTES_ENV, raw)?
542        } else {
543            current.x_forwarded_for_header_value_bytes()
544        };
545        let x_real_ip = if let Some(raw) = x_real_ip {
546            parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, raw)?
547        } else {
548            current.x_real_ip_header_value_bytes()
549        };
550        let authorization = if let Some(raw) = authorization {
551            parse_positive_usize_env(METRICS_AUTHORIZATION_HEADER_MAX_BYTES_ENV, raw)?
552        } else {
553            current.authorization_header_value_bytes()
554        };
555        let limits = MetricsHeaderLimits::new_with_authorization(
556            forwarded,
557            x_forwarded_for,
558            x_real_ip,
559            authorization,
560        )?;
561        Ok(self.with_metrics_header_limits(limits))
562    }
563
564    /// Apply the request body size limit from `STATESET_HTTP_MAX_BODY_BYTES`.
565    pub fn with_request_body_limit_from_env(self) -> Result<Self, HttpError> {
566        let max_body_bytes = std::env::var(REQUEST_BODY_MAX_BYTES_ENV).ok();
567        self.with_request_body_limit_from_value(max_body_bytes.as_deref())
568    }
569
570    fn with_request_body_limit_from_value(
571        mut self,
572        max_body_bytes: Option<&str>,
573    ) -> Result<Self, HttpError> {
574        if let Some(raw) = max_body_bytes {
575            self.max_request_body_bytes =
576                parse_positive_usize_env(REQUEST_BODY_MAX_BYTES_ENV, raw)?;
577        }
578        Ok(self)
579    }
580
581    /// Disable trusted proxy checks for `/metrics`.
582    #[must_use]
583    pub fn without_metrics_trusted_proxies(mut self) -> Self {
584        self.state = self.state.without_metrics_trusted_proxies();
585        self
586    }
587
588    /// Bind the configured bearer token to a single tenant.
589    ///
590    /// Requests using this token must present the same `x-tenant-id`.
591    #[must_use]
592    pub fn bind_auth_tenant(mut self, tenant_id: impl Into<String>) -> Self {
593        self.bound_tenant_id = Some(tenant_id.into());
594        self
595    }
596
597    /// Bind the configured bearer token to a single actor.
598    ///
599    /// When set, authenticated API requests are treated as this actor during
600    /// authorization checks, and any conflicting `x-actor-id` header is rejected.
601    #[must_use]
602    pub fn bind_auth_actor(mut self, actor_id: impl Into<String>) -> Self {
603        self.bound_actor_id = Some(actor_id.into());
604        self
605    }
606
607    /// Configure bearer authentication and bind it to a tenant in one call.
608    #[must_use]
609    pub fn with_bearer_auth_for_tenant(
610        self,
611        token: impl Into<String>,
612        tenant_id: impl Into<String>,
613    ) -> Self {
614        self.with_bearer_auth(token).bind_auth_tenant(tenant_id)
615    }
616
617    /// Configure bearer authentication and bind it to an actor in one call.
618    #[must_use]
619    pub fn with_bearer_auth_for_actor(
620        self,
621        token: impl Into<String>,
622        actor_id: impl Into<String>,
623    ) -> Self {
624        self.with_bearer_auth(token).bind_auth_actor(actor_id)
625    }
626
627    /// Add an additional bearer token for `/api/v1/*` endpoints.
628    ///
629    /// This leaves the primary configured API token unchanged.
630    #[must_use]
631    pub fn add_bearer_auth(mut self, token: impl Into<String>) -> Self {
632        self.push_bearer_auth_binding(token, None, None);
633        self
634    }
635
636    /// Add an additional bearer token bound to a tenant.
637    #[must_use]
638    pub fn add_bearer_auth_for_tenant(
639        mut self,
640        token: impl Into<String>,
641        tenant_id: impl Into<String>,
642    ) -> Self {
643        self.push_bearer_auth_binding(token, Some(tenant_id.into()), None);
644        self
645    }
646
647    /// Add an additional bearer token bound to an actor.
648    #[must_use]
649    pub fn add_bearer_auth_for_actor(
650        mut self,
651        token: impl Into<String>,
652        actor_id: impl Into<String>,
653    ) -> Self {
654        self.push_bearer_auth_binding(token, None, Some(actor_id.into()));
655        self
656    }
657
658    /// Add an additional bearer token bound to both an actor and a tenant.
659    #[must_use]
660    pub fn add_bearer_auth_for_actor_and_tenant(
661        mut self,
662        token: impl Into<String>,
663        actor_id: impl Into<String>,
664        tenant_id: impl Into<String>,
665    ) -> Self {
666        self.push_bearer_auth_binding(token, Some(tenant_id.into()), Some(actor_id.into()));
667        self
668    }
669
670    /// Enable per-tenant storage using `<base_dir>/<tenant>.db`.
671    #[must_use]
672    pub fn with_tenant_db_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
673        self.state = self.state.with_tenant_db_dir(base_dir);
674        self
675    }
676
677    /// Ignore `x-tenant-id` headers when per-tenant routing is disabled.
678    ///
679    /// By default such requests are rejected with `400` instead of silently
680    /// being served shared data. Use this escape hatch only for deployments
681    /// that intentionally front a multi-tenant proxy which handles tenant
682    /// isolation upstream.
683    #[must_use]
684    pub fn with_ignore_tenant_header(mut self) -> Self {
685        self.state = self.state.with_ignore_tenant_header();
686        self
687    }
688
689    /// Set the maximum number of lazily created tenant databases kept in-memory.
690    #[must_use]
691    pub fn with_max_tenant_dbs(mut self, max_tenant_dbs: usize) -> Self {
692        self.state = self.state.with_max_tenant_dbs(max_tenant_dbs);
693        self
694    }
695
696    /// Set the maximum accepted request body size for extractor-based endpoints.
697    #[must_use]
698    pub const fn with_max_request_body_bytes(mut self, max_request_body_bytes: usize) -> Self {
699        self.max_request_body_bytes =
700            if max_request_body_bytes == 0 { 1 } else { max_request_body_bytes };
701        self
702    }
703
704    /// Enable request authorization for `/api/v1/*` using a provided authz engine.
705    ///
706    /// By default, authorization expects actor identity to come from actor-bound
707    /// bearer tokens. If you want to trust `x-actor-id` request headers instead,
708    /// also call [`Self::trust_actor_headers_for_authz`].
709    #[must_use]
710    pub fn with_authz_engine(mut self, engine: AuthzEngine) -> Self {
711        self.authz_config = Some(AuthzConfig::new(engine));
712        self
713    }
714
715    /// Explicitly trust `x-actor-id` request headers for authorization.
716    ///
717    /// Use this only behind a trusted upstream that authenticates callers and
718    /// strips or overwrites any client-supplied actor header.
719    #[must_use]
720    pub const fn trust_actor_headers_for_authz(mut self) -> Self {
721        self.trust_actor_headers_for_authz = true;
722        self
723    }
724
725    /// Explicitly opt out of the fail-closed authentication startup check.
726    ///
727    /// By default the server refuses to start on a non-loopback bind address
728    /// when no API bearer tokens are configured. Call this (or set
729    /// `STATESET_HTTP_ALLOW_UNAUTHENTICATED=true`) to acknowledge the risk and
730    /// serve unauthenticated API traffic anyway.
731    #[must_use]
732    pub const fn allow_unauthenticated(mut self) -> Self {
733        self.allow_unauthenticated = true;
734        self
735    }
736
737    /// Fail closed on authorization for unmapped API paths.
738    ///
739    /// By default, `/api/v1` paths that the authorization layer cannot map to
740    /// a resource/action pair bypass authorization (authentication still
741    /// applies). When strict mode is enabled, such requests are denied with
742    /// HTTP 403 instead. Only takes effect when authorization is configured
743    /// via [`Self::with_authz_engine`].
744    #[must_use]
745    pub const fn with_strict_authz(mut self) -> Self {
746        self.authz_strict = true;
747        self
748    }
749
750    /// Disable API authentication (not recommended for untrusted networks).
751    #[must_use]
752    pub fn without_auth(mut self) -> Self {
753        self.api_bearer_token = None;
754        self.bound_tenant_id = None;
755        self.bound_actor_id = None;
756        self.additional_api_bearer_bindings.clear();
757        self.generated_default_token = false;
758        self
759    }
760
761    /// Enable global rate limiting with a token bucket.
762    ///
763    /// `requests_per_second` controls steady-state throughput; `burst_size`
764    /// controls how many requests can be served in a burst before throttling.
765    /// Requests exceeding the limit receive HTTP 429.
766    #[must_use]
767    pub const fn with_rate_limit(mut self, requests_per_second: u64, burst_size: u64) -> Self {
768        self.rate_limit = Some(RateLimitConfig { requests_per_second, burst_size });
769        self
770    }
771
772    /// Return the configured bearer token, if auth is enabled.
773    #[must_use]
774    pub fn bearer_auth_token(&self) -> Option<&str> {
775        self.api_bearer_token.as_deref()
776    }
777
778    /// Return the configured bearer token for `/metrics`, if enabled.
779    #[must_use]
780    pub fn metrics_bearer_auth_token(&self) -> Option<&str> {
781        self.state.metrics_bearer_auth_token()
782    }
783
784    /// Return configured metrics IP allowlist entries, if any.
785    #[must_use]
786    pub fn metrics_ip_allowlist(&self) -> Option<Vec<IpAddr>> {
787        self.state.metrics_ip_allowlist()
788    }
789
790    /// Return configured CIDR-based metrics IP allowlist entries, if any.
791    #[must_use]
792    pub fn metrics_ip_cidr_allowlist(&self) -> Option<Vec<IpCidr>> {
793        self.state.metrics_ip_cidr_allowlist()
794    }
795
796    /// Return configured trusted proxy CIDRs for `/metrics`, if any.
797    #[must_use]
798    pub fn metrics_trusted_proxies(&self) -> Option<Vec<IpCidr>> {
799        self.state.metrics_trusted_proxies()
800    }
801
802    /// Return configured max accepted forwarding header lengths for `/metrics`.
803    #[must_use]
804    pub const fn metrics_header_limits(&self) -> MetricsHeaderLimits {
805        self.state.metrics_header_limits()
806    }
807
808    /// Build the axum [`Router`] without starting the server.
809    ///
810    /// Useful for testing or embedding in a larger application.
811    pub fn build(self) -> Router {
812        let misconfigured_auth_message = self.api_auth_error();
813        let auth_bindings = self
814            .api_bearer_bindings()
815            .into_iter()
816            .map(|binding| {
817                BearerAuthBinding::new(
818                    binding.token,
819                    binding.bound_tenant_id,
820                    binding.bound_actor_id,
821                )
822            })
823            .collect::<Vec<_>>();
824        let auth_config = if auth_bindings.is_empty() { None } else { Some(auth_bindings) };
825        let trust_actor_headers_for_authz = self.trust_actor_headers_for_authz;
826        let authz_strict = self.authz_strict;
827        let authz_config = self.authz_config.map(|config| {
828            let config = if trust_actor_headers_for_authz {
829                config.with_trusted_actor_headers()
830            } else {
831                config
832            };
833            if authz_strict { config.with_strict_path_mapping() } else { config }
834        });
835        // Durable, database-backed idempotency store (with the in-memory map as
836        // a read-through cache) plus the required-key gate for money-moving
837        // create endpoints.
838        let mut idempotency_layer = crate::idempotency::IdempotencyLayer::new()
839            .with_required_keys(self.require_idempotency_keys);
840        if let Ok(commerce) = self.state.commerce_for_tenant(None) {
841            idempotency_layer = idempotency_layer.with_durable_store(commerce);
842        }
843        let router =
844            routes::api_router_with_idempotency(self.max_request_body_bytes, idempotency_layer)
845                .with_state(self.state);
846        middleware::apply_middleware(
847            router,
848            self.enable_cors,
849            self.enable_request_id,
850            auth_config,
851            authz_config,
852            misconfigured_auth_message,
853            self.rate_limit,
854        )
855    }
856
857    /// Build the router and start serving HTTP requests.
858    ///
859    /// This method will block until the server is shut down.
860    pub async fn serve(self) -> Result<(), HttpError> {
861        let token = self.api_bearer_token.clone();
862        let api_token_count = self.api_bearer_bindings().len();
863        let metrics_token = self.state.metrics_bearer_auth_token().map(ToOwned::to_owned);
864        let trusted_proxy_count = self.state.metrics_trusted_proxies().map_or(0, |v| v.len());
865        let metrics_header_limits = self.state.metrics_header_limits();
866        let bound_tenant_id = self.bound_tenant_id.clone();
867        let bound_actor_id = self.bound_actor_id.clone();
868        let generated_default_token = self.generated_default_token;
869        let authz_enabled = self.authz_config.is_some();
870        let rate_limit_enabled = self.rate_limit.is_some();
871        let trust_actor_headers_for_authz = self.trust_actor_headers_for_authz;
872        let addr = self.addr;
873
874        if api_token_count == 0 && !self.allow_unauthenticated {
875            if addr.ip().is_loopback() {
876                tracing::warn!(
877                    "No API authentication configured on a loopback bind. Configure a bearer \
878                     token with ServerBuilder::with_bearer_auth before exposing this server."
879                );
880            } else {
881                return Err(HttpError::BadRequest(format!(
882                    "Refusing to start without API authentication on non-loopback address \
883                     {addr}. Configure a bearer token with ServerBuilder::with_bearer_auth (or \
884                     add_bearer_auth_for_actor/add_bearer_auth_for_tenant), or explicitly opt \
885                     out with ServerBuilder::allow_unauthenticated() or \
886                     {ALLOW_UNAUTHENTICATED_ENV}=true."
887                )));
888            }
889        }
890
891        if let Some(message) = self.api_auth_error() {
892            return Err(HttpError::BadRequest(format!("Refusing to start: {message}")));
893        }
894
895        let app = self.build();
896
897        tracing::info!("StateSet HTTP listening on {addr}");
898        if let Some(token) = token.as_deref() {
899            tracing::info!("API bearer authentication is enabled for /api/v1/*");
900            if api_token_count > 1 {
901                tracing::info!(api_token_count, "Multiple API bearer tokens are configured");
902            }
903            if let Some(bound_tenant_id) = bound_tenant_id.as_deref() {
904                tracing::info!(
905                    tenant_id = %bound_tenant_id,
906                    "API token is bound to a specific tenant"
907                );
908            }
909            if let Some(bound_actor_id) = bound_actor_id.as_deref() {
910                tracing::info!(
911                    actor_id = %bound_actor_id,
912                    "API token is bound to a specific actor"
913                );
914            }
915            if generated_default_token {
916                tracing::warn!(
917                    "Using generated bearer token. Persist it and rotate for production deployments."
918                );
919                let preview: String = token.chars().take(8).collect();
920                tracing::info!(
921                    token_preview = %preview,
922                    token_length = token.len(),
923                    "Generated API bearer token (redacted preview)"
924                );
925            }
926        } else if api_token_count > 0 {
927            tracing::info!("API bearer authentication is enabled for /api/v1/*");
928            tracing::info!(api_token_count, "Multiple API bearer tokens are configured");
929        } else {
930            tracing::warn!("API authentication is disabled for /api/v1/*");
931        }
932        if authz_enabled && bound_actor_id.is_none() && trust_actor_headers_for_authz {
933            tracing::warn!(
934                "Request authorization is enabled for /api/v1/*; ensure x-actor-id is set by a trusted upstream"
935            );
936        }
937        if !addr.ip().is_loopback() {
938            // Production baseline: authentication alone leaves every valid
939            // token with full API access, and no throttle at all.
940            if !authz_enabled {
941                tracing::warn!(
942                    "No authorization (RBAC) configured on a non-loopback bind: any valid \
943                     bearer token has full access to every /api/v1 route. Configure \
944                     ServerBuilder::with_authz for production deployments."
945                );
946            }
947            if !rate_limit_enabled {
948                tracing::warn!(
949                    "No rate limiting configured on a non-loopback bind. Configure \
950                     ServerBuilder::with_rate_limit for production deployments."
951                );
952            }
953        }
954
955        if metrics_token.is_some() {
956            tracing::info!("Metrics bearer authentication is enabled for /metrics");
957        } else if !addr.ip().is_loopback() {
958            tracing::warn!(
959                "Metrics authentication is disabled for /metrics on a non-loopback bind"
960            );
961        }
962        if trusted_proxy_count > 0 {
963            tracing::info!(
964                trusted_proxy_count,
965                "Metrics forwarded headers are trusted only for configured proxy CIDRs"
966            );
967        }
968        tracing::info!(
969            forwarded_header_max_bytes = metrics_header_limits.forwarded_header_value_bytes(),
970            x_forwarded_for_header_max_bytes =
971                metrics_header_limits.x_forwarded_for_header_value_bytes(),
972            x_real_ip_header_max_bytes = metrics_header_limits.x_real_ip_header_value_bytes(),
973            authorization_header_max_bytes =
974                metrics_header_limits.authorization_header_value_bytes(),
975            "Metrics header byte limits configured"
976        );
977
978        let listener = tokio::net::TcpListener::bind(addr)
979            .await
980            .map_err(|e| HttpError::InternalError(format!("Failed to bind: {e}")))?;
981
982        axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
983            .await
984            .map_err(|e| HttpError::InternalError(format!("Server error: {e}")))?;
985
986        Ok(())
987    }
988}
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993
994    use axum::body::Body;
995    use axum::http::{Request, StatusCode};
996    use stateset_authz::{AuthzEngineBuilder, Role};
997    use tower::ServiceExt;
998    use uuid::Uuid;
999
1000    fn test_commerce() -> Commerce {
1001        Commerce::new(":memory:").expect("in-memory Commerce")
1002    }
1003
1004    #[test]
1005    fn builder_default_addr() {
1006        let builder = ServerBuilder::new(test_commerce());
1007        assert_eq!(builder.addr, SocketAddr::from(DEFAULT_ADDR));
1008        assert!(!builder.enable_cors);
1009        assert!(!builder.enable_request_id);
1010        assert!(builder.api_bearer_token.is_some());
1011        assert!(builder.metrics_bearer_auth_token().is_some());
1012        assert_eq!(builder.metrics_header_limits(), MetricsHeaderLimits::default());
1013        assert!(builder.bound_tenant_id.is_none());
1014        assert!(builder.bound_actor_id.is_none());
1015        assert!(builder.additional_api_bearer_bindings.is_empty());
1016        assert_eq!(builder.max_request_body_bytes, DEFAULT_REQUEST_BODY_LIMIT_BYTES);
1017        assert!(builder.authz_config.is_none());
1018        assert!(!builder.trust_actor_headers_for_authz);
1019    }
1020
1021    #[test]
1022    fn builder_with_bind() {
1023        let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
1024        let builder = ServerBuilder::new(test_commerce()).bind(addr);
1025        assert_eq!(builder.addr, addr);
1026    }
1027
1028    #[test]
1029    fn builder_with_cors() {
1030        let builder = ServerBuilder::new(test_commerce()).with_cors();
1031        assert!(builder.enable_cors);
1032    }
1033
1034    #[test]
1035    fn builder_with_request_id() {
1036        let builder = ServerBuilder::new(test_commerce()).with_request_id();
1037        assert!(builder.enable_request_id);
1038    }
1039
1040    #[test]
1041    fn builder_with_bearer_auth() {
1042        let builder = ServerBuilder::new(test_commerce());
1043        let default_metrics_token = builder
1044            .metrics_bearer_auth_token()
1045            .expect("builder should start with metrics auth")
1046            .to_string();
1047        let builder = builder.with_bearer_auth("test-token");
1048        assert_eq!(builder.bearer_auth_token(), Some("test-token"));
1049        assert_eq!(builder.metrics_bearer_auth_token(), Some(default_metrics_token.as_str()));
1050        assert!(builder.bound_tenant_id.is_none());
1051        assert!(builder.bound_actor_id.is_none());
1052    }
1053
1054    #[test]
1055    fn builder_with_metrics_bearer_auth() {
1056        let builder = ServerBuilder::new(test_commerce()).with_metrics_bearer_auth("metrics-token");
1057        assert_eq!(builder.metrics_bearer_auth_token(), Some("metrics-token"));
1058    }
1059
1060    #[test]
1061    fn builder_with_bearer_auth_keeps_explicit_metrics_token() {
1062        let builder = ServerBuilder::new(test_commerce())
1063            .with_metrics_bearer_auth("metrics-token")
1064            .with_bearer_auth("api-token");
1065        assert_eq!(builder.bearer_auth_token(), Some("api-token"));
1066        assert_eq!(builder.metrics_bearer_auth_token(), Some("metrics-token"));
1067    }
1068
1069    #[test]
1070    fn builder_with_metrics_ip_allowlist() {
1071        let builder = ServerBuilder::new(test_commerce())
1072            .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap(), "10.0.0.1".parse().unwrap()]);
1073
1074        let allowlist = builder.metrics_ip_allowlist().unwrap();
1075        assert_eq!(allowlist.len(), 2);
1076    }
1077
1078    #[test]
1079    fn parse_ip_allowlist_csv_parses_and_sorts_unique_values() {
1080        let parsed =
1081            parse_ip_allowlist_csv(METRICS_IP_ALLOWLIST_ENV, "127.0.0.1, 10.0.0.1,127.0.0.1")
1082                .unwrap();
1083        assert_eq!(
1084            parsed,
1085            vec!["10.0.0.1".parse::<IpAddr>().unwrap(), "127.0.0.1".parse::<IpAddr>().unwrap()]
1086        );
1087    }
1088
1089    #[test]
1090    fn parse_ip_cidr_allowlist_csv_rejects_invalid_entries() {
1091        let err = parse_ip_cidr_allowlist_csv(METRICS_IP_CIDR_ALLOWLIST_ENV, "10.0.0.0/8,bogus")
1092            .expect_err("should reject invalid CIDR");
1093        assert!(err.to_string().contains(METRICS_IP_CIDR_ALLOWLIST_ENV));
1094    }
1095
1096    #[test]
1097    fn builder_with_metrics_ip_cidr_allowlist() {
1098        let builder = ServerBuilder::new(test_commerce()).with_metrics_ip_cidr_allowlist([
1099            "10.0.0.0/8".parse().unwrap(),
1100            "127.0.0.1".parse().unwrap(),
1101        ]);
1102
1103        let cidrs = builder.metrics_ip_cidr_allowlist().unwrap();
1104        assert_eq!(cidrs, vec!["10.0.0.0/8".parse().unwrap(), "127.0.0.1/32".parse().unwrap()]);
1105    }
1106
1107    #[test]
1108    fn builder_with_metrics_trusted_proxies() {
1109        let builder = ServerBuilder::new(test_commerce())
1110            .with_metrics_trusted_proxies(["10.0.0.0/8".parse().unwrap()]);
1111        let proxies = builder.metrics_trusted_proxies().unwrap();
1112        assert_eq!(proxies, vec!["10.0.0.0/8".parse().unwrap()]);
1113    }
1114
1115    #[test]
1116    fn builder_with_metrics_header_limits() {
1117        let limits = MetricsHeaderLimits::new(1024, 1536, 256).unwrap();
1118        let builder = ServerBuilder::new(test_commerce()).with_metrics_header_limits(limits);
1119        assert_eq!(builder.metrics_header_limits(), limits);
1120    }
1121
1122    #[test]
1123    fn builder_with_metrics_network_policy_from_env() {
1124        let builder = ServerBuilder::new(test_commerce())
1125            .with_metrics_network_policy_from_values(
1126                Some("127.0.0.1"),
1127                Some("10.0.0.0/8"),
1128                Some("10.1.0.0/16"),
1129            )
1130            .expect("policy should parse");
1131
1132        assert_eq!(builder.metrics_ip_allowlist(), Some(vec!["127.0.0.1".parse().unwrap()]));
1133        assert_eq!(builder.metrics_ip_cidr_allowlist(), Some(vec!["10.0.0.0/8".parse().unwrap()]));
1134        assert_eq!(builder.metrics_trusted_proxies(), Some(vec!["10.1.0.0/16".parse().unwrap()]));
1135    }
1136
1137    #[test]
1138    fn builder_with_metrics_network_policy_from_env_can_disable_lists_with_empty_values() {
1139        let builder = ServerBuilder::new(test_commerce())
1140            .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap()])
1141            .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()])
1142            .with_metrics_trusted_proxies(["10.1.0.0/16".parse().unwrap()])
1143            .with_metrics_network_policy_from_values(Some("   "), Some(","), Some("  ,  "))
1144            .expect("empty policy values should disable lists");
1145
1146        assert!(builder.metrics_ip_allowlist().is_none());
1147        assert!(builder.metrics_ip_cidr_allowlist().is_none());
1148        assert!(builder.metrics_trusted_proxies().is_none());
1149    }
1150
1151    #[test]
1152    fn parse_positive_usize_env_rejects_zero_and_empty_values() {
1153        let zero =
1154            parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, "0").unwrap_err();
1155        assert!(zero.to_string().contains("greater than zero"));
1156
1157        let empty =
1158            parse_positive_usize_env(METRICS_X_REAL_IP_HEADER_MAX_BYTES_ENV, "   ").unwrap_err();
1159        assert!(empty.to_string().contains("positive integer"));
1160    }
1161
1162    #[test]
1163    fn builder_with_metrics_header_limits_from_values() {
1164        let builder = ServerBuilder::new(test_commerce())
1165            .with_metrics_header_limits_from_values(
1166                Some("1024"),
1167                Some("1536"),
1168                Some("256"),
1169                Some("768"),
1170            )
1171            .expect("limits should parse");
1172        let limits = builder.metrics_header_limits();
1173        assert_eq!(limits.forwarded_header_value_bytes(), 1024);
1174        assert_eq!(limits.x_forwarded_for_header_value_bytes(), 1536);
1175        assert_eq!(limits.x_real_ip_header_value_bytes(), 256);
1176        assert_eq!(limits.authorization_header_value_bytes(), 768);
1177    }
1178
1179    #[test]
1180    fn builder_with_metrics_header_limits_from_values_applies_partial_overrides() {
1181        let builder = ServerBuilder::new(test_commerce())
1182            .with_metrics_header_limits_from_values(None, Some("4096"), None, None)
1183            .expect("partial limits should parse");
1184        let limits = builder.metrics_header_limits();
1185        assert_eq!(
1186            limits.forwarded_header_value_bytes(),
1187            MetricsHeaderLimits::DEFAULT_FORWARDED_HEADER_VALUE_BYTES
1188        );
1189        assert_eq!(limits.x_forwarded_for_header_value_bytes(), 4096);
1190        assert_eq!(
1191            limits.x_real_ip_header_value_bytes(),
1192            MetricsHeaderLimits::DEFAULT_X_REAL_IP_HEADER_VALUE_BYTES
1193        );
1194        assert_eq!(
1195            limits.authorization_header_value_bytes(),
1196            MetricsHeaderLimits::DEFAULT_AUTHORIZATION_HEADER_VALUE_BYTES
1197        );
1198    }
1199
1200    #[test]
1201    fn builder_with_request_body_limit() {
1202        let builder = ServerBuilder::new(test_commerce()).with_max_request_body_bytes(4096);
1203        assert_eq!(builder.max_request_body_bytes, 4096);
1204    }
1205
1206    #[test]
1207    fn builder_with_request_body_limit_from_value() {
1208        let builder = ServerBuilder::new(test_commerce())
1209            .with_request_body_limit_from_value(Some("8192"))
1210            .expect("request body limit should parse");
1211        assert_eq!(builder.max_request_body_bytes, 8192);
1212    }
1213
1214    #[test]
1215    fn builder_with_authz_engine() {
1216        let engine = AuthzEngineBuilder::new()
1217            .add_role(Role::viewer())
1218            .assign_role("viewer-1", "viewer")
1219            .build();
1220        let builder = ServerBuilder::new(test_commerce()).with_authz_engine(engine);
1221        assert!(builder.authz_config.is_some());
1222    }
1223
1224    #[test]
1225    fn builder_trusts_actor_headers_for_authz() {
1226        let builder = ServerBuilder::new(test_commerce()).trust_actor_headers_for_authz();
1227        assert!(builder.trust_actor_headers_for_authz);
1228    }
1229
1230    #[test]
1231    fn builder_with_bearer_auth_for_actor() {
1232        let builder = ServerBuilder::new(test_commerce())
1233            .with_bearer_auth_for_actor("actor-token", "admin-1");
1234        assert_eq!(builder.bearer_auth_token(), Some("actor-token"));
1235        assert_eq!(builder.bound_actor_id.as_deref(), Some("admin-1"));
1236    }
1237
1238    #[test]
1239    fn builder_adds_additional_actor_bound_token() {
1240        let builder = ServerBuilder::new(test_commerce())
1241            .without_auth()
1242            .add_bearer_auth_for_actor("viewer-token", "viewer-1");
1243        assert!(builder.bearer_auth_token().is_none());
1244        assert_eq!(builder.additional_api_bearer_bindings.len(), 1);
1245        assert_eq!(builder.additional_api_bearer_bindings[0].token, "viewer-token");
1246        assert_eq!(
1247            builder.additional_api_bearer_bindings[0].bound_actor_id.as_deref(),
1248            Some("viewer-1")
1249        );
1250    }
1251
1252    #[test]
1253    fn builder_with_bearer_auth_for_tenant() {
1254        let builder = ServerBuilder::new(test_commerce())
1255            .with_bearer_auth_for_tenant("tenant-token", "tenant-1");
1256        assert_eq!(builder.bearer_auth_token(), Some("tenant-token"));
1257        assert_eq!(builder.bound_tenant_id.as_deref(), Some("tenant-1"));
1258    }
1259
1260    #[test]
1261    fn builder_without_auth() {
1262        let builder = ServerBuilder::new(test_commerce())
1263            .with_bearer_auth_for_tenant("token", "tenant-1")
1264            .bind_auth_actor("admin-1")
1265            .without_auth();
1266        assert!(builder.bearer_auth_token().is_none());
1267        assert!(builder.metrics_bearer_auth_token().is_some());
1268        assert!(builder.bound_tenant_id.is_none());
1269        assert!(builder.bound_actor_id.is_none());
1270    }
1271
1272    #[test]
1273    fn builder_allow_unauthenticated_flag() {
1274        let builder = ServerBuilder::new(test_commerce());
1275        assert!(!builder.allow_unauthenticated, "secure default must be false");
1276        let builder = builder.allow_unauthenticated();
1277        assert!(builder.allow_unauthenticated);
1278    }
1279
1280    #[test]
1281    fn builder_require_idempotency_keys_flag() {
1282        let builder = ServerBuilder::new(test_commerce());
1283        assert!(builder.require_idempotency_keys, "secure default must require keys");
1284        let builder = builder.require_idempotency_keys(false);
1285        assert!(!builder.require_idempotency_keys);
1286    }
1287
1288    #[test]
1289    fn builder_require_idempotency_keys_from_value() {
1290        let builder = ServerBuilder::new(test_commerce())
1291            .with_require_idempotency_keys_from_value(Some("false"))
1292            .expect("false should parse");
1293        assert!(!builder.require_idempotency_keys);
1294
1295        let builder = ServerBuilder::new(test_commerce())
1296            .with_require_idempotency_keys_from_value(None)
1297            .expect("unset keeps default");
1298        assert!(builder.require_idempotency_keys, "unset must keep the secure default");
1299
1300        assert!(
1301            ServerBuilder::new(test_commerce())
1302                .with_require_idempotency_keys_from_value(Some("banana"))
1303                .is_err(),
1304            "invalid values must be rejected"
1305        );
1306    }
1307
1308    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1309    async fn built_server_returns_428_on_keyless_payment_create_by_default() {
1310        use tower::ServiceExt as _;
1311        let app = ServerBuilder::new(test_commerce()).without_auth().build();
1312        let response = app
1313            .oneshot(
1314                axum::http::Request::post("/api/v1/payments")
1315                    .header("content-type", "application/json")
1316                    .body(axum::body::Body::from("{}"))
1317                    .unwrap(),
1318            )
1319            .await
1320            .unwrap();
1321        assert_eq!(response.status(), axum::http::StatusCode::PRECONDITION_REQUIRED);
1322    }
1323
1324    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1325    async fn built_server_allows_keyless_payment_create_when_opted_out() {
1326        use tower::ServiceExt as _;
1327        let app = ServerBuilder::new(test_commerce())
1328            .without_auth()
1329            .require_idempotency_keys(false)
1330            .build();
1331        let response = app
1332            .oneshot(
1333                axum::http::Request::post("/api/v1/payments")
1334                    .header("content-type", "application/json")
1335                    .body(axum::body::Body::from("{}"))
1336                    .unwrap(),
1337            )
1338            .await
1339            .unwrap();
1340        assert_ne!(
1341            response.status(),
1342            axum::http::StatusCode::PRECONDITION_REQUIRED,
1343            "opt-out must disable the 428 gate"
1344        );
1345    }
1346
1347    #[test]
1348    fn builder_strict_authz_flag() {
1349        let builder = ServerBuilder::new(test_commerce());
1350        assert!(!builder.authz_strict, "default must preserve existing behavior");
1351        let builder = builder.with_strict_authz();
1352        assert!(builder.authz_strict);
1353    }
1354
1355    #[test]
1356    fn parse_bool_env_accepts_expected_values() {
1357        for raw in ["1", "true", "TRUE", "yes"] {
1358            assert!(matches!(parse_bool_env("TEST_ENV", raw), Ok(true)), "raw={raw}");
1359        }
1360        for raw in ["0", "false", "no", "", "  "] {
1361            assert!(matches!(parse_bool_env("TEST_ENV", raw), Ok(false)), "raw={raw}");
1362        }
1363        assert!(parse_bool_env("TEST_ENV", "maybe").is_err());
1364    }
1365
1366    #[tokio::test]
1367    async fn serve_refuses_non_loopback_bind_without_auth() {
1368        let err = ServerBuilder::new(test_commerce())
1369            .without_auth()
1370            .bind("192.0.2.1:0".parse().expect("socket addr"))
1371            .serve()
1372            .await
1373            .expect_err("must refuse to start without auth on a non-loopback bind");
1374        match err {
1375            HttpError::BadRequest(message) => {
1376                assert!(message.contains("Refusing to start"), "message: {message}");
1377                assert!(message.contains("allow_unauthenticated"), "message: {message}");
1378                assert!(message.contains("STATESET_HTTP_ALLOW_UNAUTHENTICATED"));
1379            }
1380            other => panic!("unexpected error: {other:?}"),
1381        }
1382    }
1383
1384    #[tokio::test]
1385    async fn allow_unauthenticated_bypasses_non_loopback_refusal() {
1386        // 192.0.2.1 (TEST-NET-1) is not locally assigned, so binding fails —
1387        // reaching the bind step proves the fail-closed check was opted out of.
1388        let err = ServerBuilder::new(test_commerce())
1389            .without_auth()
1390            .allow_unauthenticated()
1391            .bind("192.0.2.1:0".parse().expect("socket addr"))
1392            .serve()
1393            .await
1394            .expect_err("bind to TEST-NET-1 must fail");
1395        match err {
1396            HttpError::InternalError(message) => {
1397                assert!(message.contains("Failed to bind"), "message: {message}");
1398            }
1399            other => panic!("expected bind failure, got: {other:?}"),
1400        }
1401    }
1402
1403    #[tokio::test]
1404    async fn serve_allows_loopback_bind_without_auth() {
1405        let serve_future = ServerBuilder::new(test_commerce())
1406            .without_auth()
1407            .bind("127.0.0.1:0".parse().expect("socket addr"))
1408            .serve();
1409        // The server must start (and then block serving); a quick return would
1410        // be a startup refusal.
1411        let result =
1412            tokio::time::timeout(std::time::Duration::from_millis(500), serve_future).await;
1413        assert!(result.is_err(), "loopback bind without auth must start and keep serving");
1414    }
1415
1416    #[test]
1417    fn builder_without_metrics_auth() {
1418        let builder = ServerBuilder::new(test_commerce()).without_metrics_auth();
1419        assert!(builder.bearer_auth_token().is_some());
1420        assert!(builder.metrics_bearer_auth_token().is_none());
1421    }
1422
1423    #[test]
1424    fn builder_without_metrics_ip_allowlist() {
1425        let builder = ServerBuilder::new(test_commerce())
1426            .with_metrics_ip_allowlist(["127.0.0.1".parse().unwrap()])
1427            .without_metrics_ip_allowlist();
1428        assert!(builder.metrics_ip_allowlist().is_none());
1429    }
1430
1431    #[test]
1432    fn builder_without_metrics_ip_cidr_allowlist() {
1433        let builder = ServerBuilder::new(test_commerce())
1434            .with_metrics_ip_cidr_allowlist(["10.0.0.0/8".parse().unwrap()])
1435            .without_metrics_ip_cidr_allowlist();
1436        assert!(builder.metrics_ip_cidr_allowlist().is_none());
1437    }
1438
1439    #[test]
1440    fn builder_without_metrics_trusted_proxies() {
1441        let builder = ServerBuilder::new(test_commerce())
1442            .with_metrics_trusted_proxies(["10.0.0.0/8".parse().unwrap()])
1443            .without_metrics_trusted_proxies();
1444        assert!(builder.metrics_trusted_proxies().is_none());
1445    }
1446
1447    #[test]
1448    fn builder_chaining() {
1449        let addr: SocketAddr = "0.0.0.0:9090".parse().unwrap();
1450        let builder = ServerBuilder::new(test_commerce())
1451            .bind(addr)
1452            .with_cors()
1453            .with_request_id()
1454            .with_bearer_auth("chain-token")
1455            .bind_auth_tenant("chain-tenant")
1456            .bind_auth_actor("chain-actor");
1457        assert_eq!(builder.addr, addr);
1458        assert!(builder.enable_cors);
1459        assert!(builder.enable_request_id);
1460        assert_eq!(builder.bearer_auth_token(), Some("chain-token"));
1461        assert_eq!(builder.bound_tenant_id.as_deref(), Some("chain-tenant"));
1462        assert_eq!(builder.bound_actor_id.as_deref(), Some("chain-actor"));
1463    }
1464
1465    #[test]
1466    fn builder_with_max_tenant_dbs() {
1467        let tenant_dir =
1468            std::env::temp_dir().join(format!("stateset-http-builder-{}", Uuid::new_v4()));
1469        let builder = ServerBuilder::new(test_commerce())
1470            .with_tenant_db_dir(tenant_dir.clone())
1471            .with_max_tenant_dbs(1);
1472
1473        let tenant_a = builder.state.commerce_for_tenant(Some("tenant-a")).unwrap();
1474        let second_while_in_use = builder.state.commerce_for_tenant(Some("tenant-b"));
1475        assert!(matches!(second_while_in_use, Err(HttpError::TooManyRequests(_))));
1476
1477        drop(tenant_a);
1478        let second_after_release = builder.state.commerce_for_tenant(Some("tenant-b"));
1479        assert!(second_after_release.is_ok());
1480
1481        let _ = std::fs::remove_dir_all(tenant_dir);
1482    }
1483
1484    #[test]
1485    fn builder_builds_router() {
1486        let _router = ServerBuilder::new(test_commerce()).build();
1487    }
1488
1489    #[tokio::test]
1490    async fn built_router_fails_closed_for_unbound_tenant_routing() {
1491        let tenant_dir =
1492            std::env::temp_dir().join(format!("stateset-http-misconfig-{}", Uuid::new_v4()));
1493        let builder = ServerBuilder::new(test_commerce()).with_tenant_db_dir(tenant_dir.clone());
1494        let token =
1495            builder.bearer_auth_token().expect("default auth token should be present").to_string();
1496        let router = builder.build();
1497
1498        let resp = router
1499            .oneshot(
1500                Request::get("/api/v1/orders")
1501                    .header("authorization", format!("Bearer {token}"))
1502                    .header("x-tenant-id", "tenant-1")
1503                    .body(Body::empty())
1504                    .unwrap(),
1505            )
1506            .await
1507            .unwrap();
1508        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1509
1510        let _ = std::fs::remove_dir_all(tenant_dir);
1511    }
1512
1513    #[tokio::test]
1514    async fn built_router_fails_closed_for_invalid_bound_actor() {
1515        let builder = ServerBuilder::new(test_commerce()).bind_auth_actor("invalid actor");
1516        let token =
1517            builder.bearer_auth_token().expect("default auth token should be present").to_string();
1518        let router = builder.build();
1519
1520        let resp = router
1521            .oneshot(
1522                Request::get("/api/v1/orders")
1523                    .header("authorization", format!("Bearer {token}"))
1524                    .header("x-tenant-id", "tenant-1")
1525                    .body(Body::empty())
1526                    .unwrap(),
1527            )
1528            .await
1529            .unwrap();
1530        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1531    }
1532
1533    #[tokio::test]
1534    async fn built_router_fails_closed_for_actor_binding_without_auth() {
1535        let router =
1536            ServerBuilder::new(test_commerce()).without_auth().bind_auth_actor("admin-1").build();
1537
1538        let resp = router
1539            .oneshot(
1540                Request::get("/api/v1/orders")
1541                    .header("x-tenant-id", "tenant-1")
1542                    .body(Body::empty())
1543                    .unwrap(),
1544            )
1545            .await
1546            .unwrap();
1547        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1548    }
1549
1550    #[tokio::test]
1551    async fn built_router_fails_closed_for_duplicate_api_tokens() {
1552        let builder = ServerBuilder::new(test_commerce())
1553            .with_bearer_auth("duplicate-token")
1554            .add_bearer_auth_for_actor("duplicate-token", "viewer-1");
1555        let router = builder.build();
1556
1557        let resp = router
1558            .oneshot(
1559                Request::get("/api/v1/orders")
1560                    .header("authorization", "Bearer duplicate-token")
1561                    .header("x-tenant-id", "tenant-1")
1562                    .body(Body::empty())
1563                    .unwrap(),
1564            )
1565            .await
1566            .unwrap();
1567        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1568    }
1569
1570    #[tokio::test]
1571    async fn built_router_fails_closed_for_authz_without_actor_bound_tokens_or_trusted_headers() {
1572        let engine = AuthzEngineBuilder::new()
1573            .add_role(Role::viewer())
1574            .assign_role("viewer-1", "viewer")
1575            .build();
1576        let builder = ServerBuilder::new(test_commerce()).with_authz_engine(engine);
1577        let token =
1578            builder.bearer_auth_token().expect("default auth token should be present").to_string();
1579        let router = builder.build();
1580
1581        let resp = router
1582            .oneshot(
1583                Request::get("/api/v1/orders")
1584                    .header("authorization", format!("Bearer {token}"))
1585                    .header("x-tenant-id", "tenant-1")
1586                    .header("x-actor-id", "viewer-1")
1587                    .body(Body::empty())
1588                    .unwrap(),
1589            )
1590            .await
1591            .unwrap();
1592        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1593    }
1594
1595    #[tokio::test]
1596    async fn built_router_fails_closed_for_tenant_routing_with_unbound_additional_token() {
1597        let tenant_dir =
1598            std::env::temp_dir().join(format!("stateset-http-extra-token-{}", Uuid::new_v4()));
1599        let builder = ServerBuilder::new(test_commerce())
1600            .with_bearer_auth_for_tenant("primary-token", "tenant-1")
1601            .add_bearer_auth_for_actor("viewer-token", "viewer-1")
1602            .with_tenant_db_dir(tenant_dir.clone());
1603        let router = builder.build();
1604
1605        let resp = router
1606            .oneshot(
1607                Request::get("/api/v1/orders")
1608                    .header("authorization", "Bearer viewer-token")
1609                    .header("x-tenant-id", "tenant-1")
1610                    .body(Body::empty())
1611                    .unwrap(),
1612            )
1613            .await
1614            .unwrap();
1615        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1616
1617        let _ = std::fs::remove_dir_all(tenant_dir);
1618    }
1619
1620    #[tokio::test]
1621    async fn built_router_serves_health() {
1622        let router = ServerBuilder::new(test_commerce()).with_cors().with_request_id().build();
1623
1624        let resp =
1625            router.oneshot(Request::get("/health").body(Body::empty()).unwrap()).await.unwrap();
1626        assert_eq!(resp.status(), StatusCode::OK);
1627    }
1628
1629    #[tokio::test]
1630    async fn built_router_serves_api_orders() {
1631        let router = ServerBuilder::new(test_commerce()).without_auth().build();
1632
1633        let resp = router
1634            .oneshot(Request::get("/api/v1/orders").body(Body::empty()).unwrap())
1635            .await
1636            .unwrap();
1637        assert_eq!(resp.status(), StatusCode::OK);
1638    }
1639
1640    #[tokio::test]
1641    async fn built_router_serves_api_customers() {
1642        let router = ServerBuilder::new(test_commerce()).without_auth().build();
1643
1644        let resp = router
1645            .oneshot(Request::get("/api/v1/customers").body(Body::empty()).unwrap())
1646            .await
1647            .unwrap();
1648        assert_eq!(resp.status(), StatusCode::OK);
1649    }
1650
1651    #[tokio::test]
1652    async fn built_router_serves_api_products() {
1653        let router = ServerBuilder::new(test_commerce()).without_auth().build();
1654
1655        let resp = router
1656            .oneshot(Request::get("/api/v1/products").body(Body::empty()).unwrap())
1657            .await
1658            .unwrap();
1659        assert_eq!(resp.status(), StatusCode::OK);
1660    }
1661
1662    #[tokio::test]
1663    async fn built_router_404_for_unknown_path() {
1664        let router = ServerBuilder::new(test_commerce()).without_auth().build();
1665
1666        let resp = router
1667            .oneshot(Request::get("/api/v1/nonexistent").body(Body::empty()).unwrap())
1668            .await
1669            .unwrap();
1670        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1671    }
1672
1673    #[test]
1674    fn builder_debug_impl() {
1675        let builder = ServerBuilder::new(test_commerce());
1676        let dbg = format!("{builder:?}");
1677        assert!(dbg.contains("ServerBuilder"));
1678        assert!(dbg.contains("<redacted>"));
1679    }
1680
1681    #[tokio::test]
1682    async fn built_router_blocks_api_without_token_by_default() {
1683        let router = ServerBuilder::new(test_commerce()).build();
1684
1685        let resp = router
1686            .oneshot(Request::get("/api/v1/orders").body(Body::empty()).unwrap())
1687            .await
1688            .unwrap();
1689        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1690    }
1691
1692    #[tokio::test]
1693    async fn built_router_allows_api_with_token() {
1694        // Sends `x-tenant-id` without a tenant DB dir; opt into the explicit
1695        // escape hatch now that silent fallthrough is rejected.
1696        let builder = ServerBuilder::new(test_commerce()).with_ignore_tenant_header();
1697        let token =
1698            builder.bearer_auth_token().expect("default auth token should be present").to_string();
1699        let router = builder.build();
1700
1701        let resp = router
1702            .oneshot(
1703                Request::get("/api/v1/orders")
1704                    .header("authorization", format!("Bearer {token}"))
1705                    .header("x-tenant-id", "tenant-1")
1706                    .body(Body::empty())
1707                    .unwrap(),
1708            )
1709            .await
1710            .unwrap();
1711        assert_eq!(resp.status(), StatusCode::OK);
1712    }
1713
1714    #[tokio::test]
1715    async fn built_router_blocks_metrics_without_token_by_default() {
1716        let router = ServerBuilder::new(test_commerce()).build();
1717
1718        let resp =
1719            router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1720        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1721    }
1722
1723    #[tokio::test]
1724    async fn built_router_allows_metrics_with_token() {
1725        let builder = ServerBuilder::new(test_commerce());
1726        let token = builder
1727            .metrics_bearer_auth_token()
1728            .expect("default metrics token should be present")
1729            .to_string();
1730        let router = builder.build();
1731
1732        let resp = router
1733            .oneshot(
1734                Request::get("/metrics")
1735                    .header("authorization", format!("Bearer {token}"))
1736                    .body(Body::empty())
1737                    .unwrap(),
1738            )
1739            .await
1740            .unwrap();
1741        assert_eq!(resp.status(), StatusCode::OK);
1742    }
1743
1744    #[tokio::test]
1745    async fn built_router_allows_metrics_without_token_when_disabled() {
1746        let router = ServerBuilder::new(test_commerce()).without_metrics_auth().build();
1747
1748        let resp =
1749            router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1750        assert_eq!(resp.status(), StatusCode::OK);
1751    }
1752
1753    #[tokio::test]
1754    async fn built_router_keeps_metrics_protected_after_without_auth() {
1755        let router = ServerBuilder::new(test_commerce()).without_auth().build();
1756
1757        let resp =
1758            router.oneshot(Request::get("/metrics").body(Body::empty()).unwrap()).await.unwrap();
1759        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1760    }
1761
1762    #[tokio::test]
1763    async fn built_router_rejects_mismatched_tenant_for_bound_token() {
1764        let router = ServerBuilder::new(test_commerce())
1765            .with_bearer_auth_for_tenant("bound-token", "tenant-1")
1766            .build();
1767
1768        let resp = router
1769            .oneshot(
1770                Request::get("/api/v1/orders")
1771                    .header("authorization", "Bearer bound-token")
1772                    .header("x-tenant-id", "tenant-2")
1773                    .body(Body::empty())
1774                    .unwrap(),
1775            )
1776            .await
1777            .unwrap();
1778        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1779    }
1780
1781    #[tokio::test]
1782    async fn serve_refuses_public_bind_without_auth() {
1783        let addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
1784        let err = ServerBuilder::new(test_commerce())
1785            .bind(addr)
1786            .without_auth()
1787            .serve()
1788            .await
1789            .expect_err("should reject public bind without auth");
1790
1791        assert!(err.to_string().contains("Refusing to start without API auth"));
1792    }
1793
1794    #[tokio::test]
1795    async fn serve_refuses_tenant_routing_without_tenant_binding() {
1796        let tenant_dir =
1797            std::env::temp_dir().join(format!("stateset-http-serve-misconfig-{}", Uuid::new_v4()));
1798        let err = ServerBuilder::new(test_commerce())
1799            .with_tenant_db_dir(tenant_dir.clone())
1800            .serve()
1801            .await
1802            .expect_err("should reject unbound tenant routing");
1803
1804        assert!(err.to_string().contains("binding every API bearer token"));
1805        let _ = std::fs::remove_dir_all(tenant_dir);
1806    }
1807
1808    #[tokio::test]
1809    async fn serve_refuses_tenant_routing_without_auth() {
1810        let tenant_dir =
1811            std::env::temp_dir().join(format!("stateset-http-serve-no-auth-{}", Uuid::new_v4()));
1812        let err = ServerBuilder::new(test_commerce())
1813            .with_tenant_db_dir(tenant_dir.clone())
1814            .without_auth()
1815            .serve()
1816            .await
1817            .expect_err("should reject tenant routing without auth");
1818
1819        assert!(err.to_string().contains("requires API auth"));
1820        let _ = std::fs::remove_dir_all(tenant_dir);
1821    }
1822
1823    #[tokio::test]
1824    async fn serve_refuses_authz_without_actor_bound_tokens_or_trusted_headers() {
1825        let engine = AuthzEngineBuilder::new()
1826            .add_role(Role::viewer())
1827            .assign_role("viewer-1", "viewer")
1828            .build();
1829        let err = ServerBuilder::new(test_commerce())
1830            .with_authz_engine(engine)
1831            .serve()
1832            .await
1833            .expect_err("should reject authz configuration without actor identity source");
1834
1835        assert!(err.to_string().contains("binding every API bearer token"));
1836    }
1837}