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
1 change: 1 addition & 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 @@ -25,7 +25,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 {
format!(
"TRUSTED_SERVER_CONFIG_{}_{}",
normalize_env_segment(store_name),
Expand Down Expand Up @@ -601,6 +603,15 @@ mod tests {
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};

#[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
1 change: 1 addition & 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 Down
147 changes: 138 additions & 9 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
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::settings_from_config_blob;
use trusted_server_core::ec::EcContext;
Expand Down Expand Up @@ -79,31 +81,78 @@ fn load_startup_settings() -> Result<Settings, Report<TrustedServerError>> {
Settings::from_toml(include_str!("../../../trusted-server.example.toml"))
}

/// 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 missing string values at `{primary_key}` and legacy `{legacy_key}`"
)]
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.get().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(),
})
})?;
settings_from_config_blob(envelope)
}

#[cfg(any(test, target_arch = "wasm32"))]
fn cloudflare_config_envelope(
value: &serde_json::Value,
) -> Result<&str, CloudflareConfigEnvelopeError> {
match value.get(CONFIG_BLOB_KEY) {
Some(envelope) => envelope
.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 @@ -625,3 +674,83 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
router.build()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[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 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_reports_missing_keys() {
let value = serde_json::json!({});

assert_eq!(
cloudflare_config_envelope(&value),
Err(CloudflareConfigEnvelopeError::Missing {
primary_key: CONFIG_BLOB_KEY,
legacy_key: LEGACY_CONFIG_BLOB_KEY,
}),
"missing config should name both accepted keys"
);
}

#[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"
);
}
}
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,6 +23,6 @@ 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":""}'
3 changes: 3 additions & 0 deletions crates/trusted-server-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ web-time = { workspace = true }
getrandom = { workspace = true, features = ["js"] }
uuid = { workspace = true, features = ["js"] }

[build-dependencies]
edgezero-core = { workspace = true }

[features]
default = []
# Exposes test-only constructors (e.g. `IntegrationRegistry::from_request_filters`)
Expand Down
33 changes: 33 additions & 0 deletions crates/trusted-server-core/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
use std::env;
use std::path::PathBuf;

use edgezero_core::manifest::ManifestLoader;

fn main() {
println!("cargo:rerun-if-changed=build.rs");

// Keep every adapter's compiled default synchronized with the repository manifest.
let manifest_path = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("should receive CARGO_MANIFEST_DIR from Cargo"),
)
.join("../..")
.join("edgezero.toml");
println!("cargo:rerun-if-changed={}", manifest_path.display());

let manifest = match ManifestLoader::from_path(&manifest_path) {
Ok(manifest) => manifest,
Err(error) => {
println!(
Comment thread
ChristianPavilonis marked this conversation as resolved.
"cargo::error=should load EdgeZero manifest at {}: {error}",
manifest_path.display()
);
std::process::exit(1);
}
};
let Some(config_store) = manifest.manifest().stores.config.as_ref() else {
println!(
"cargo::error=should declare [stores.config] in EdgeZero manifest at {}",
manifest_path.display()
);
std::process::exit(1);
};
let default_store_id = config_store.default_id();
println!("cargo:rustc-env=TRUSTED_SERVER_DEFAULT_CONFIG_STORE_ID={default_store_id}");
}
7 changes: 6 additions & 1 deletion crates/trusted-server-core/src/config_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ use error_stack::Report;
use crate::error::TrustedServerError;
use crate::settings::Settings;

/// Default logical config-store id, from `[stores.config].default` in `edgezero.toml`.
///
Comment thread
ChristianPavilonis marked this conversation as resolved.
/// Derived at build time so every adapter uses the repository manifest's default.
pub const DEFAULT_CONFIG_STORE_ID: &str = env!("TRUSTED_SERVER_DEFAULT_CONFIG_STORE_ID");

/// Default config-store key containing the Trusted Server app-config blob.
pub const CONFIG_BLOB_KEY: &str = "trusted_server_config";
pub const CONFIG_BLOB_KEY: &str = DEFAULT_CONFIG_STORE_ID;
Comment thread
ChristianPavilonis marked this conversation as resolved.
Comment thread
ChristianPavilonis marked this conversation as resolved.

/// Reconstruct validated [`Settings`] from a serialized config blob envelope.
///
Expand Down
42 changes: 39 additions & 3 deletions crates/trusted-server-core/src/settings_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ use error_stack::{Report, ResultExt};
use serde::Deserialize;
use sha2::{Digest as _, Sha256};

use crate::config_payload::settings_from_config_blob;
use crate::config_payload::{DEFAULT_CONFIG_STORE_ID, settings_from_config_blob};
use crate::error::TrustedServerError;
use crate::platform::{PlatformConfigStore, StoreName};
use crate::settings::Settings;

const DEFAULT_CONFIG_STORE_ID: &str = "trusted_server_config";
const FASTLY_CHUNK_POINTER_KIND: &str = "fastly_config_chunks";
const FASTLY_CONFIG_ENTRY_LIMIT: usize = 8_000;

Expand Down Expand Up @@ -41,12 +40,21 @@ pub fn config_key(env: &EnvConfig) -> String {
}

/// Returns the default `EdgeZero` app-config store name.
///
/// Process-environment overrides apply to native adapters such as Axum. Fastly
/// has no process environment, so it uses the manifest default as the logical
/// name and resolves the physical store through a resource link.
Comment thread
ChristianPavilonis marked this conversation as resolved.
#[must_use]
pub fn default_config_store_name() -> StoreName {
Comment thread
ChristianPavilonis marked this conversation as resolved.
config_store_name(&EnvConfig::from_env())
}

/// Returns the default config-store key containing the app-config blob.
///
/// Process-environment overrides apply to native adapters such as Axum. When
/// using a key override, pass the same value to `ts config push --key`; the CLI
/// otherwise writes at the logical store ID. Fastly has no process environment,
/// so its custom entry point uses the manifest default key.
#[must_use]
pub fn default_config_key() -> String {
config_key(&EnvConfig::from_env())
Expand Down Expand Up @@ -188,7 +196,7 @@ fn configuration_error<T>(message: String) -> Result<T, Report<TrustedServerErro
#[cfg(test)]
mod tests {
use super::*;
use crate::config_payload::CONFIG_BLOB_KEY;
use crate::config_payload::{CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID};
use crate::platform::PlatformError;
use crate::settings::Settings;
use crate::test_support::tests::crate_test_settings_str;
Expand Down Expand Up @@ -231,6 +239,34 @@ mod tests {
serde_json::to_string(&envelope).expect("should serialize envelope")
}

#[test]
Comment thread
ChristianPavilonis marked this conversation as resolved.
fn config_defaults_match_edgezero_manifest() {
let manifest = edgezero_core::manifest::ManifestLoader::try_load_from_str(include_str!(
"../../../edgezero.toml"
))
.expect("should load the repository EdgeZero manifest");
let manifest_default = manifest
.manifest()
.stores
.config
.as_ref()
.expect("should declare [stores.config]")
.default_id();

assert_eq!(
DEFAULT_CONFIG_STORE_ID, manifest_default,
"compiled default should match edgezero.toml"
);
assert_eq!(
CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID,
"default blob key should match the default config store id"
);
assert_eq!(
manifest_default, "trusted_server_config",
Comment thread
ChristianPavilonis marked this conversation as resolved.
"Trusted Server should retain its expected default config store"
);
}

#[test]
fn config_selectors_default_to_the_logical_store_id() {
let env = EnvConfig::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
data = "test-api-key"

[local_server.config_stores]
# Generated integration configs inject the trusted_server_config blob
# Generated integration configs inject the manifest-default app-config blob
# into the store required by the Fastly entry point.
# GENERATED_TRUSTED_SERVER_CONFIG_STORES

Expand Down
Loading
Loading