//! Shared server state and configuration for the `HttpAdapter` router. use std::path::PathBuf; use std::sync::Arc; use alkcall::core::auth::IdentityProvider; use alkcall::registry::registration::OperationRegistry; /// The stealth decoy surface for paths that are not registered /// operations and not part of the reserved default surface (the 6 /// gateway endpoints, `/healthz`, `/openapi.json`, the MCP route, the /// WS upgrade path). Set by the assembly layer at `HttpAdapter` /// construction. The existence of the decoy path is fixed by ADR-010; /// the variant is a two-way-door config default. #[derive(Clone, Default, Debug)] pub enum DecoyConfig { /// Serve a fake `404 Not Found` (the default — a fake nginx 404). #[default] NotFound, /// Serve a static site from the given directory root. StaticSite { /// The directory tree served for unregistered paths. root: PathBuf, }, /// Redirect unregistered paths to the given URL. Redirect { /// The redirect target URL. to: String, }, } /// State embedded in the axum `Router`: the registry and identity /// provider every request handler reaches through the router state, plus /// the decoy config for the fallback, the pre-serialized /// `/openapi.json` projection cache (SRV-09), and the WS session /// lifecycle config the upgrade handler enforces: the shared pump-handle /// registry (WS-08) and the concurrent-session cap (WS-09). #[derive(Clone)] pub(crate) struct RouterState { pub(crate) registry: Arc, pub(crate) identity_provider: Arc, pub(crate) decoy: DecoyConfig, pub(crate) openapi_doc: crate::server::adapter::CachedOpenAPIDoc, pub(crate) ws_sessions: Arc, /// Session cap (WS-09): one `HttpAdapter`-wide semaphore, built /// once at construction — the upgrade handler acquires one permit /// per upgrade and holds it for the session's lifetime. pub(crate) ws_session_slots: Arc, /// Idle-read timeout for the WS pumps (WS-01); `None` disables. pub(crate) ws_idle_timeout: Option, /// The openable-ALPN set for WS sessions (WS-22): the per-ALPN /// open-op specs + handlers registered on each session's fork. /// `None` (the default) declares no openables — a WS session then /// carries no data-channel open ops (the pre-Unit-2 shape). pub(crate) ws_openable_alpns: Option>, /// The `op/register` surface's `AccessControl` for WS sessions /// (review 007 WS-29): the per-session announce op is registered /// with this ACL. Default: `AccessControl::default()` (the /// permissive crate default). pub(crate) ws_op_register_acl: alkcall::registry::spec::AccessControl, } impl axum::extract::FromRef for crate::websocket::SessionState { fn from_ref(state: &RouterState) -> Self { crate::websocket::SessionState::new( Arc::clone(&state.registry), Arc::clone(&state.ws_sessions), Arc::clone(&state.ws_session_slots), state.ws_idle_timeout, state.ws_openable_alpns.clone(), state.ws_op_register_acl.clone(), ) } } impl axum::extract::FromRef for DecoyConfig { fn from_ref(state: &RouterState) -> Self { state.decoy.clone() } } impl axum::extract::FromRef for crate::server::adapter::CachedOpenAPIDoc { fn from_ref(state: &RouterState) -> Self { state.openapi_doc.clone() } } #[cfg(test)] mod tests { use super::*; #[test] fn decoy_config_default_is_not_found() { assert!(matches!(DecoyConfig::default(), DecoyConfig::NotFound)); } #[test] fn router_state_from_ref_extracts_decoy() { let state = RouterState { registry: Arc::new(OperationRegistry::new()), identity_provider: Arc::new(NoopProvider), decoy: DecoyConfig::Redirect { to: "https://example.com".to_string(), }, openapi_doc: crate::server::adapter::CachedOpenAPIDoc::new(&OperationRegistry::new()), ws_sessions: Arc::new(crate::websocket::WsSessions::new()), ws_session_slots: Arc::new(tokio::sync::Semaphore::new( crate::websocket::DEFAULT_WS_MAX_SESSIONS, )), ws_idle_timeout: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT), ws_openable_alpns: None, ws_op_register_acl: alkcall::registry::spec::AccessControl::default(), }; let extracted: DecoyConfig = axum::extract::FromRef::from_ref(&state); assert!(matches!(extracted, DecoyConfig::Redirect { .. })); } struct NoopProvider; impl IdentityProvider for NoopProvider { fn resolve_from_fingerprint(&self, _: &str) -> Option { None } fn resolve_from_token( &self, _: &alkcall::core::auth::AuthToken, ) -> Option { None } } }