From 881e63cebad87b9cb1587db26e965c430a938d14 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Sat, 26 Sep 2026 04:16:59 +0200 Subject: [PATCH 1/2] refactor(config): make typed gateway config authoritative --- gateway/src/lib.rs | 2 +- gateway/src/main.rs | 12 +++---- gateway/src/settings.rs | 79 +++++++++++++---------------------------- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 8787749..99aff26 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -17,7 +17,7 @@ //! [`crate::module::ServiceHealthError`] //! * [`services`] — concrete [`crate::module::ServiceModule`] implementations //! with typed DTOs -//! * [`settings`] — environment-based configuration with redacted [`Debug`] +//! * [`settings`] — secret/auth environment loading plus typed runtime configuration with redacted [`Debug`] //! and `--dev-keys` ephemeral keypair generation //! * [`sse`] — Server-Sent Events via [`tokio::sync::broadcast::Sender`] //! * [`openapi`] — `OpenAPI` schema generation via `utoipa` diff --git a/gateway/src/main.rs b/gateway/src/main.rs index f868a6b..570804c 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -24,8 +24,6 @@ async fn main() -> anyhow::Result<()> { let proxy_upstream_url = cfg.gateway.proxy_upstream_url.clone(); let refresh_token_ttl_secs = i64::try_from(cfg.gateway.refresh_token_ttl_secs) .context("gateway.refresh_token_ttl_secs exceeds i64::MAX")?; - let access_token_ttl_secs = i64::try_from(cfg.gateway.access_token_ttl_secs) - .context("gateway.access_token_ttl_secs exceeds i64::MAX")?; #[cfg(feature = "otel")] let provider = @@ -40,15 +38,13 @@ async fn main() -> anyhow::Result<()> { ) .init(); - let mut settings = if dev_keys { + let settings = if dev_keys { gateway_example::settings::Settings::load_dev_keys_from_env()? } else { gateway_example::settings::Settings::load()? - }; - settings.access_token_ttl_secs = access_token_ttl_secs; - settings.allowed_origins = Arc::from(cfg.gateway.cors.allowed_origins.as_str()); - settings.sse_broadcast_buffer = cfg.gateway.sse_broadcast_buffer; - settings.session.cookie_secure = cfg.gateway.session.cookie_secure; + } + .apply_runtime_config(&cfg.gateway)?; + let access_token_ttl_secs = settings.access_token_ttl_secs; let db_pool = create_db_pool().await?; run_migrations(&db_pool) diff --git a/gateway/src/settings.rs b/gateway/src/settings.rs index 4aa470b..79c6d03 100644 --- a/gateway/src/settings.rs +++ b/gateway/src/settings.rs @@ -1,8 +1,7 @@ //! Shared gateway settings. //! -//! Secret/auth material still comes from deployment environment variables. -//! Non-secret runtime settings are overwritten from `rwf_config::Config` by -//! production `main`, keeping the typed config tree authoritative. +//! Secret/auth material comes from deployment environment variables. Non-secret +//! runtime settings are applied explicitly from `rwf_config::GatewayConfig`. use std::str::FromStr; use std::sync::Arc; @@ -16,9 +15,6 @@ use crate::pem::{ed25519_spki_der, pem_encode}; pub const JWT_ISS: &str = "gateway-example"; pub const JWT_AUD: &str = "gateway-example-api"; pub const DEFAULT_ADMIN_USER_ID: &str = "00000000-0000-0000-0000-000000000001"; -pub const DEFAULT_ALLOWED_ORIGINS: &str = - "http://localhost:3000,http://localhost:3001,http://localhost:3002"; - #[must_use] fn short_fingerprint(bytes: &[u8]) -> String { let mut h: u64 = 0xcbf2_9ce4_8422_2325; @@ -29,44 +25,6 @@ fn short_fingerprint(bytes: &[u8]) -> String { format!("{h:016x}") } -fn bool_env(name: &str, default: bool) -> anyhow::Result { - match std::env::var(name) { - Ok(value) if value.eq_ignore_ascii_case("true") || value == "1" => Ok(true), - Ok(value) if value.eq_ignore_ascii_case("false") || value == "0" => Ok(false), - Ok(value) => anyhow::bail!("{name} must be true/false or 1/0, got {value:?}"), - Err(std::env::VarError::NotPresent) => Ok(default), - Err(e) => Err(e).with_context(|| format!("failed to read {name}")), - } -} - -fn positive_i64_env(name: &str, default: i64) -> anyhow::Result { - let value = match std::env::var(name) { - Ok(raw) => raw - .parse::() - .with_context(|| format!("{name} must be an integer, got {raw:?}"))?, - Err(std::env::VarError::NotPresent) => default, - Err(e) => return Err(e).with_context(|| format!("failed to read {name}")), - }; - if value <= 0 { - anyhow::bail!("{name} must be positive, got {value}"); - } - Ok(value) -} - -fn positive_usize_env(name: &str, default: usize) -> anyhow::Result { - let value = match std::env::var(name) { - Ok(raw) => raw - .parse::() - .with_context(|| format!("{name} must be a positive integer, got {raw:?}"))?, - Err(std::env::VarError::NotPresent) => default, - Err(e) => return Err(e).with_context(|| format!("failed to read {name}")), - }; - if value == 0 { - anyhow::bail!("{name} must be greater than zero"); - } - Ok(value) -} - #[derive(Debug, Clone)] pub struct SessionSettings { pub cookie_secure: bool, @@ -116,8 +74,24 @@ impl std::fmt::Debug for Settings { } impl Settings { - /// Load secrets plus legacy environment fallbacks. Production `main` - /// replaces non-secret values with the already-validated typed config. + /// Apply the canonical typed runtime configuration. + /// + /// # Errors + /// Returns an error if a validated integer cannot be represented by the + /// gateway's signed duration fields. + pub fn apply_runtime_config( + mut self, + config: &rwf_config::GatewayConfig, + ) -> Result { + self.access_token_ttl_secs = i64::try_from(config.access_token_ttl_secs) + .context("gateway.access_token_ttl_secs exceeds i64::MAX")?; + self.allowed_origins = Arc::from(config.cors.allowed_origins.as_str()); + self.sse_broadcast_buffer = config.sse_broadcast_buffer; + self.session.cookie_secure = config.session.cookie_secure; + Ok(self) + } + + /// Load secret/auth deployment settings. /// /// # Errors /// Returns an error for missing/invalid keys, credentials, IDs or values. @@ -154,11 +128,11 @@ impl Settings { .with_context(|| format!("invalid ADMIN_USER_ID {admin_user_id_raw:?}"))?; let session = SessionSettings { - cookie_secure: bool_env("SESSION_COOKIE_SECURE", true)?, cookie_name: std::env::var("SESSION_COOKIE_NAME") .unwrap_or_else(|_| "rwf_session".to_string()), csrf_cookie_name: std::env::var("CSRF_COOKIE_NAME") .unwrap_or_else(|_| "rwf_csrf".to_string()), + ..SessionSettings::default() }; Ok(Self { @@ -166,14 +140,11 @@ impl Settings { jwt_public_key_pem: Arc::from(jwt_public_key_pem.as_str()), encoding_key, decoding_key, - access_token_ttl_secs: positive_i64_env("ACCESS_TOKEN_TTL_SECS", 15 * 60)?, + access_token_ttl_secs: 15 * 60, admin_user_id, default_admin_password: Arc::from(default_admin_password.as_str()), - allowed_origins: Arc::from( - std::env::var("ALLOWED_ORIGINS") - .unwrap_or_else(|_| DEFAULT_ALLOWED_ORIGINS.to_string()), - ), - sse_broadcast_buffer: positive_usize_env("SSE_BROADCAST_BUFFER", 256)?, + allowed_origins: Arc::from("http://localhost:3000,http://localhost:3001,http://localhost:3002"), + sse_broadcast_buffer: 256, session, }) } @@ -224,7 +195,7 @@ impl Settings { access_token_ttl_secs: 15 * 60, admin_user_id, default_admin_password: Arc::from(admin_password), - allowed_origins: Arc::from(DEFAULT_ALLOWED_ORIGINS), + allowed_origins: Arc::from("http://localhost:3000,http://localhost:3001,http://localhost:3002"), sse_broadcast_buffer: 256, session: SessionSettings { cookie_secure: false, From 0512d422e9a691fc7e157619f6763d523f75b75a Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Sat, 26 Sep 2026 06:28:00 +0200 Subject: [PATCH 2/2] style: apply rustfmt to gateway settings --- gateway/src/settings.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gateway/src/settings.rs b/gateway/src/settings.rs index 79c6d03..6b7d3ac 100644 --- a/gateway/src/settings.rs +++ b/gateway/src/settings.rs @@ -15,6 +15,7 @@ use crate::pem::{ed25519_spki_der, pem_encode}; pub const JWT_ISS: &str = "gateway-example"; pub const JWT_AUD: &str = "gateway-example-api"; pub const DEFAULT_ADMIN_USER_ID: &str = "00000000-0000-0000-0000-000000000001"; + #[must_use] fn short_fingerprint(bytes: &[u8]) -> String { let mut h: u64 = 0xcbf2_9ce4_8422_2325; @@ -143,7 +144,9 @@ impl Settings { access_token_ttl_secs: 15 * 60, admin_user_id, default_admin_password: Arc::from(default_admin_password.as_str()), - allowed_origins: Arc::from("http://localhost:3000,http://localhost:3001,http://localhost:3002"), + allowed_origins: Arc::from( + "http://localhost:3000,http://localhost:3001,http://localhost:3002", + ), sse_broadcast_buffer: 256, session, }) @@ -195,7 +198,9 @@ impl Settings { access_token_ttl_secs: 15 * 60, admin_user_id, default_admin_password: Arc::from(admin_password), - allowed_origins: Arc::from("http://localhost:3000,http://localhost:3001,http://localhost:3002"), + allowed_origins: Arc::from( + "http://localhost:3000,http://localhost:3001,http://localhost:3002", + ), sse_broadcast_buffer: 256, session: SessionSettings { cookie_secure: false,