From 543a89bdbd9f0cd073fa6de2260c86f1bbfa4ee3 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:12:22 -0800 Subject: [PATCH 1/4] Fix env var config roundtrip through build.rs TOML serialization Three bugs caused env-var-sourced integration settings to lose their types when build.rs serialized Settings to TOML: 1. collect_env_vars in build.rs skipped empty objects, arrays, and null values, so new env vars for those fields never triggered a rebuild. 2. vec_from_seq_or_map hard-failed on bracket-wrapped strings that aren't valid JSON (e.g. glob patterns like [/.static/prebid/{*rest}]). Now falls back to bracket-stripping + comma-splitting. 3. Env vars arrive as strings ("true", "1000") in the config crate's HashMap. Without eager normalization, toml's serializer wrote them as TOML strings, so the runtime re-parse produced String("true") instead of Bool(true). Added IntegrationSettings::normalize() called from a new from_toml_and_env path used only by build.rs, keeping the runtime from_toml path clean. Also replaced the publisher.origin_url trailing-slash normalization with a validator that rejects trailing slashes at validation time. --- crates/common/Cargo.toml | 1 + crates/common/build.rs | 74 +++--------- crates/common/src/settings.rs | 214 ++++++++++++++++++++++++++-------- 3 files changed, 182 insertions(+), 107 deletions(-) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 17a0e04b..4ce1b74f 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -68,3 +68,4 @@ default = [] [dev-dependencies] temp-env = { workspace = true } tokio-test = { workspace = true } +toml = { workspace = true } diff --git a/crates/common/build.rs b/crates/common/build.rs index 4bac64f0..cb1e60ae 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -9,8 +9,6 @@ mod auction_config_types; #[path = "src/settings.rs"] mod settings; -use serde_json::Value; -use std::collections::HashSet; use std::fs; use std::path::Path; @@ -18,73 +16,29 @@ const TRUSTED_SERVER_INIT_CONFIG_PATH: &str = "../../trusted-server.toml"; const TRUSTED_SERVER_OUTPUT_CONFIG_PATH: &str = "../../target/trusted-server-out.toml"; fn main() { - merge_toml(); - rerun_if_changed(); -} - -fn rerun_if_changed() { - // Watch the root trusted-server.toml file for changes - println!("cargo:rerun-if-changed={}", TRUSTED_SERVER_INIT_CONFIG_PATH); - - // Create a default Settings instance and convert to JSON to discover all fields - let default_settings = settings::Settings::default(); - let settings_json = serde_json::to_value(&default_settings).unwrap(); - - let mut env_vars = HashSet::new(); - collect_env_vars(&settings_json, &mut env_vars, &[]); - - // Print rerun-if-env-changed for each variable - let mut sorted_vars: Vec<_> = env_vars.into_iter().collect(); - sorted_vars.sort(); - - for var in sorted_vars { - println!("cargo:rerun-if-env-changed={}", var); - } -} - -fn merge_toml() { - // Get the OUT_DIR where we'll copy the config file - let dest_path = Path::new(TRUSTED_SERVER_OUTPUT_CONFIG_PATH); + // Always rerun build.rs: integration settings are stored in a flat + // HashMap, so we cannot enumerate all possible env + // var keys ahead of time. Emitting rerun-if-changed for a nonexistent + // file forces cargo to always rerun the build script. + println!("cargo:rerun-if-changed=_always_rebuild_sentinel_"); // Read init config let init_config_path = Path::new(TRUSTED_SERVER_INIT_CONFIG_PATH); let toml_content = fs::read_to_string(init_config_path) - .unwrap_or_else(|_| panic!("Failed to read {:?}", init_config_path)); + .unwrap_or_else(|_| panic!("Failed to read {init_config_path:?}")); - // For build time: use from_toml to parse with environment variables - let settings = settings::Settings::from_toml(&toml_content) + // Merge base TOML with environment variable overrides and write output + let settings = settings::Settings::from_toml_and_env(&toml_content) .expect("Failed to parse settings at build time"); - // Write the merged settings to the output directory as TOML let merged_toml = toml::to_string_pretty(&settings).expect("Failed to serialize settings to TOML"); - fs::write(dest_path, merged_toml).unwrap_or_else(|_| panic!("Failed to write {:?}", dest_path)); -} - -fn collect_env_vars(value: &Value, env_vars: &mut HashSet, path: &[String]) { - if let Value::Object(map) = value { - for (key, val) in map { - let mut new_path = path.to_owned(); - new_path.push(key.to_uppercase()); - - match val { - Value::String(_) | Value::Number(_) | Value::Bool(_) => { - // Leaf node - create environment variable - let env_var = format!( - "{}{}{}", - settings::ENVIRONMENT_VARIABLE_PREFIX, - settings::ENVIRONMENT_VARIABLE_SEPARATOR, - new_path.join(settings::ENVIRONMENT_VARIABLE_SEPARATOR) - ); - env_vars.insert(env_var); - } - Value::Object(_) => { - // Recurse into nested objects - collect_env_vars(val, env_vars, &new_path); - } - _ => {} - } - } + // Only write when content changes to avoid unnecessary recompilation. + let dest_path = Path::new(TRUSTED_SERVER_OUTPUT_CONFIG_PATH); + let current = fs::read_to_string(dest_path).unwrap_or_default(); + if current != merged_toml { + fs::write(dest_path, merged_toml) + .unwrap_or_else(|_| panic!("Failed to write {dest_path:?}")); } } diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index 5963b602..cf4c1145 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -21,6 +21,7 @@ pub const ENVIRONMENT_VARIABLE_SEPARATOR: &str = "__"; pub struct Publisher { pub domain: String, pub cookie_domain: String, + #[validate(custom(function = validate_no_trailing_slash))] pub origin_url: String, /// Secret used to encrypt/decrypt proxied URLs in `/first-party/proxy`. /// Keep this secret stable to allow existing links to decode. @@ -56,16 +57,6 @@ impl Publisher { .unwrap_or_else(|| self.origin_url.clone()) } - fn normalize(&mut self) { - let trimmed = self.origin_url.trim_end_matches('/'); - if trimmed != self.origin_url { - log::warn!( - "publisher.origin_url ends with '/': normalizing to {}", - trimmed - ); - self.origin_url = trimmed.to_string(); - } - } } #[derive(Debug, Default, Clone, Deserialize, Serialize)] @@ -122,6 +113,16 @@ impl IntegrationSettings { } } + /// Normalizes all entries in place, converting JSON-encoded strings from + /// environment variables into their proper typed representations. + /// Called eagerly after deserialization so that TOML serialization in + /// build.rs preserves correct types. + pub fn normalize(&mut self) { + for value in self.entries.values_mut() { + *value = Self::normalize_env_value(value.clone()); + } + } + /// Retrieves and validates a typed configuration for an integration. /// /// # Errors @@ -139,9 +140,7 @@ impl IntegrationSettings { None => return Ok(None), }; - let normalized = Self::normalize_env_value(raw.clone()); - - let config: T = serde_json::from_value(normalized).change_context( + let config: T = serde_json::from_value(raw.clone()).change_context( TrustedServerError::Configuration { message: format!( "Integration '{integration_id}' configuration could not be parsed" @@ -355,15 +354,41 @@ impl Settings { Ok(settings) } - /// Creates a new [`Settings`] instance from a TOML string. + /// Creates a new [`Settings`] instance from a pre-built TOML string. /// - /// Parses the provided TOML configuration and applies any environment - /// variable overrides using the `TRUSTED_SERVER__` prefix. + /// Use this for the runtime path where the TOML has already been + /// fully resolved (env vars baked in by build.rs). /// /// # Errors /// /// - [`TrustedServerError::Configuration`] if the TOML is invalid or missing required fields pub fn from_toml(toml_str: &str) -> Result> { + let toml = File::from_str(toml_str, FileFormat::Toml); + let config = Config::builder().add_source(toml).build().change_context( + TrustedServerError::Configuration { + message: "Failed to build configuration".to_string(), + }, + )?; + let settings: Self = + config + .try_deserialize() + .change_context(TrustedServerError::Configuration { + message: "Failed to deserialize configuration".to_string(), + })?; + + Ok(settings) + } + + /// Creates a new [`Settings`] instance from a TOML string, applying + /// environment variable overrides using the `TRUSTED_SERVER__` prefix. + /// + /// Used by build.rs to merge the base config with env vars before + /// baking the result into the binary. + /// + /// # Errors + /// + /// - [`TrustedServerError::Configuration`] if the TOML is invalid or missing required fields + pub fn from_toml_and_env(toml_str: &str) -> Result> { let environment = Environment::default() .prefix(ENVIRONMENT_VARIABLE_PREFIX) .separator(ENVIRONMENT_VARIABLE_SEPARATOR); @@ -383,7 +408,7 @@ impl Settings { message: "Failed to deserialize configuration".to_string(), })?; - settings.publisher.normalize(); + settings.integrations.normalize(); Ok(settings) } @@ -410,6 +435,16 @@ impl Settings { } } +fn validate_no_trailing_slash(value: &str) -> Result<(), ValidationError> { + if value.ends_with('/') { + let mut err = ValidationError::new("trailing_slash"); + err.add_param("value".into(), &value); + err.message = Some("origin_url must not end with '/'".into()); + return Err(err); + } + Ok(()) +} + fn validate_path(value: &str) -> Result<(), ValidationError> { Regex::new(value).map(|_| ()).map_err(|err| { let mut validation_error = ValidationError::new("invalid_regex"); @@ -491,7 +526,24 @@ where JsonValue::String(s) => { let txt = s.trim(); if txt.starts_with('[') { - serde_json::from_str::>(txt).map_err(serde::de::Error::custom) + if let Ok(vec) = serde_json::from_str::>(txt) { + return Ok(vec); + } + // Not valid JSON array — strip brackets and split on commas + let inner = txt[1..txt.len() - 1].trim(); + let parts: Vec<&str> = inner + .split(',') + .map(str::trim) + .filter(|p| !p.is_empty()) + .collect(); + let mut out: Vec = Vec::with_capacity(parts.len()); + for p in parts { + let json = format!("\"{}\"", p.replace('"', "\\\"")); + let parsed: T = + serde_json::from_str(&json).map_err(serde::de::Error::custom)?; + out.push(parsed); + } + Ok(out) } else { let parts = if txt.contains(',') { txt.split(',') @@ -524,8 +576,10 @@ mod tests { use regex::Regex; use serde_json::json; - use crate::integrations::{nextjs::NextJsIntegrationConfig, prebid::PrebidIntegrationConfig}; - use crate::streaming_replacer::create_url_replacer; + use crate::integrations::{ + nextjs::NextJsIntegrationConfig, prebid::PrebidIntegrationConfig, + testlight::TestlightConfig, + }; use crate::test_support::tests::{crate_test_settings_str, create_test_settings}; #[test] @@ -617,31 +671,17 @@ mod tests { } #[test] - fn from_toml_normalizes_trailing_slash_in_origin_url() { + fn validate_rejects_trailing_slash_in_origin_url() { let toml_str = crate_test_settings_str().replace( r#"origin_url = "https://origin.test-publisher.com""#, r#"origin_url = "https://origin.test-publisher.com/""#, ); - let settings = Settings::from_toml(&toml_str).expect("should parse valid TOML"); - assert_eq!( - settings.publisher.origin_url, "https://origin.test-publisher.com", - "origin_url should be normalized by trimming trailing slashes" - ); - - let origin_host = settings.publisher.origin_host(); - let mut replacer = create_url_replacer( - &origin_host, - &settings.publisher.origin_url, - "proxy.example.com", - "https", - ); - - let processed = replacer.process_chunk(b"https://origin.test-publisher.com/news", true); - let rewritten = String::from_utf8(processed).expect("should be valid UTF-8"); - assert_eq!( - rewritten, "https://proxy.example.com/news", - "rewriting should keep the delimiter slash between host and path" + let settings = Settings::from_toml(&toml_str).expect("should parse TOML"); + let result = settings.validate(); + assert!( + result.is_err(), + "origin_url ending with '/' should fail validation" ); } @@ -709,7 +749,7 @@ mod tests { Some("https://origin.test-publisher.com"), || { temp_env::with_var(env_key, Some("[\"smartadserver\",\"rubicon\"]"), || { - let res = Settings::from_toml(&toml_str); + let res = Settings::from_toml_and_env(&toml_str); if res.is_err() { eprintln!("JSON override error: {:?}", res.as_ref().err()); } @@ -761,7 +801,7 @@ mod tests { || { temp_env::with_var(env_key0, Some("smartadserver"), || { temp_env::with_var(env_key1, Some("openx"), || { - let res = Settings::from_toml(&toml_str); + let res = Settings::from_toml_and_env(&toml_str); if res.is_err() { eprintln!("Indexed override error: {:?}", res.as_ref().err()); } @@ -820,7 +860,7 @@ mod tests { temp_env::with_var(path_key, Some("^/env-handler"), || { temp_env::with_var(username_key, Some("env-user"), || { temp_env::with_var(password_key, Some("env-pass"), || { - let settings = Settings::from_toml(&toml_str) + let settings = Settings::from_toml_and_env(&toml_str) .expect("Settings should load from env"); assert_eq!(settings.handlers.len(), 1); let handler = &settings.handlers[0]; @@ -846,7 +886,7 @@ mod tests { env_key, Some(r#"{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}"#), || { - let settings = Settings::from_toml(&toml_str) + let settings = Settings::from_toml_and_env(&toml_str) .expect("Settings should parse with JSON response_headers env"); assert_eq!(settings.response_headers.len(), 2); assert_eq!( @@ -880,7 +920,7 @@ mod tests { ), Some("https://change-publisher.com"), || { - let settings = Settings::from_toml(&crate_test_settings_str()); + let settings = Settings::from_toml_and_env(&crate_test_settings_str()); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( @@ -904,7 +944,7 @@ mod tests { ), Some("https://change-publisher.com"), || { - let settings = Settings::from_toml(&toml_str); + let settings = Settings::from_toml_and_env(&toml_str); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( @@ -1009,7 +1049,7 @@ mod tests { temp_env::with_var(timeout_key, Some("2500"), || { temp_env::with_var(rewrite_key, Some("true"), || { temp_env::with_var(enabled_key, Some("true"), || { - let settings = Settings::from_toml(&toml_str) + let settings = Settings::from_toml_and_env(&toml_str) .expect("Settings should load"); let config = settings @@ -1058,6 +1098,86 @@ mod tests { assert!(config.is_none(), "Disabled integrations should be skipped"); } + /// Tests the full build.rs round-trip: env vars are baked into Settings + /// at build time via from_toml_and_env, serialized to TOML, then parsed + /// back at runtime via from_toml. Verifies that env-sourced integration + /// values (strings like "true") are normalized to proper types so the + /// serialized TOML has correct types. + #[test] + fn test_env_var_roundtrip_normalizes_integration_types() { + let toml_str = crate_test_settings_str(); + + let integration_prefix = format!( + "{}{}INTEGRATIONS{}TESTLIGHT{}", + ENVIRONMENT_VARIABLE_PREFIX, + ENVIRONMENT_VARIABLE_SEPARATOR, + ENVIRONMENT_VARIABLE_SEPARATOR, + ENVIRONMENT_VARIABLE_SEPARATOR, + ); + let enabled_key = format!("{}ENABLED", integration_prefix); + let endpoint_key = format!("{}ENDPOINT", integration_prefix); + + temp_env::with_var(enabled_key, Some("true"), || { + temp_env::with_var( + endpoint_key, + Some("https://testlight-env.test/auction"), + || { + // Step 1: Parse with env vars (what build.rs does) + let settings = + Settings::from_toml_and_env(&toml_str).expect("Settings should parse"); + + // Verify normalization converted "true" to bool + let raw = settings.integrations.get("testlight").unwrap(); + assert!( + raw.get("enabled").unwrap().is_boolean(), + "enabled should be normalized to bool, got: {:?}", + raw.get("enabled") + ); + + // Step 2: Serialize to TOML (what build.rs does) + let merged_toml = + toml::to_string_pretty(&settings).expect("Should serialize to TOML"); + + // Step 3: Parse back (what runtime does) + let runtime_settings = + Settings::from_toml(&merged_toml).expect("Runtime should parse"); + + let config = runtime_settings + .integration_config::("testlight") + .expect("should get config") + .expect("should be enabled"); + + assert_eq!(config.endpoint, "https://testlight-env.test/auction"); + assert!(config.enabled); + }, + ); + }); + } + + /// Verifies that from_toml does NOT read environment variables. + /// The runtime path should only use the pre-built TOML. + #[test] + fn test_from_toml_ignores_env_vars() { + let toml_str = crate_test_settings_str(); + + temp_env::with_var( + format!( + "{}{}PUBLISHER{}DOMAIN", + ENVIRONMENT_VARIABLE_PREFIX, + ENVIRONMENT_VARIABLE_SEPARATOR, + ENVIRONMENT_VARIABLE_SEPARATOR, + ), + Some("env-override.com"), + || { + let settings = Settings::from_toml(&toml_str).expect("should parse"); + assert_eq!( + settings.publisher.domain, "test-publisher.com", + "from_toml should ignore env vars" + ); + }, + ); + } + #[test] fn test_rewrite_is_excluded() { let rewrite = Rewrite { From c688e790f4081656fe296cda565fa01ccf9c74da Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:20:33 -0800 Subject: [PATCH 2/4] Fixed formatting --- crates/common/Cargo.toml | 2 +- crates/common/src/settings.rs | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 4ce1b74f..c360474e 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -41,6 +41,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } tokio = { workspace = true } +toml = { workspace = true } trusted-server-js = { path = "../js" } url = { workspace = true } urlencoding = { workspace = true } @@ -68,4 +69,3 @@ default = [] [dev-dependencies] temp-env = { workspace = true } tokio-test = { workspace = true } -toml = { workspace = true } diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index cf4c1145..dab11edb 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -56,7 +56,6 @@ impl Publisher { }) .unwrap_or_else(|| self.origin_url.clone()) } - } #[derive(Debug, Default, Clone, Deserialize, Serialize)] @@ -363,18 +362,10 @@ impl Settings { /// /// - [`TrustedServerError::Configuration`] if the TOML is invalid or missing required fields pub fn from_toml(toml_str: &str) -> Result> { - let toml = File::from_str(toml_str, FileFormat::Toml); - let config = Config::builder().add_source(toml).build().change_context( - TrustedServerError::Configuration { - message: "Failed to build configuration".to_string(), - }, - )?; let settings: Self = - config - .try_deserialize() - .change_context(TrustedServerError::Configuration { - message: "Failed to deserialize configuration".to_string(), - })?; + toml::from_str(toml_str).change_context(TrustedServerError::Configuration { + message: "Failed to deserialize TOML configuration".to_string(), + })?; Ok(settings) } From 54f8884bf25a036efdb6b108805f0951f9140250 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:38:04 -0800 Subject: [PATCH 3/4] Fixed clippy --- crates/common/src/settings.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index dab11edb..90b8ec12 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -1090,8 +1090,8 @@ mod tests { } /// Tests the full build.rs round-trip: env vars are baked into Settings - /// at build time via from_toml_and_env, serialized to TOML, then parsed - /// back at runtime via from_toml. Verifies that env-sourced integration + /// at build time via `from_toml_and_env`, serialized to TOML, then parsed + /// back at runtime via `from_toml`. Verifies that env-sourced integration /// values (strings like "true") are normalized to proper types so the /// serialized TOML has correct types. #[test] @@ -1145,7 +1145,7 @@ mod tests { }); } - /// Verifies that from_toml does NOT read environment variables. + /// Verifies that `from_toml` does NOT read environment variables. /// The runtime path should only use the pre-built TOML. #[test] fn test_from_toml_ignores_env_vars() { From 3522af484d30ebb450a5a1a3c4a4e9f52e4df2fd Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:19:21 -0800 Subject: [PATCH 4/4] Review fixes: remove dead Settings::new(), harden config pipeline - Remove Settings::new() which read raw source TOML instead of the build-output TOML, making it broken after the from_toml/from_toml_and_env split. The canonical runtime entry point is get_settings() in settings_data.rs. - Move InsecureSecretKey and certificate_check guards from the removed Settings::new() into get_settings() where they belong. - Add .validate() call in from_toml_and_env() so trailing-slash and other validation errors are caught at build time, not just at runtime. - Add ends_with(']') guard to vec_from_seq_or_map bracket fallback to prevent incorrect slicing on malformed input like "[foo". - Update docs and comments to reflect that env var overrides are build-time only (baked by build.rs), not runtime. - Remove unused `use core::str` import and test_settings_new test. --- crates/common/src/settings.rs | 83 +++--------------------------- crates/common/src/settings_data.rs | 16 +++++- docs/guide/configuration.md | 17 ++++-- 3 files changed, 34 insertions(+), 82 deletions(-) diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index 90b8ec12..57cb3951 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -1,5 +1,3 @@ -use core::str; - use config::{Config, Environment, File, FileFormat}; use error_stack::{Report, ResultExt}; use regex::Regex; @@ -322,37 +320,6 @@ pub struct Settings { #[allow(unused)] impl Settings { - /// Creates a new [`Settings`] instance from the embedded configuration file. - /// - /// Loads the configuration from the embedded `trusted-server.toml` file - /// and applies any environment variable overrides. - /// - /// # Errors - /// - /// - [`TrustedServerError::InvalidUtf8`] if the embedded TOML file contains invalid UTF-8 - /// - [`TrustedServerError::Configuration`] if the configuration is invalid or missing required fields - /// - [`TrustedServerError::InsecureSecretKey`] if the secret key is set to the default value - pub fn new() -> Result> { - let toml_bytes = include_bytes!("../../../trusted-server.toml"); - let toml_str = - str::from_utf8(toml_bytes).change_context(TrustedServerError::InvalidUtf8 { - message: "embedded trusted-server.toml file".to_string(), - })?; - - let settings = Self::from_toml(toml_str)?; - - // Validate that the secret key is not the default - if settings.synthetic.secret_key == "secret-key" { - return Err(Report::new(TrustedServerError::InsecureSecretKey)); - } - - if !settings.proxy.certificate_check { - log::warn!("INSECURE: proxy.certificate_check is disabled — TLS certificates will NOT be verified"); - } - - Ok(settings) - } - /// Creates a new [`Settings`] instance from a pre-built TOML string. /// /// Use this for the runtime path where the TOML has already been @@ -400,6 +367,13 @@ impl Settings { })?; settings.integrations.normalize(); + + settings.validate().map_err(|err| { + Report::new(TrustedServerError::Configuration { + message: format!("Build-time configuration validation failed: {err}"), + }) + })?; + Ok(settings) } @@ -516,7 +490,7 @@ where } JsonValue::String(s) => { let txt = s.trim(); - if txt.starts_with('[') { + if txt.starts_with('[') && txt.ends_with(']') { if let Ok(vec) = serde_json::from_str::>(txt) { return Ok(vec); } @@ -573,47 +547,6 @@ mod tests { }; use crate::test_support::tests::{crate_test_settings_str, create_test_settings}; - #[test] - fn test_settings_new() { - // Test that Settings::new() loads successfully - let settings = Settings::new(); - assert!(settings.is_ok(), "Settings should load from embedded TOML"); - - let settings = settings.expect("should load settings from embedded TOML"); - - assert!(!settings.publisher.domain.is_empty()); - assert!(!settings.publisher.cookie_domain.is_empty()); - assert!(!settings.publisher.origin_url.is_empty()); - - let prebid_cfg = settings - .integration_config::("prebid") - .expect("Prebid config query should succeed") - .expect("Prebid config should load from default settings"); - assert!(!prebid_cfg.server_url.is_empty()); - assert!( - settings - .integration_config::("nextjs") - .expect("Next.js config query should succeed") - .is_none(), - "Next.js integration should be disabled by default" - ); - let raw_nextjs = settings - .integrations - .get("nextjs") - .expect("embedded config should include nextjs block"); - assert_eq!(raw_nextjs["enabled"], json!(false)); - assert_eq!( - raw_nextjs["rewrite_attributes"], - json!(["href", "link", "siteBaseUrl", "siteProductionDomain", "url"]), - "Next.js rewrite attributes should include href/link/siteBaseUrl/siteProductionDomain/url for RSC navigation" - ); - - assert!(!settings.synthetic.counter_store.is_empty()); - assert!(!settings.synthetic.opid_store.is_empty()); - assert!(!settings.synthetic.secret_key.is_empty()); - assert!(!settings.synthetic.template.is_empty()); - } - #[test] fn test_settings_from_valid_toml() { let toml_str = crate_test_settings_str(); diff --git a/crates/common/src/settings_data.rs b/crates/common/src/settings_data.rs index 01967add..4f8e36a0 100644 --- a/crates/common/src/settings_data.rs +++ b/crates/common/src/settings_data.rs @@ -10,8 +10,10 @@ pub use crate::auction_config_types::AuctionConfig; const SETTINGS_DATA: &[u8] = include_bytes!("../../../target/trusted-server-out.toml"); /// Creates a new [`Settings`] instance from the embedded configuration file. -/// Loads the configuration from the embedded `trusted-server.toml` file -/// and applies any environment variable overrides. +/// +/// Loads the pre-built TOML that was generated by `build.rs` (base config +/// merged with any `TRUSTED_SERVER__` environment variable overrides at +/// build time). Environment variables are **not** read at runtime. /// /// # Errors /// @@ -32,6 +34,16 @@ pub fn get_settings() -> Result> { message: "Failed to validate configuration".to_string(), })?; + if settings.synthetic.secret_key == "secret-key" { + return Err(Report::new(TrustedServerError::InsecureSecretKey)); + } + + if !settings.proxy.certificate_check { + log::warn!( + "INSECURE: proxy.certificate_check is disabled — TLS certificates will NOT be verified" + ); + } + Ok(settings) } diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index d193e856..8ae8243b 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -7,7 +7,7 @@ Learn how to configure Trusted Server for your deployment. Trusted Server uses a flexible configuration system based on: 1. **TOML Files** - `trusted-server.toml` for base configuration -2. **Environment Variables** - Runtime overrides with `TRUSTED_SERVER__` prefix +2. **Environment Variables** - Build-time overrides with `TRUSTED_SERVER__` prefix (baked into the binary by `build.rs`) 3. **Fastly Stores** - KV/Config/Secret stores for runtime data ## Quick Start @@ -32,7 +32,9 @@ template = "{{ client_ip }}:{{ user_agent }}:{{ accept_language }}:{{ accept_enc ### Environment Variable Overrides -Override any setting at runtime: +Override any setting at build time. Environment variables are merged into the +config by `build.rs` and baked into the compiled binary — they are **not** read +at runtime. ```bash # Format: TRUSTED_SERVER__SECTION__FIELD @@ -97,7 +99,11 @@ bidders = ["kargo", "rubicon", "appnexus"] The sections below consolidate the full configuration reference on this page. -## Environment Variable Overrides +## Environment Variable Overrides (Build-Time) + +Environment variables with the `TRUSTED_SERVER__` prefix are merged into the +base TOML configuration by `build.rs` at compile time. The resulting config is +embedded in the binary. Changing an environment variable requires a rebuild. ### Format @@ -1041,6 +1047,7 @@ trusted-server.dev.toml # Development overrides **Environment Variables Not Applied**: +- Env vars are applied at **build time** only — rebuild after changing them - Verify prefix: `TRUSTED_SERVER__` - Check separator: `__` (double underscore) - Confirm variable is exported: `echo $VARIABLE_NAME` @@ -1051,9 +1058,9 @@ trusted-server.dev.toml # Development overrides **Print Loaded Config** (test only): ```rust -use trusted_server_common::settings::Settings; +use trusted_server_common::settings_data::get_settings; -let settings = Settings::new()?; +let settings = get_settings()?; println!("{:#?}", settings); ```