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 @@ -620,3 +669,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":""}'
12 changes: 5 additions & 7 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::proxy::{AssetProxyCachePolicy, stream_asset_body};
use trusted_server_core::response_privacy::TerminalPrivateResponse;
use trusted_server_core::settings::Settings;
use trusted_server_core::settings_data::default_config_store_name;

mod app;
mod backend;
Expand All @@ -46,18 +47,15 @@ use crate::middleware::{HEADER_X_TS_FINALIZED, apply_finalize_headers, resolve_g
use crate::platform::{FastlyPlatformGeo, client_info_from_request};
use crate::rate_limiter::{FastlyRateLimiter, RATE_COUNTER_NAME};

const TRUSTED_SERVER_CONFIG_STORE: &str = "trusted_server_config";

/// Opens the Fastly Config Store used by the `EdgeZero` dispatcher.
/// Opens the manifest-default Fastly Config Store used by the `EdgeZero` dispatcher.
///
/// # Errors
///
/// Returns [`fastly::Error`] if the config store cannot be opened.
fn open_trusted_server_config_store() -> Result<ConfigStoreHandle, fastly::Error> {
let store = EdgeZeroFastlyConfigStore::try_open(TRUSTED_SERVER_CONFIG_STORE).map_err(|e| {
fastly::Error::msg(format!(
"failed to open config store `{TRUSTED_SERVER_CONFIG_STORE}`: {e}"
))
let store_name = default_config_store_name();
Comment thread
ChristianPavilonis marked this conversation as resolved.
let store = EdgeZeroFastlyConfigStore::try_open(store_name.as_ref()).map_err(|e| {
fastly::Error::msg(format!("failed to open config store `{store_name}`: {e}"))
})?;
Ok(ConfigStoreHandle::new(Arc::new(store)))
}
Expand Down
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 on lines 19 to +20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingCONFIG_BLOB_KEY is now an alias for the store id, but it is not interchangeable with default_config_key().

default_config_key() honours EDGEZERO__STORES__CONFIG__<ID>__KEY; this constant never does. The consumers are already split along that line — crates/trusted-server-adapter-fastly/src/app.rs and crates/trusted-server-adapter-axum/src/app.rs read through the env-aware accessor, while generate-viceroy-config.rs and tests/common/config.rs use the constant. That is safe today (the Fastly guest has no process environment, and the generator is host-side), but after this PR the two names read as synonyms and one silently ignores an override the other honours. A line of doc keeps the next reader out of that trap:

Suggested change
/// 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;
/// Default config-store key containing the Trusted Server app-config blob.
///
/// This is the *un-overridden* default. Runtime readers that must honour
/// `EDGEZERO__STORES__CONFIG__<ID>__KEY` call
/// [`crate::settings_data::default_config_key`] instead.
pub const CONFIG_BLOB_KEY: &str = DEFAULT_CONFIG_STORE_ID;

(Verified in a scratch worktree at this head: cargo fmt --all -- --check, cargo clippy-fastly, and cargo doc -p trusted-server-core --no-deps all clean — the new intra-doc link resolves.)


/// Reconstruct validated [`Settings`] from a serialized config blob envelope.
///
Expand Down
Loading
Loading