Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion crates/trusted-server-adapter-axum/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ fn normalize_env_segment(s: &str) -> String {
s.to_uppercase().replace(['-', '.', ' '], "_")
}

fn config_env_var(store_name: &str, key: &str) -> String {
/// Returns the environment-variable name for a config store entry.
#[must_use]
pub fn config_env_var(store_name: &str, key: &str) -> String {
Comment thread
ChristianPavilonis marked this conversation as resolved.
format!(
"TRUSTED_SERVER_CONFIG_{}_{}",
normalize_env_segment(store_name),
Expand Down Expand Up @@ -608,6 +610,15 @@ mod tests {
);
}

#[test]
fn config_env_var_normalizes_store_and_key() {
assert_eq!(
config_env_var("my-store.name", "my key"),
"TRUSTED_SERVER_CONFIG_MY_STORE_NAME_MY_KEY",
"should normalize environment-variable segments"
);
}

#[test]
fn config_store_reads_from_env_var() {
temp_env::with_var(
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-cloudflare/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cloudflare = ["edgezero-adapter-cloudflare/cloudflare", "dep:worker"]
[dependencies]
async-trait = { workspace = true }
bytes = { workspace = true }
derive_more = { workspace = true }
edgezero-adapter-cloudflare = { workspace = true }
edgezero-core = { workspace = true }
error-stack = { workspace = true }
Expand All @@ -42,4 +43,5 @@ worker = { workspace = true }
trusted-server-core = { workspace = true, features = ["test-utils"] }
base64 = { workspace = true }
edgezero-core = { workspace = true }
toml = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
170 changes: 161 additions & 9 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use trusted_server_core::auction::{
AuctionOrchestrator, build_orchestrator_with_plan, compile_auction_plan,
};
use trusted_server_core::cache_policy::EdgeCacheHeader;
#[cfg(any(test, target_arch = "wasm32"))]
use trusted_server_core::config_payload::CONFIG_BLOB_KEY;
#[cfg(target_arch = "wasm32")]
use trusted_server_core::config_payload::{DEFAULT_SECRET_STORE_ID, settings_from_config_blob};
use trusted_server_core::ec::EcContext;
Expand Down Expand Up @@ -99,29 +101,52 @@ fn load_startup_settings() -> Result<Settings, Report<TrustedServerError>> {
.attach("use TrustedServerApp::routes_with_settings for host tests"))
}

/// Older Cloudflare bindings used this JSON property before config stores adopted
/// the manifest-derived default.
///
/// Remove this fallback only when support for those bindings is deliberately retired.
#[cfg(any(test, target_arch = "wasm32"))]
const LEGACY_CONFIG_BLOB_KEY: &str = "app_config";

#[cfg(any(test, target_arch = "wasm32"))]
#[derive(Debug, Eq, PartialEq, derive_more::Display)]
enum CloudflareConfigEnvelopeError {
Comment thread
ChristianPavilonis marked this conversation as resolved.
Comment thread
ChristianPavilonis marked this conversation as resolved.
#[display(
"Cloudflare TRUSTED_SERVER_CONFIG has no `{primary_key}` or legacy `{legacy_key}` property"
)]
Missing {
primary_key: &'static str,
legacy_key: &'static str,
},
#[display("Cloudflare TRUSTED_SERVER_CONFIG value at `{key}` must be a string")]
NonString { key: &'static str },
}

#[cfg(any(test, target_arch = "wasm32"))]
impl core::error::Error for CloudflareConfigEnvelopeError {}

#[cfg(target_arch = "wasm32")]
fn settings_from_cloudflare_config_json() -> Result<Settings, Report<TrustedServerError>> {
let raw_config = CLOUDFLARE_CONFIG_JSON.with(|slot| slot.get().cloned());
let raw_config = raw_config.ok_or_else(|| {
Report::new(TrustedServerError::Configuration {
message: "Cloudflare TRUSTED_SERVER_CONFIG is required".to_string(),
})
.attach("set TRUSTED_SERVER_CONFIG to JSON containing the app_config blob envelope")
.attach(format!(
"set TRUSTED_SERVER_CONFIG to JSON containing the `{CONFIG_BLOB_KEY}` blob envelope"
))
})?;
let value: serde_json::Value = serde_json::from_str(&raw_config).map_err(|error| {
Report::new(TrustedServerError::Configuration {
message: "invalid Cloudflare TRUSTED_SERVER_CONFIG JSON".to_string(),
})
.attach(format!("failed to parse TRUSTED_SERVER_CONFIG: {error}"))
})?;
let envelope = value
.get("app_config")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
Report::new(TrustedServerError::Configuration {
message: "Cloudflare TRUSTED_SERVER_CONFIG missing app_config".to_string(),
})
})?;
let envelope = cloudflare_config_envelope(&value).map_err(|error| {
Report::new(TrustedServerError::Configuration {
message: error.to_string(),
})
})?;
let env = CLOUDFLARE_ENV
.with(|slot| slot.get().cloned())
.ok_or_else(|| {
Expand All @@ -134,6 +159,34 @@ fn settings_from_cloudflare_config_json() -> Result<Settings, Report<TrustedServ
settings_from_config_blob(envelope, &secret_store, &default_secret_store)
}

#[cfg(any(test, target_arch = "wasm32"))]
fn cloudflare_config_envelope(
value: &serde_json::Value,
) -> Result<&str, CloudflareConfigEnvelopeError> {
match value.get(CONFIG_BLOB_KEY).filter(|envelope| {
// Treat a blank placeholder as absent so a populated legacy property
// remains usable during migration.
envelope.as_str() != Some("")
}) {
Some(envelope) => envelope
Comment thread
ChristianPavilonis marked this conversation as resolved.
.as_str()
.ok_or(CloudflareConfigEnvelopeError::NonString {
key: CONFIG_BLOB_KEY,
}),
None => match value.get(LEGACY_CONFIG_BLOB_KEY) {
Some(envelope) => envelope
.as_str()
.ok_or(CloudflareConfigEnvelopeError::NonString {
key: LEGACY_CONFIG_BLOB_KEY,
}),
None => Err(CloudflareConfigEnvelopeError::Missing {
primary_key: CONFIG_BLOB_KEY,
legacy_key: LEGACY_CONFIG_BLOB_KEY,
}),
},
}
}

/// Build the application state from explicit settings.
///
/// # Errors
Expand Down Expand Up @@ -745,6 +798,20 @@ mod tests {
);
}

#[test]
fn cloudflare_config_prefers_manifest_default_key() {
let value = serde_json::json!({
LEGACY_CONFIG_BLOB_KEY: "legacy-envelope",
CONFIG_BLOB_KEY: "manifest-envelope",
});

assert_eq!(
cloudflare_config_envelope(&value),
Ok("manifest-envelope"),
"manifest-derived key should take precedence"
);
}

#[test]
fn disabled_startup_accepts_dormant_multi_provider_auction_plan() {
let mut settings = Settings::from_toml(
Expand Down Expand Up @@ -838,4 +905,89 @@ mod tests {
"should identify unsupported fanout: {error:?}"
);
}

#[test]
fn cloudflare_config_accepts_legacy_app_config_key() {
let value = serde_json::json!({ LEGACY_CONFIG_BLOB_KEY: "legacy-envelope" });

assert_eq!(
cloudflare_config_envelope(&value),
Ok("legacy-envelope"),
"legacy app_config key should remain compatible"
);
}

#[test]
fn cloudflare_config_treats_blank_primary_as_absent() {
let value = serde_json::json!({
CONFIG_BLOB_KEY: "",
LEGACY_CONFIG_BLOB_KEY: "legacy-envelope",
});

assert_eq!(
cloudflare_config_envelope(&value),
Ok("legacy-envelope"),
"blank primary should not shadow a populated legacy property"
);
}

#[test]
fn cloudflare_config_reports_missing_keys() {
let value = serde_json::json!({});

let error = cloudflare_config_envelope(&value)
.expect_err("should reject config without either accepted property");

assert_eq!(
error,
CloudflareConfigEnvelopeError::Missing {
primary_key: CONFIG_BLOB_KEY,
legacy_key: LEGACY_CONFIG_BLOB_KEY,
},
"missing config should name both accepted keys"
);
assert_eq!(
error.to_string(),
format!(
"Cloudflare TRUSTED_SERVER_CONFIG has no `{CONFIG_BLOB_KEY}` or legacy `{LEGACY_CONFIG_BLOB_KEY}` property"
),
"missing config should report absent properties, not invalid types"
);
}

#[test]
fn cloudflare_config_does_not_mask_malformed_manifest_value() {
Comment thread
ChristianPavilonis marked this conversation as resolved.
let value = serde_json::json!({
LEGACY_CONFIG_BLOB_KEY: "legacy-envelope",
CONFIG_BLOB_KEY: true,
});

assert_eq!(
cloudflare_config_envelope(&value),
Err(CloudflareConfigEnvelopeError::NonString {
key: CONFIG_BLOB_KEY,
}),
"malformed manifest-derived value should not fall back"
);
}

#[test]
fn cloudflare_config_reports_malformed_legacy_value() {
let value = serde_json::json!({ LEGACY_CONFIG_BLOB_KEY: false });
let error = cloudflare_config_envelope(&value)
.expect_err("should reject a malformed legacy config value");

assert_eq!(
error,
CloudflareConfigEnvelopeError::NonString {
key: LEGACY_CONFIG_BLOB_KEY,
},
"malformed legacy value should name the legacy key"
);
assert_eq!(
error.to_string(),
"Cloudflare TRUSTED_SERVER_CONFIG value at `app_config` must be a string",
"configuration error should name the malformed legacy key"
);
}
}
38 changes: 38 additions & 0 deletions crates/trusted-server-adapter-cloudflare/tests/config_defaults.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Keep the checked-in Cloudflare configuration aligned with the runtime defaults.

use trusted_server_core::config_payload::{CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID};

#[test]
fn cloudflare_manifest_uses_the_runtime_config_store_id() {
let manifest: toml::Value = toml::from_str(include_str!("../cloudflare.toml"))
.expect("should parse the Cloudflare manifest");

assert_eq!(
manifest["stores"]["config"]["name"].as_str(),
Some(DEFAULT_CONFIG_STORE_ID),
"Cloudflare config store should match the manifest-derived runtime default"
);
}

#[test]
fn wrangler_placeholder_uses_the_runtime_blob_key() {
let manifest: toml::Value = toml::from_str(include_str!("../wrangler.toml"))
.expect("should parse the Wrangler manifest");
let raw_config = manifest["vars"]["TRUSTED_SERVER_CONFIG"]
.as_str()
.expect("should declare the config JSON binding");
let config: serde_json::Value =
serde_json::from_str(raw_config).expect("should parse the config JSON placeholder");
let entries = config
.as_object()
.expect("should contain config properties");

assert_eq!(entries.len(), 1, "should declare only the current blob key");
assert_eq!(
entries
.get(CONFIG_BLOB_KEY)
.and_then(serde_json::Value::as_str),
Some(""),
"placeholder should use the runtime key and remain invalid until seeded"
);
}
6 changes: 3 additions & 3 deletions crates/trusted-server-adapter-cloudflare/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"

[vars]
# TRUSTED_SERVER_CONFIG is required at startup. Replace this intentionally
# invalid placeholder with JSON containing an `app_config` blob envelope before
# deploying or running `wrangler dev` against real traffic.
TRUSTED_SERVER_CONFIG = '{"app_config":""}'
# invalid placeholder with JSON containing the manifest-default app-config blob
# envelope before deploying or running `wrangler dev` against real traffic.
TRUSTED_SERVER_CONFIG = '{"trusted_server_config":""}'

# App-config secret values are provisioned as Worker secrets with
# `wrangler secret put <key-name>`. The pushed blob contains only those key
Expand Down
Loading
Loading