From cbbdd041009181fcee7874691f68999c5d6249b1 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 9 Jul 2026 15:34:39 -0500 Subject: [PATCH 1/7] Honor EdgeZero app config store default --- .../src/app.rs | 85 +++++++++++++++++-- .../wrangler.toml | 6 +- crates/trusted-server-core/Cargo.toml | 3 + crates/trusted-server-core/build.rs | 23 ++++- .../trusted-server-core/src/config_payload.rs | 4 +- .../trusted-server-core/src/settings_data.rs | 31 ++++++- .../fixtures/configs/viceroy-template.toml | 2 +- .../src/bin/generate-viceroy-config.rs | 16 ++-- .../tests/common/config.rs | 7 +- .../tests/environments/axum.rs | 10 ++- docs/guide/configuration.md | 29 ++++--- edgezero.toml | 2 +- 12 files changed, 172 insertions(+), 46 deletions(-) diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 7ea582ee8..dbbb6812a 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -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; @@ -105,7 +107,9 @@ fn settings_from_cloudflare_config_json() -> Result Result Result Option<&str> { + const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; + + match value.get(CONFIG_BLOB_KEY) { + Some(envelope) => envelope.as_str(), + None if CONFIG_BLOB_KEY != LEGACY_CONFIG_BLOB_KEY => value + .get(LEGACY_CONFIG_BLOB_KEY) + .and_then(serde_json::Value::as_str), + None => None, + } +} + /// Build the application state from explicit settings. /// /// # Errors @@ -708,6 +724,34 @@ mod tests { ); } + fn config_value(entries: &[(&str, &str)]) -> serde_json::Value { + serde_json::Value::Object( + entries + .iter() + .map(|(key, value)| { + ( + (*key).to_string(), + serde_json::Value::String((*value).to_string()), + ) + }) + .collect(), + ) + } + + #[test] + fn cloudflare_config_prefers_manifest_default_key() { + let value = config_value(&[ + ("app_config", "legacy-envelope"), + (CONFIG_BLOB_KEY, "manifest-envelope"), + ]); + + assert_eq!( + cloudflare_config_envelope(&value), + Some("manifest-envelope"), + "manifest-derived key should take precedence" + ); + } + #[test] fn disabled_startup_accepts_dormant_multi_provider_auction_plan() { let mut settings = Settings::from_toml( @@ -801,4 +845,27 @@ mod tests { "should identify unsupported fanout: {error:?}" ); } + + #[test] + fn cloudflare_config_accepts_legacy_app_config_key() { + let value = config_value(&[("app_config", "legacy-envelope")]); + + assert_eq!( + cloudflare_config_envelope(&value), + Some("legacy-envelope"), + "legacy app_config key should remain compatible" + ); + } + + #[test] + fn cloudflare_config_does_not_mask_malformed_manifest_value() { + let mut value = config_value(&[("app_config", "legacy-envelope")]); + value[CONFIG_BLOB_KEY] = serde_json::Value::Bool(true); + + assert_eq!( + cloudflare_config_envelope(&value), + None, + "malformed manifest-derived value should not fall back" + ); + } } diff --git a/crates/trusted-server-adapter-cloudflare/wrangler.toml b/crates/trusted-server-adapter-cloudflare/wrangler.toml index 48eb2db8d..f3b5ef6aa 100644 --- a/crates/trusted-server-adapter-cloudflare/wrangler.toml +++ b/crates/trusted-server-adapter-cloudflare/wrangler.toml @@ -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 `. The pushed blob contains only those key diff --git a/crates/trusted-server-core/Cargo.toml b/crates/trusted-server-core/Cargo.toml index 01780dd39..3dbf41df3 100644 --- a/crates/trusted-server-core/Cargo.toml +++ b/crates/trusted-server-core/Cargo.toml @@ -59,6 +59,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`) diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index c2bce4fe2..f8b9d38ba 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -1,3 +1,24 @@ +use std::env; +use std::path::PathBuf; + +use edgezero_core::manifest::ManifestLoader; + fn main() { - println!("cargo:rerun-if-changed=build.rs"); + 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 = ManifestLoader::from_path(&manifest_path) + .expect("should load the repository EdgeZero manifest"); + let config_store = manifest + .manifest() + .stores + .config + .as_ref() + .expect("should declare [stores.config] in edgezero.toml"); + let default_store_id = config_store.default_id(); + println!("cargo:rustc-env=TRUSTED_SERVER_DEFAULT_CONFIG_STORE_ID={default_store_id}"); } diff --git a/crates/trusted-server-core/src/config_payload.rs b/crates/trusted-server-core/src/config_payload.rs index 497d48b3e..ae354b6de 100644 --- a/crates/trusted-server-core/src/config_payload.rs +++ b/crates/trusted-server-core/src/config_payload.rs @@ -18,7 +18,9 @@ use crate::settings::Settings; pub const DEFAULT_SECRET_STORE_ID: &str = "trusted_server_secrets"; /// Default config-store key containing the Trusted Server app-config blob. -pub const CONFIG_BLOB_KEY: &str = "trusted_server_config"; +/// +/// Derived from `[stores.config].default` in `edgezero.toml` at build time. +pub const CONFIG_BLOB_KEY: &str = env!("TRUSTED_SERVER_DEFAULT_CONFIG_STORE_ID"); /// Reconstruct runtime [`Settings`] from a serialized config blob envelope. /// diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index 103ac819c..82a1fc021 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -3,8 +3,7 @@ use error_stack::{Report, ResultExt}; use serde::Deserialize; use sha2::{Digest as _, Sha256}; -use crate::config_payload::DEFAULT_SECRET_STORE_ID; -use crate::config_payload::settings_from_config_blob; +use crate::config_payload::{settings_from_config_blob, CONFIG_BLOB_KEY, DEFAULT_SECRET_STORE_ID}; use crate::error::TrustedServerError; use crate::platform::{PlatformConfigStore, PlatformSecretStore, StoreName}; use crate::settings::Settings; @@ -33,13 +32,13 @@ struct FastlyChunkRef { /// Resolves the `EdgeZero` app-config store name from runtime configuration. #[must_use] pub fn config_store_name(env: &EnvConfig) -> StoreName { - StoreName::from(env.store_name("config", DEFAULT_CONFIG_STORE_ID)) + StoreName::from(env.store_name("config", CONFIG_BLOB_KEY)) } /// Resolves the config-store key containing the app-config blob. #[must_use] pub fn config_key(env: &EnvConfig) -> String { - env.store_key("config", DEFAULT_CONFIG_STORE_ID) + env.store_key("config", CONFIG_BLOB_KEY) } /// Returns the default `EdgeZero` app-config store name. @@ -280,6 +279,30 @@ mod tests { ) } + #[test] + 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!( + CONFIG_BLOB_KEY, manifest_default, + "compiled default should match edgezero.toml" + ); + assert_eq!( + manifest_default, "trusted_server_config", + "Trusted Server should retain its expected default config store" + ); + } + #[test] fn config_selectors_default_to_the_logical_store_id() { let env = EnvConfig::default(); diff --git a/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml b/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml index 816dcbfcf..eae7220a8 100644 --- a/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml +++ b/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml @@ -88,7 +88,7 @@ [local_server.config_stores.edgezero_runtime_env.contents] EDGEZERO__SERVICES__0000000000000000000000__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME = "ts_secrets" - # 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 diff --git a/crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs b/crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs index 58c26736e..4c2d80a99 100644 --- a/crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs +++ b/crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use edgezero_core::blob_envelope::BlobEnvelope; use trusted_server_core::config::TrustedServerAppConfig; +use trusted_server_core::config_payload::CONFIG_BLOB_KEY; const GENERATED_AT: &str = "2026-06-23T00:00:00Z"; const GENERATED_STORES_MARKER: &str = " # GENERATED_TRUSTED_SERVER_CONFIG_STORES"; @@ -148,10 +149,10 @@ fn inject_generated_config_stores(template: &str, envelope_json: &str) -> Result fn generated_config_store_blocks(envelope_json: &str) -> String { format!( r#" # Generated by generate-viceroy-config. Do not edit generated output. - [local_server.config_stores.trusted_server_config] + [local_server.config_stores.{CONFIG_BLOB_KEY}] format = "inline-toml" - [local_server.config_stores.trusted_server_config.contents] - trusted_server_config = '''{envelope_json}'''"# + [local_server.config_stores.{CONFIG_BLOB_KEY}.contents] + {CONFIG_BLOB_KEY} = '''{envelope_json}'''"# ) } @@ -277,8 +278,8 @@ mod tests { .expect("should inject generated stores"); assert!( - generated.contains("[local_server.config_stores.trusted_server_config]"), - "should include app config store" + generated.contains(&format!("[local_server.config_stores.{CONFIG_BLOB_KEY}]")), + "should include manifest-default app config store" ); assert!( !generated.contains("edgezero_enabled"), @@ -302,11 +303,10 @@ mod tests { let parsed: toml::Value = toml::from_str(&generated).expect("should parse as TOML"); assert_eq!( - parsed["local_server"]["config_stores"]["trusted_server_config"]["contents"] - ["trusted_server_config"] + parsed["local_server"]["config_stores"][CONFIG_BLOB_KEY]["contents"][CONFIG_BLOB_KEY] .as_str(), Some(envelope.as_str()), - "trusted_server_config should contain the app-config blob" + "manifest-default config store should contain the app-config blob" ); } diff --git a/crates/trusted-server-integration-tests/tests/common/config.rs b/crates/trusted-server-integration-tests/tests/common/config.rs index d1fddcb95..c6b5c054a 100644 --- a/crates/trusted-server-integration-tests/tests/common/config.rs +++ b/crates/trusted-server-integration-tests/tests/common/config.rs @@ -1,6 +1,7 @@ use edgezero_core::blob_envelope::BlobEnvelope; use error_stack::Report; use trusted_server_core::config::TrustedServerAppConfig; +use trusted_server_core::config_payload::CONFIG_BLOB_KEY; use crate::common::runtime::{TestError, TestResult}; @@ -35,7 +36,11 @@ pub fn integration_app_config_envelope(origin_port: u16) -> TestResult { pub fn cloudflare_config_json(origin_port: u16) -> TestResult { let envelope = integration_app_config_envelope(origin_port)?; - serde_json::to_string(&serde_json::json!({ "app_config": envelope })).map_err(|error| { + let config = serde_json::Value::Object(serde_json::Map::from_iter([( + CONFIG_BLOB_KEY.to_string(), + serde_json::Value::String(envelope), + )])); + serde_json::to_string(&config).map_err(|error| { Report::new(TestError::ConfigGeneration).attach(format!( "failed to serialize Cloudflare config binding: {error}" )) diff --git a/crates/trusted-server-integration-tests/tests/environments/axum.rs b/crates/trusted-server-integration-tests/tests/environments/axum.rs index 3623d8491..b2e812653 100644 --- a/crates/trusted-server-integration-tests/tests/environments/axum.rs +++ b/crates/trusted-server-integration-tests/tests/environments/axum.rs @@ -6,6 +6,7 @@ use error_stack::ResultExt as _; use std::io::{BufRead as _, BufReader}; use std::path::Path; use std::process::{Child, Command, Stdio}; +use trusted_server_core::config_payload::CONFIG_BLOB_KEY; /// Default port the Axum dev server binds to when no `PORT` env var is supplied. const AXUM_DEFAULT_PORT: u16 = 8787; @@ -57,13 +58,14 @@ impl RuntimeEnvironment for AxumDevServer { let port = super::find_available_port().unwrap_or(AXUM_DEFAULT_PORT); let app_config = integration_app_config_envelope(origin_port())?; + let config_segment = CONFIG_BLOB_KEY + .to_ascii_uppercase() + .replace(['-', '.', ' '], "_"); + let config_variable = format!("TRUSTED_SERVER_CONFIG_{config_segment}_{config_segment}"); let mut child = Command::new(&binary) .env("PORT", port.to_string()) - .env( - "TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG", - app_config, - ) + .env(config_variable, app_config) .envs(INTEGRATION_SECRET_ENV.iter().copied()) .stdout(Stdio::null()) .stderr(Stdio::piped()) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 89dc43aea..3bcf6ebcc 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2094,25 +2094,28 @@ After the EdgeZero cutover, the Fastly adapter always dispatches through the EdgeZero entry point. The former `edgezero_enabled` and `edgezero_rollout_pct` canary keys are no longer read. -The Fastly service must still provide a `trusted_server_config` config store -because the entry point opens it before dispatch and passes the handle to -EdgeZero-backed platform services. The store may be empty unless another feature -adds keys to it. +The Fastly service must provide the logical config store selected by +`[stores.config].default` in `edgezero.toml`. By default, the logical store ID +is also the platform store name and the app-config blob key. Deployments can +override those independently with +`EDGEZERO__STORES__CONFIG____NAME` and +`EDGEZERO__STORES__CONFIG____KEY`. -**Local development** (`fastly.toml`): +The resolved store and key must contain a valid Trusted Server app-config blob +envelope. An absent or empty entry makes application startup fail closed. Use +the Trusted Server CLI to provision the store and publish the validated config. -```toml -[local_server.config_stores] - [local_server.config_stores.trusted_server_config] - format = "inline-toml" - [local_server.config_stores.trusted_server_config.contents] +**Local development** (writes the entry used by Viceroy in `fastly.toml`): + +```bash +ts config push --adapter fastly --local ``` -**Production setup** (Fastly CLI): +**Production setup**: ```bash -# Create the store once and attach it to the service. -fastly config-store create --name trusted_server_config +ts provision --adapter fastly +ts config push --adapter fastly ``` Rollback to the legacy entry point is no longer controlled by runtime config diff --git a/edgezero.toml b/edgezero.toml index 2120ca5c9..1695e8023 100644 --- a/edgezero.toml +++ b/edgezero.toml @@ -16,7 +16,7 @@ version = "0.1.0" # -- Stores ------------------------------------------------------------------ # Logical store ids only. These are the portable Trusted Server names; the # physical store each adapter binds is overridable out of band (Fastly binds -# `ec_identity_store`/`app_config`/secret stores in `fastly.toml`, Spin via its +# `ec_identity_store`/`trusted_server_config`/secret stores in `fastly.toml`, Spin via its # runtime config, Cloudflare via bindings), so the ids here do not have to match # any one platform's names. `default` is the primary logical id. From 3a1b677cce7638b248c5d5285df17937228a4441 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 17 Aug 2026 12:56:56 -0500 Subject: [PATCH 2/7] Address app config review feedback --- .../src/app.rs | 126 +++++++++++++++--- crates/trusted-server-core/build.rs | 15 ++- .../trusted-server-core/src/config_payload.rs | 9 +- .../trusted-server-core/src/settings_data.rs | 15 ++- .../src/bin/generate-viceroy-config.rs | 13 +- .../tests/environments/axum.rs | 9 +- docs/guide/configuration.md | 13 +- edgezero.toml | 6 +- 8 files changed, 164 insertions(+), 42 deletions(-) diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index dbbb6812a..414c58ec3 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -100,6 +100,43 @@ fn load_startup_settings() -> Result> { .attach("use TrustedServerApp::routes_with_settings for host tests")) } +#[cfg(any(test, target_arch = "wasm32"))] +// 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. +const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; + +#[cfg(any(test, target_arch = "wasm32"))] +#[derive(Debug, Eq, PartialEq)] +enum CloudflareConfigEnvelopeError { + Missing { + primary_key: &'static str, + legacy_key: &'static str, + }, + NonString { + key: &'static str, + }, +} + +#[cfg(any(test, target_arch = "wasm32"))] +impl CloudflareConfigEnvelopeError { + fn configuration_message(&self) -> String { + match self { + Self::Missing { + primary_key, + legacy_key, + } => { + format!( + "Cloudflare TRUSTED_SERVER_CONFIG missing string values at `{primary_key}` and legacy `{legacy_key}`" + ) + } + Self::NonString { key } => { + format!("Cloudflare TRUSTED_SERVER_CONFIG value at `{key}` must be a string") + } + } + } +} + #[cfg(target_arch = "wasm32")] fn settings_from_cloudflare_config_json() -> Result> { let raw_config = CLOUDFLARE_CONFIG_JSON.with(|slot| slot.get().cloned()); @@ -117,11 +154,9 @@ fn settings_from_cloudflare_config_json() -> Result Result Option<&str> { - const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; - +fn cloudflare_config_envelope( + value: &serde_json::Value, +) -> Result<&str, CloudflareConfigEnvelopeError> { match value.get(CONFIG_BLOB_KEY) { - Some(envelope) => envelope.as_str(), - None if CONFIG_BLOB_KEY != LEGACY_CONFIG_BLOB_KEY => value - .get(LEGACY_CONFIG_BLOB_KEY) - .and_then(serde_json::Value::as_str), - None => None, + Some(envelope) => envelope + .as_str() + .ok_or(CloudflareConfigEnvelopeError::NonString { + key: CONFIG_BLOB_KEY, + }), + None if CONFIG_BLOB_KEY != LEGACY_CONFIG_BLOB_KEY => { + 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, + }), + } + } + None => Err(CloudflareConfigEnvelopeError::Missing { + primary_key: CONFIG_BLOB_KEY, + legacy_key: LEGACY_CONFIG_BLOB_KEY, + }), } } @@ -741,13 +795,13 @@ mod tests { #[test] fn cloudflare_config_prefers_manifest_default_key() { let value = config_value(&[ - ("app_config", "legacy-envelope"), + (LEGACY_CONFIG_BLOB_KEY, "legacy-envelope"), (CONFIG_BLOB_KEY, "manifest-envelope"), ]); assert_eq!( cloudflare_config_envelope(&value), - Some("manifest-envelope"), + Ok("manifest-envelope"), "manifest-derived key should take precedence" ); } @@ -848,24 +902,60 @@ mod tests { #[test] fn cloudflare_config_accepts_legacy_app_config_key() { - let value = config_value(&[("app_config", "legacy-envelope")]); + let value = config_value(&[(LEGACY_CONFIG_BLOB_KEY, "legacy-envelope")]); assert_eq!( cloudflare_config_envelope(&value), - Some("legacy-envelope"), + 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() { - let mut value = config_value(&[("app_config", "legacy-envelope")]); + let mut value = config_value(&[(LEGACY_CONFIG_BLOB_KEY, "legacy-envelope")]); value[CONFIG_BLOB_KEY] = serde_json::Value::Bool(true); assert_eq!( cloudflare_config_envelope(&value), - None, + 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.configuration_message(), + "Cloudflare TRUSTED_SERVER_CONFIG value at `app_config` must be a string", + "configuration error should name the malformed legacy key" + ); + } } diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index f8b9d38ba..4e31647ef 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -4,6 +4,9 @@ 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"), ) @@ -11,8 +14,16 @@ fn main() { .join("edgezero.toml"); println!("cargo:rerun-if-changed={}", manifest_path.display()); - let manifest = ManifestLoader::from_path(&manifest_path) - .expect("should load the repository EdgeZero manifest"); + let manifest = match ManifestLoader::from_path(&manifest_path) { + Ok(manifest) => manifest, + Err(error) => { + println!( + "cargo::error=should load EdgeZero manifest at {}: {error}", + manifest_path.display() + ); + std::process::exit(1); + } + }; let config_store = manifest .manifest() .stores diff --git a/crates/trusted-server-core/src/config_payload.rs b/crates/trusted-server-core/src/config_payload.rs index ae354b6de..a955d2af9 100644 --- a/crates/trusted-server-core/src/config_payload.rs +++ b/crates/trusted-server-core/src/config_payload.rs @@ -17,10 +17,13 @@ use crate::settings::Settings; /// Canonical logical secret store used by Trusted Server app-config secrets. pub const DEFAULT_SECRET_STORE_ID: &str = "trusted_server_secrets"; -/// Default config-store key containing the Trusted Server app-config blob. +/// Default logical config-store id, from `[stores.config].default` in `edgezero.toml`. /// -/// Derived from `[stores.config].default` in `edgezero.toml` at build time. -pub const CONFIG_BLOB_KEY: &str = env!("TRUSTED_SERVER_DEFAULT_CONFIG_STORE_ID"); +/// 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 = DEFAULT_CONFIG_STORE_ID; /// Reconstruct runtime [`Settings`] from a serialized config blob envelope. /// diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index 82a1fc021..bf7a1303f 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -3,7 +3,8 @@ use error_stack::{Report, ResultExt}; use serde::Deserialize; use sha2::{Digest as _, Sha256}; -use crate::config_payload::{settings_from_config_blob, CONFIG_BLOB_KEY, DEFAULT_SECRET_STORE_ID}; +pub use crate::config_payload::DEFAULT_CONFIG_STORE_ID; +use crate::config_payload::{settings_from_config_blob, DEFAULT_SECRET_STORE_ID}; use crate::error::TrustedServerError; use crate::platform::{PlatformConfigStore, PlatformSecretStore, StoreName}; use crate::settings::Settings; @@ -32,13 +33,13 @@ struct FastlyChunkRef { /// Resolves the `EdgeZero` app-config store name from runtime configuration. #[must_use] pub fn config_store_name(env: &EnvConfig) -> StoreName { - StoreName::from(env.store_name("config", CONFIG_BLOB_KEY)) + StoreName::from(env.store_name("config", DEFAULT_CONFIG_STORE_ID)) } /// Resolves the config-store key containing the app-config blob. #[must_use] pub fn config_key(env: &EnvConfig) -> String { - env.store_key("config", CONFIG_BLOB_KEY) + env.store_key("config", DEFAULT_CONFIG_STORE_ID) } /// Returns the default `EdgeZero` app-config store name. @@ -197,7 +198,7 @@ fn configuration_error(message: String) -> Result Result fn generated_config_store_blocks(envelope_json: &str) -> String { format!( r#" # Generated by generate-viceroy-config. Do not edit generated output. - [local_server.config_stores.{CONFIG_BLOB_KEY}] + [local_server.config_stores.{DEFAULT_CONFIG_STORE_ID}] format = "inline-toml" - [local_server.config_stores.{CONFIG_BLOB_KEY}.contents] + [local_server.config_stores.{DEFAULT_CONFIG_STORE_ID}.contents] {CONFIG_BLOB_KEY} = '''{envelope_json}'''"# ) } @@ -278,7 +278,9 @@ mod tests { .expect("should inject generated stores"); assert!( - generated.contains(&format!("[local_server.config_stores.{CONFIG_BLOB_KEY}]")), + generated.contains(&format!( + "[local_server.config_stores.{DEFAULT_CONFIG_STORE_ID}]" + )), "should include manifest-default app config store" ); assert!( @@ -303,7 +305,8 @@ mod tests { let parsed: toml::Value = toml::from_str(&generated).expect("should parse as TOML"); assert_eq!( - parsed["local_server"]["config_stores"][CONFIG_BLOB_KEY]["contents"][CONFIG_BLOB_KEY] + parsed["local_server"]["config_stores"][DEFAULT_CONFIG_STORE_ID]["contents"] + [CONFIG_BLOB_KEY] .as_str(), Some(envelope.as_str()), "manifest-default config store should contain the app-config blob" diff --git a/crates/trusted-server-integration-tests/tests/environments/axum.rs b/crates/trusted-server-integration-tests/tests/environments/axum.rs index b2e812653..bdfef74b8 100644 --- a/crates/trusted-server-integration-tests/tests/environments/axum.rs +++ b/crates/trusted-server-integration-tests/tests/environments/axum.rs @@ -6,7 +6,7 @@ use error_stack::ResultExt as _; use std::io::{BufRead as _, BufReader}; use std::path::Path; use std::process::{Child, Command, Stdio}; -use trusted_server_core::config_payload::CONFIG_BLOB_KEY; +use trusted_server_core::config_payload::{CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID}; /// Default port the Axum dev server binds to when no `PORT` env var is supplied. const AXUM_DEFAULT_PORT: u16 = 8787; @@ -58,10 +58,13 @@ impl RuntimeEnvironment for AxumDevServer { let port = super::find_available_port().unwrap_or(AXUM_DEFAULT_PORT); let app_config = integration_app_config_envelope(origin_port())?; - let config_segment = CONFIG_BLOB_KEY + let store_segment = DEFAULT_CONFIG_STORE_ID .to_ascii_uppercase() .replace(['-', '.', ' '], "_"); - let config_variable = format!("TRUSTED_SERVER_CONFIG_{config_segment}_{config_segment}"); + let key_segment = CONFIG_BLOB_KEY + .to_ascii_uppercase() + .replace(['-', '.', ' '], "_"); + let config_variable = format!("TRUSTED_SERVER_CONFIG_{store_segment}_{key_segment}"); let mut child = Command::new(&binary) .env("PORT", port.to_string()) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 3bcf6ebcc..06e860f98 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2097,9 +2097,16 @@ canary keys are no longer read. The Fastly service must provide the logical config store selected by `[stores.config].default` in `edgezero.toml`. By default, the logical store ID is also the platform store name and the app-config blob key. Deployments can -override those independently with -`EDGEZERO__STORES__CONFIG____NAME` and -`EDGEZERO__STORES__CONFIG____KEY`. +override the platform store name with +`EDGEZERO__STORES__CONFIG____NAME`; both the runtime and `ts config push` +honor that override. The runtime also reads +`EDGEZERO__STORES__CONFIG____KEY`, but `ts config push` requires a matching +`--key` argument to publish the blob at that key: + +```bash +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY=staging \ + ts config push --adapter fastly --key staging +``` The resolved store and key must contain a valid Trusted Server app-config blob envelope. An absent or empty entry makes application startup fail closed. Use diff --git a/edgezero.toml b/edgezero.toml index 1695e8023..b40b20281 100644 --- a/edgezero.toml +++ b/edgezero.toml @@ -16,9 +16,9 @@ version = "0.1.0" # -- Stores ------------------------------------------------------------------ # Logical store ids only. These are the portable Trusted Server names; the # physical store each adapter binds is overridable out of band (Fastly binds -# `ec_identity_store`/`trusted_server_config`/secret stores in `fastly.toml`, Spin via its -# runtime config, Cloudflare via bindings), so the ids here do not have to match -# any one platform's names. `default` is the primary logical id. +# `ec_identity_store`/`trusted_server_config`/secret stores in `fastly.toml`, +# Spin via its runtime config, Cloudflare via bindings), so the ids here do not +# have to match any one platform's names. `default` is the primary logical id. [stores.kv] ids = ["trusted_server_kv"] From 42a1ef8f96b2caba8f337c5e950351791794b5b3 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 20 Aug 2026 09:03:13 -0500 Subject: [PATCH 3/7] Address remaining config review feedback --- .../src/platform.rs | 13 ++++++- .../src/app.rs | 35 ++++++++----------- crates/trusted-server-core/build.rs | 13 +++---- .../tests/environments/axum.rs | 13 +++---- docs/guide/configuration.md | 29 ++++++++------- 5 files changed, 54 insertions(+), 49 deletions(-) diff --git a/crates/trusted-server-adapter-axum/src/platform.rs b/crates/trusted-server-adapter-axum/src/platform.rs index 7dcdd53d8..d13588115 100644 --- a/crates/trusted-server-adapter-axum/src/platform.rs +++ b/crates/trusted-server-adapter-axum/src/platform.rs @@ -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 { format!( "TRUSTED_SERVER_CONFIG_{}_{}", normalize_env_segment(store_name), @@ -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( diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 414c58ec3..0f3245c32 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -100,10 +100,11 @@ fn load_startup_settings() -> Result> { .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"))] -// 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. const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; #[cfg(any(test, target_arch = "wasm32"))] @@ -181,25 +182,17 @@ fn cloudflare_config_envelope( .ok_or(CloudflareConfigEnvelopeError::NonString { key: CONFIG_BLOB_KEY, }), - None if CONFIG_BLOB_KEY != LEGACY_CONFIG_BLOB_KEY => { - 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, + 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, - }), + None => Err(CloudflareConfigEnvelopeError::Missing { + primary_key: CONFIG_BLOB_KEY, + legacy_key: LEGACY_CONFIG_BLOB_KEY, + }), + }, } } diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index 4e31647ef..df790cb7f 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -24,12 +24,13 @@ fn main() { std::process::exit(1); } }; - let config_store = manifest - .manifest() - .stores - .config - .as_ref() - .expect("should declare [stores.config] in edgezero.toml"); + 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}"); } diff --git a/crates/trusted-server-integration-tests/tests/environments/axum.rs b/crates/trusted-server-integration-tests/tests/environments/axum.rs index bdfef74b8..6e1388cea 100644 --- a/crates/trusted-server-integration-tests/tests/environments/axum.rs +++ b/crates/trusted-server-integration-tests/tests/environments/axum.rs @@ -6,7 +6,8 @@ use error_stack::ResultExt as _; use std::io::{BufRead as _, BufReader}; use std::path::Path; use std::process::{Child, Command, Stdio}; -use trusted_server_core::config_payload::{CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID}; +use trusted_server_adapter_axum::platform::config_env_var; +use trusted_server_core::settings_data::{default_config_key, default_config_store_name}; /// Default port the Axum dev server binds to when no `PORT` env var is supplied. const AXUM_DEFAULT_PORT: u16 = 8787; @@ -58,13 +59,9 @@ impl RuntimeEnvironment for AxumDevServer { let port = super::find_available_port().unwrap_or(AXUM_DEFAULT_PORT); let app_config = integration_app_config_envelope(origin_port())?; - let store_segment = DEFAULT_CONFIG_STORE_ID - .to_ascii_uppercase() - .replace(['-', '.', ' '], "_"); - let key_segment = CONFIG_BLOB_KEY - .to_ascii_uppercase() - .replace(['-', '.', ' '], "_"); - let config_variable = format!("TRUSTED_SERVER_CONFIG_{store_segment}_{key_segment}"); + let store_name = default_config_store_name(); + let config_key = default_config_key(); + let config_variable = config_env_var(store_name.as_ref(), &config_key); let mut child = Command::new(&binary) .env("PORT", port.to_string()) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 06e860f98..e277651ac 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2094,23 +2094,26 @@ After the EdgeZero cutover, the Fastly adapter always dispatches through the EdgeZero entry point. The former `edgezero_enabled` and `edgezero_rollout_pct` canary keys are no longer read. -The Fastly service must provide the logical config store selected by -`[stores.config].default` in `edgezero.toml`. By default, the logical store ID -is also the platform store name and the app-config blob key. Deployments can -override the platform store name with -`EDGEZERO__STORES__CONFIG____NAME`; both the runtime and `ts config push` -honor that override. The runtime also reads -`EDGEZERO__STORES__CONFIG____KEY`, but `ts config push` requires a matching -`--key` argument to publish the blob at that key: +The Fastly service opens the logical config store selected by +`[stores.config].default` in `edgezero.toml` and reads the app-config blob at +its default key. A Fastly resource link maps that logical store name to the +physical config store for the service at runtime. + +For a service-specific physical store, create the store, link it with the +logical name, then push to that physical store: ```bash -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY=staging \ - ts config push --adapter fastly --key staging +fastly config-store create --name +fastly resource-link create --service-id --version \ + --resource-id --name trusted_server_config +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config push --adapter fastly ``` -The resolved store and key must contain a valid Trusted Server app-config blob -envelope. An absent or empty entry makes application startup fail closed. Use -the Trusted Server CLI to provision the store and publish the validated config. +The resource-link name must match the logical store ID, and the pushed store +must contain a valid Trusted Server app-config blob envelope at its default key. +An absent or empty entry makes application startup fail closed. Use the Trusted +Server CLI to provision the store and publish the validated config. **Local development** (writes the entry used by Viceroy in `fastly.toml`): From 06317ad01c5d8410e31c2d6001c6c01cee2118af Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 21 Aug 2026 13:40:14 -0500 Subject: [PATCH 4/7] Address remaining Fastly config review feedback --- .../trusted-server-core/src/settings_data.rs | 8 +++ .../tests/common/config.rs | 6 +-- docs/guide/configuration.md | 51 ++++++++++++++----- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index bf7a1303f..84bf174f0 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -43,12 +43,20 @@ 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. #[must_use] pub fn default_config_store_name() -> StoreName { 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. 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()) diff --git a/crates/trusted-server-integration-tests/tests/common/config.rs b/crates/trusted-server-integration-tests/tests/common/config.rs index c6b5c054a..3bcbfe53e 100644 --- a/crates/trusted-server-integration-tests/tests/common/config.rs +++ b/crates/trusted-server-integration-tests/tests/common/config.rs @@ -36,11 +36,7 @@ pub fn integration_app_config_envelope(origin_port: u16) -> TestResult { pub fn cloudflare_config_json(origin_port: u16) -> TestResult { let envelope = integration_app_config_envelope(origin_port)?; - let config = serde_json::Value::Object(serde_json::Map::from_iter([( - CONFIG_BLOB_KEY.to_string(), - serde_json::Value::String(envelope), - )])); - serde_json::to_string(&config).map_err(|error| { + serde_json::to_string(&serde_json::json!({ CONFIG_BLOB_KEY: envelope })).map_err(|error| { Report::new(TestError::ConfigGeneration).attach(format!( "failed to serialize Cloudflare config binding: {error}" )) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index e277651ac..8a04307e8 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2099,21 +2099,51 @@ The Fastly service opens the logical config store selected by its default key. A Fastly resource link maps that logical store name to the physical config store for the service at runtime. -For a service-specific physical store, create the store, link it with the -logical name, then push to that physical store: +Fastly store names are account-level. For a first deployment, the default setup +is safe only when no other service in the account uses the +`trusted_server_config` physical store: + +```bash +ts provision --adapter fastly +ts config push --adapter fastly --dry-run +ts config push --adapter fastly +``` + +For the first deployment of a service in a shared account, select a +service-specific physical store during both provisioning and config push. The +first deployment applies the generated setup entry and links the physical store +under the logical name: + +```bash +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts provision --adapter fastly +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config push --adapter fastly --dry-run +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config push --adapter fastly +``` + +For an already-deployed service, Fastly does not reapply setup entries. Create +and link the service-specific physical store explicitly, seed it, then activate +the cloned service version: ```bash fastly config-store create --name -fastly resource-link create --service-id --version \ +fastly resource-link create --service-id --version latest --autoclone \ --resource-id --name trusted_server_config +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config push --adapter fastly --dry-run EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ ts config push --adapter fastly +fastly service-version activate --service-id --version latest ``` -The resource-link name must match the logical store ID, and the pushed store -must contain a valid Trusted Server app-config blob envelope at its default key. -An absent or empty entry makes application startup fail closed. Use the Trusted -Server CLI to provision the store and publish the validated config. +Confirm that each dry run names the intended physical store before writing. In +a shared account, it must be the service-specific store rather than the +account-level default. The resource-link name must match the logical store ID, +and the physical store must contain a valid Trusted Server app-config blob +envelope at its default key. An absent or empty entry makes application startup +fail closed. **Local development** (writes the entry used by Viceroy in `fastly.toml`): @@ -2121,13 +2151,6 @@ Server CLI to provision the store and publish the validated config. ts config push --adapter fastly --local ``` -**Production setup**: - -```bash -ts provision --adapter fastly -ts config push --adapter fastly -``` - Rollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required. From 5850f5d587d72831bfe14f4aaafe5adc0963f047 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 24 Aug 2026 12:28:07 -0500 Subject: [PATCH 5/7] Address final app config review feedback --- Cargo.lock | 1 + .../Cargo.toml | 1 + .../src/app.rs | 62 ++++++------------- .../trusted-server-core/src/settings_data.rs | 11 ++-- docs/guide/configuration.md | 27 +++----- 5 files changed, 35 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 311597aae..2c7e21301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5365,6 +5365,7 @@ dependencies = [ "async-trait", "base64", "bytes", + "derive_more", "edgezero-adapter-cloudflare", "edgezero-core", "error-stack", diff --git a/crates/trusted-server-adapter-cloudflare/Cargo.toml b/crates/trusted-server-adapter-cloudflare/Cargo.toml index 097844012..2dfd9773d 100644 --- a/crates/trusted-server-adapter-cloudflare/Cargo.toml +++ b/crates/trusted-server-adapter-cloudflare/Cargo.toml @@ -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 } diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 0f3245c32..6dbd69393 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -108,35 +108,21 @@ fn load_startup_settings() -> Result> { const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; #[cfg(any(test, target_arch = "wasm32"))] -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, derive_more::Display)] enum CloudflareConfigEnvelopeError { + #[display( + "Cloudflare TRUSTED_SERVER_CONFIG missing string values at `{primary_key}` and legacy `{legacy_key}`" + )] Missing { primary_key: &'static str, legacy_key: &'static str, }, - NonString { - 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 CloudflareConfigEnvelopeError { - fn configuration_message(&self) -> String { - match self { - Self::Missing { - primary_key, - legacy_key, - } => { - format!( - "Cloudflare TRUSTED_SERVER_CONFIG missing string values at `{primary_key}` and legacy `{legacy_key}`" - ) - } - Self::NonString { key } => { - format!("Cloudflare TRUSTED_SERVER_CONFIG value at `{key}` must be a string") - } - } - } -} +impl core::error::Error for CloudflareConfigEnvelopeError {} #[cfg(target_arch = "wasm32")] fn settings_from_cloudflare_config_json() -> Result> { @@ -157,7 +143,7 @@ fn settings_from_cloudflare_config_json() -> Result serde_json::Value { - serde_json::Value::Object( - entries - .iter() - .map(|(key, value)| { - ( - (*key).to_string(), - serde_json::Value::String((*value).to_string()), - ) - }) - .collect(), - ) - } - #[test] fn cloudflare_config_prefers_manifest_default_key() { - let value = config_value(&[ - (LEGACY_CONFIG_BLOB_KEY, "legacy-envelope"), - (CONFIG_BLOB_KEY, "manifest-envelope"), - ]); + let value = serde_json::json!({ + LEGACY_CONFIG_BLOB_KEY: "legacy-envelope", + CONFIG_BLOB_KEY: "manifest-envelope", + }); assert_eq!( cloudflare_config_envelope(&value), @@ -895,7 +867,7 @@ mod tests { #[test] fn cloudflare_config_accepts_legacy_app_config_key() { - let value = config_value(&[(LEGACY_CONFIG_BLOB_KEY, "legacy-envelope")]); + let value = serde_json::json!({ LEGACY_CONFIG_BLOB_KEY: "legacy-envelope" }); assert_eq!( cloudflare_config_envelope(&value), @@ -920,8 +892,10 @@ mod tests { #[test] fn cloudflare_config_does_not_mask_malformed_manifest_value() { - let mut value = config_value(&[(LEGACY_CONFIG_BLOB_KEY, "legacy-envelope")]); - value[CONFIG_BLOB_KEY] = serde_json::Value::Bool(true); + let value = serde_json::json!({ + LEGACY_CONFIG_BLOB_KEY: "legacy-envelope", + CONFIG_BLOB_KEY: true, + }); assert_eq!( cloudflare_config_envelope(&value), @@ -946,7 +920,7 @@ mod tests { "malformed legacy value should name the legacy key" ); assert_eq!( - error.configuration_message(), + error.to_string(), "Cloudflare TRUSTED_SERVER_CONFIG value at `app_config` must be a string", "configuration error should name the malformed legacy key" ); diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index 84bf174f0..a6d2420a4 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -4,13 +4,11 @@ use serde::Deserialize; use sha2::{Digest as _, Sha256}; pub use crate::config_payload::DEFAULT_CONFIG_STORE_ID; -use crate::config_payload::{settings_from_config_blob, DEFAULT_SECRET_STORE_ID}; +use crate::config_payload::{DEFAULT_SECRET_STORE_ID, settings_from_config_blob}; use crate::error::TrustedServerError; use crate::platform::{PlatformConfigStore, PlatformSecretStore, StoreName}; use crate::settings::Settings; -/// Canonical logical config store used by Trusted Server app config. -pub 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; @@ -54,9 +52,10 @@ pub fn default_config_store_name() -> StoreName { /// Returns the default config-store key containing the app-config blob. /// -/// Process-environment overrides apply to native adapters such as Axum. Fastly -/// has no process environment, so its custom entry point uses the manifest -/// default key. +/// 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()) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 8a04307e8..06827bf1b 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2109,23 +2109,16 @@ ts config push --adapter fastly --dry-run ts config push --adapter fastly ``` -For the first deployment of a service in a shared account, select a -service-specific physical store during both provisioning and config push. The -first deployment applies the generated setup entry and links the physical store -under the logical name: - -```bash -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ - ts provision --adapter fastly -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ - ts config push --adapter fastly --dry-run -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ - ts config push --adapter fastly -``` - -For an already-deployed service, Fastly does not reapply setup entries. Create -and link the service-specific physical store explicitly, seed it, then activate -the cloned service version: +The pinned EdgeZero provisioner cannot create a service-specific physical store +with a different logical resource-link name during first deployment. Its +`__NAME` override becomes both the physical store name and the generated Fastly +setup-table key, so the deployed service would link that physical name while the +Trusted Server entry point opens `trusted_server_config`. Do not use +`ts provision` with a `__NAME` override for this case. + +For a service in a shared account, create the service and a service version +first. Then create and link the service-specific physical store explicitly, +seed it, and activate the linked version before publishing traffic: ```bash fastly config-store create --name From fd290ac6cb020556b406ac6f797c6d42f0b8ea0b Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 21 Sep 2026 10:43:41 -0500 Subject: [PATCH 6/7] Address app config review feedback and verify store defaults --- Cargo.lock | 1 + .../Cargo.toml | 1 + .../src/app.rs | 18 ++- .../tests/config_defaults.rs | 38 +++++ .../tests/config_store_defaults.rs | 151 ++++++++++++++++++ .../trusted-server-core/src/config_payload.rs | 6 + .../trusted-server-core/src/settings_data.rs | 51 ++++-- docs/guide/configuration.md | 143 ++++++++++++----- docs/guide/fastly.md | 11 +- docs/guide/getting-started.md | 23 ++- 10 files changed, 382 insertions(+), 61 deletions(-) create mode 100644 crates/trusted-server-adapter-cloudflare/tests/config_defaults.rs create mode 100644 crates/trusted-server-cli/tests/config_store_defaults.rs diff --git a/Cargo.lock b/Cargo.lock index 2c7e21301..d7e6b2eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5374,6 +5374,7 @@ dependencies = [ "log", "serde_json", "tokio", + "toml", "trusted-server-core", "trusted-server-js", "worker", diff --git a/crates/trusted-server-adapter-cloudflare/Cargo.toml b/crates/trusted-server-adapter-cloudflare/Cargo.toml index 2dfd9773d..e04c0916e 100644 --- a/crates/trusted-server-adapter-cloudflare/Cargo.toml +++ b/crates/trusted-server-adapter-cloudflare/Cargo.toml @@ -42,4 +42,5 @@ worker = { workspace = true } [dev-dependencies] base64 = { workspace = true } edgezero-core = { workspace = true } +toml = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 6dbd69393..0383ff9da 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -111,7 +111,7 @@ const LEGACY_CONFIG_BLOB_KEY: &str = "app_config"; #[derive(Debug, Eq, PartialEq, derive_more::Display)] enum CloudflareConfigEnvelopeError { #[display( - "Cloudflare TRUSTED_SERVER_CONFIG missing string values at `{primary_key}` and legacy `{legacy_key}`" + "Cloudflare TRUSTED_SERVER_CONFIG has no `{primary_key}` or legacy `{legacy_key}` property" )] Missing { primary_key: &'static str, @@ -880,14 +880,24 @@ mod tests { 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!( - cloudflare_config_envelope(&value), - Err(CloudflareConfigEnvelopeError::Missing { + 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] diff --git a/crates/trusted-server-adapter-cloudflare/tests/config_defaults.rs b/crates/trusted-server-adapter-cloudflare/tests/config_defaults.rs new file mode 100644 index 000000000..e573c9747 --- /dev/null +++ b/crates/trusted-server-adapter-cloudflare/tests/config_defaults.rs @@ -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" + ); +} diff --git a/crates/trusted-server-cli/tests/config_store_defaults.rs b/crates/trusted-server-cli/tests/config_store_defaults.rs new file mode 100644 index 000000000..23c5f86e9 --- /dev/null +++ b/crates/trusted-server-cli/tests/config_store_defaults.rs @@ -0,0 +1,151 @@ +//! Exercise the CLI's manifest resolution against the compiled runtime defaults. + +use std::collections::BTreeMap; +use std::fs; +use std::process::{Command, Output}; + +use tempfile::TempDir; +use trusted_server_core::config_payload::{CONFIG_BLOB_KEY, DEFAULT_CONFIG_STORE_ID}; + +fn project() -> TempDir { + let directory = tempfile::tempdir().expect("should create a temporary project"); + let mut manifest: toml::Value = toml::from_str(include_str!("../../../edgezero.toml")) + .expect("should parse the repository manifest"); + // Keep the real store declarations without loading unrelated adapter files. + manifest["adapters"] + .as_table_mut() + .expect("should declare adapters") + .retain(|name, _| name == "axum"); + fs::write( + directory.path().join("edgezero.toml"), + toml::to_string(&manifest).expect("should serialize the Axum test manifest"), + ) + .expect("should write the test manifest"); + fs::write( + directory.path().join("trusted-server.toml"), + include_str!("../../../trusted-server.example.toml"), + ) + .expect("should copy the example app config"); + directory +} + +fn push(project: &TempDir, args: &[&str], overrides: &[(&str, &str)]) -> Output { + let output = Command::new(env!("CARGO_BIN_EXE_ts")) + .args([ + "config", + "push", + "--adapter", + "axum", + "--local", + "--no-diff", + ]) + .args(args) + .current_dir(project.path()) + // Do not inherit operator configuration or credentials from the test runner. + .env_clear() + .env("RUST_LOG", "info") + .env("TRUSTED_SERVER__PUBLISHER__DOMAIN", "publisher.example.com") + .env( + "TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN", + ".publisher.example.com", + ) + .env( + "TRUSTED_SERVER__PUBLISHER__ORIGIN_URL", + "https://upstream.example.com", + ) + .envs(overrides.iter().copied()) + .output() + .expect("should run a local config push"); + assert!( + output.status.success(), + "local push should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + output +} + +fn stored_entries(project: &TempDir) -> BTreeMap { + // Axum's local file is keyed by logical ID even with a physical-name override. + let path = project.path().join(format!( + ".edgezero/local-config-{DEFAULT_CONFIG_STORE_ID}.json" + )); + let raw = fs::read_to_string(path).expect("should write the manifest-default local store"); + serde_json::from_str(&raw).expect("should parse the stored config entries") +} + +#[test] +fn config_push_default_store_and_key_match_the_compiled_runtime() { + let project = project(); + push(&project, &["--yes"], &[]); + + let entries = stored_entries(&project); + assert_eq!(entries.len(), 1, "should write only the default blob key"); + let envelope: serde_json::Value = serde_json::from_str( + entries + .get(CONFIG_BLOB_KEY) + .expect("should write at the compiled runtime's default key"), + ) + .expect("should write a JSON blob envelope"); + assert_eq!( + envelope["data"]["publisher"]["domain"], + "publisher.example.com" + ); +} + +#[test] +fn config_push_resolves_the_physical_name_and_explicit_key() { + let project = project(); + let name_var = format!( + "EDGEZERO__STORES__CONFIG__{}__NAME", + DEFAULT_CONFIG_STORE_ID.to_uppercase() + ); + let key_var = format!( + "EDGEZERO__STORES__CONFIG__{}__KEY", + DEFAULT_CONFIG_STORE_ID.to_uppercase() + ); + let overrides = [ + (name_var.as_str(), "example_config"), + (key_var.as_str(), "active_config"), + ]; + let preview = push( + &project, + &["--dry-run", "--key", "active_config"], + &overrides, + ); + let stdout = String::from_utf8_lossy(&preview.stdout); + assert!( + stdout.contains(&format!( + "store `{DEFAULT_CONFIG_STORE_ID}` (platform name `example_config`)" + )), + "preview should resolve the logical and physical store names: {stdout}" + ); + assert!( + !project.path().join(".edgezero").exists(), + "preview should not write local config" + ); + + push(&project, &["--yes", "--key", "active_config"], &overrides); + let entries = stored_entries(&project); + assert_eq!(entries.len(), 1); + assert!( + entries.contains_key("active_config"), + "explicit push key should match the runtime override" + ); +} + +#[test] +fn config_push_does_not_use_the_runtime_key_override_without_the_key_flag() { + let project = project(); + let key_var = format!( + "EDGEZERO__STORES__CONFIG__{}__KEY", + DEFAULT_CONFIG_STORE_ID.to_uppercase() + ); + push(&project, &["--yes"], &[(&key_var, "active_config")]); + + let entries = stored_entries(&project); + assert_eq!(entries.len(), 1); + assert!( + entries.contains_key(CONFIG_BLOB_KEY), + "runtime-only key override should not move the CLI's write destination" + ); +} diff --git a/crates/trusted-server-core/src/config_payload.rs b/crates/trusted-server-core/src/config_payload.rs index a955d2af9..c9690b01f 100644 --- a/crates/trusted-server-core/src/config_payload.rs +++ b/crates/trusted-server-core/src/config_payload.rs @@ -23,6 +23,12 @@ pub const DEFAULT_SECRET_STORE_ID: &str = "trusted_server_secrets"; 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. +/// +/// Intentionally matches the logical store ID: an ordinary `ts config push` +/// writes there unless `--key` selects another key. This constant does not apply +/// runtime overrides; use [`crate::settings_data::config_key`] with the adapter's +/// runtime configuration, or [`crate::settings_data::default_config_key`] for +/// process-environment overrides. pub const CONFIG_BLOB_KEY: &str = DEFAULT_CONFIG_STORE_ID; /// Reconstruct runtime [`Settings`] from a serialized config blob envelope. diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index a6d2420a4..07108fac6 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -40,22 +40,23 @@ pub fn config_key(env: &EnvConfig) -> String { env.store_key("config", DEFAULT_CONFIG_STORE_ID) } -/// Returns the default `EdgeZero` app-config store name. +/// Resolves the `EdgeZero` app-config store name from the process environment. /// -/// 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. +/// Native adapters such as Axum use this wrapper. Fastly instead supplies an +/// `EnvConfig` populated from service-scoped entries in `edgezero_runtime_env` +/// and resolves the store name from that configuration. Without an override, +/// both paths use the manifest default logical store ID. #[must_use] pub fn default_config_store_name() -> StoreName { config_store_name(&EnvConfig::from_env()) } -/// Returns the default config-store key containing the app-config blob. +/// Resolves the app-config blob key from the process environment. /// -/// 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. +/// Native adapters such as Axum use this wrapper. Fastly resolves the key from +/// service-scoped entries in `edgezero_runtime_env` instead. A runtime `__KEY` +/// override must match `ts config push --key`; an ordinary push without that +/// flag writes at the logical store ID, regardless of the `__KEY` override. #[must_use] pub fn default_config_key() -> String { config_key(&EnvConfig::from_env()) @@ -366,8 +367,12 @@ mod tests { entries: BTreeMap::from([(CONFIG_BLOB_KEY.to_string(), envelope_json)]), }; - let loaded = load_settings(&store, &StoreName::from("app_config"), CONFIG_BLOB_KEY) - .expect("should load settings"); + let loaded = load_settings( + &store, + &StoreName::from(DEFAULT_CONFIG_STORE_ID), + CONFIG_BLOB_KEY, + ) + .expect("should load settings"); assert_eq!( loaded.publisher.domain, settings.publisher.domain, @@ -413,8 +418,12 @@ mod tests { ]), }; - let loaded = load_settings(&store, &StoreName::from("app_config"), CONFIG_BLOB_KEY) - .expect("should load settings"); + let loaded = load_settings( + &store, + &StoreName::from(DEFAULT_CONFIG_STORE_ID), + CONFIG_BLOB_KEY, + ) + .expect("should load settings"); assert_eq!( loaded.publisher.domain, settings.publisher.domain, @@ -443,8 +452,12 @@ mod tests { entries: BTreeMap::from([(CONFIG_BLOB_KEY.to_string(), pointer)]), }; - let err = load_settings(&store, &StoreName::from("app_config"), CONFIG_BLOB_KEY) - .expect_err("should reject malformed chunk length metadata"); + let err = load_settings( + &store, + &StoreName::from(DEFAULT_CONFIG_STORE_ID), + CONFIG_BLOB_KEY, + ) + .expect_err("should reject malformed chunk length metadata"); assert!( err.to_string().contains("chunk lengths total mismatch"), @@ -458,8 +471,12 @@ mod tests { entries: BTreeMap::new(), }; - let err = load_settings(&store, &StoreName::from("app_config"), CONFIG_BLOB_KEY) - .expect_err("should fail when blob is missing"); + let err = load_settings( + &store, + &StoreName::from(DEFAULT_CONFIG_STORE_ID), + CONFIG_BLOB_KEY, + ) + .expect_err("should fail when blob is missing"); assert!( err.to_string().contains(CONFIG_BLOB_KEY), diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 06827bf1b..1412bed21 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2094,56 +2094,127 @@ After the EdgeZero cutover, the Fastly adapter always dispatches through the EdgeZero entry point. The former `edgezero_enabled` and `edgezero_rollout_pct` canary keys are no longer read. -The Fastly service opens the logical config store selected by -`[stores.config].default` in `edgezero.toml` and reads the app-config blob at -its default key. A Fastly resource link maps that logical store name to the -physical config store for the service at runtime. +`[stores.config].default` in `edgezero.toml` supplies the logical config store +ID and default blob key, currently `trusted_server_config`. Fastly has no +process environment. Its entry point reads service-scoped overrides from the +`edgezero_runtime_env` Config Store before opening the app-config store: -Fastly store names are account-level. For a first deployment, the default setup -is safe only when no other service in the account uses the -`trusted_server_config` physical store: +```mermaid +flowchart TD + A[Manifest default store ID] --> B[Resolve store name and blob key] + C[Service-scoped entries in edgezero_runtime_env] --> B + B --> D[Open the resolved resource-link name] + D --> E[Read the selected blob key from the linked physical store] +``` -```bash -ts provision --adapter fastly -ts config push --adapter fastly --dry-run -ts config push --adapter fastly +For this logical ID, the runtime selectors are: + +```text +EDGEZERO__SERVICES____STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME +EDGEZERO__SERVICES____STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY ``` -The pinned EdgeZero provisioner cannot create a service-specific physical store -with a different logical resource-link name during first deployment. Its -`__NAME` override becomes both the physical store name and the generated Fastly -setup-table key, so the deployed service would link that physical name while the -Trusted Server entry point opens `trusted_server_config`. Do not use -`ts provision` with a `__NAME` override for this case. +The runtime ignores unscoped entries. Missing or blank selectors fall back to +the logical ID. A resource link must exist under the resolved name, not always +under `trusted_server_config`. + +### Initial setup with a service-specific store + +Fastly store names are account-level. Choose a physical name that is not used +by another service. The default physical name is safe only if the service owns +that store exclusively. + +Create the Fastly service and an editable service version before provisioning +non-default mappings. Select its ID through top-level `service_id` in +`fastly.toml` or `FASTLY_SERVICE_ID`. If both are set, they must agree. Do not +reuse the checked-in service ID for your deployment. Without a service ID, +provisioning rejects non-default mappings before creating resources. -For a service in a shared account, create the service and a service version -first. Then create and link the service-specific physical store explicitly, -seed it, and activate the linked version before publishing traffic: +The following example is for initial setup before the service receives traffic. +Replace the service ID and choose your own physical store name: ```bash -fastly config-store create --name -fastly resource-link create --service-id --version latest --autoclone \ - --resource-id --name trusted_server_config -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ - ts config push --adapter fastly --dry-run -EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ - ts config push --adapter fastly -fastly service-version activate --service-id --version latest +export FASTLY_SERVICE_ID="" +export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=example_config + +ts provision --adapter fastly --dry-run +ts provision --adapter fastly ``` -Confirm that each dry run names the intended physical store before writing. In -a shared account, it must be the service-specific store rather than the -account-level default. The resource-link name must match the logical store ID, -and the physical store must contain a valid Trusted Server app-config blob -envelope at its default key. An absent or empty entry makes application startup -fail closed. +Provisioning creates the stores and persists the selected name in the +service-scoped `edgezero_runtime_env` entry. Keep all intended store-name +overrides set when provisioning, including any [secret-store mapping](/guide/fastly#secret-stores). +Provisioning reconciles mappings for all declared stores, so omitting a previous +override can remove it. + +For an existing service, Fastly does not reapply `[setup]` entries. Follow the +provisioner's resource-link instructions. Both the app-config store and the +runtime-env store must be linked to the same editable version. For this example: + +```bash +fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \ + --resource-id --name example_config +fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \ + --resource-id --name edgezero_runtime_env -**Local development** (writes the entry used by Viceroy in `fastly.toml`): +ts config push --adapter fastly --dry-run +ts config push --adapter fastly +fastly compute publish --service-id "$FASTLY_SERVICE_ID" --version latest +``` + +Look up each store ID by its name before linking. Confirm the push dry run names +`example_config`, not the account-level default. Publish the application to the +linked version only after seeding its config store; a missing or invalid blob +makes application startup fail closed. Do not activate a new service's empty +version before uploading the application. If your deployment separates upload +from activation, activate the prepared version with +`fastly service-version activate --service-id "$FASTLY_SERVICE_ID" --version ` +only after both the code and config are ready. + +Keep the `__NAME` override in your deployment environment for **every subsequent +push**. The CLI reads its process environment, not the service's persisted +runtime mapping. Omitting the override can write to the wrong physical store. +Reject empty values in deployment scripts rather than relying on the fallback. + +For a live service, changing entries in its active `edgezero_runtime_env` store +changes runtime selection immediately, independently of service-version +activation. Do not use the initial-setup sequence to migrate a live mapping. +Prepare and seed the destination and make its resource link available to the +active version before switching the selector, or use an isolated staged runtime +configuration. + +An existing deployment may instead link a service-specific physical store under +the logical name `trusted_server_config`, with no runtime `__NAME` override. +That alias works, but the CLI still needs the physical-name override on every +push. Do not add a runtime override unless a link under the newly selected name +also exists. + +### Selecting another blob key + +A normal push writes at the logical store ID. A runtime `__KEY` override does +not change that write destination. To select another production key, first push +with `ts config push --adapter fastly --key `, then set the matching +service-scoped `__KEY` entry in `edgezero_runtime_env`. Changing that entry affects +the active service immediately. Do not point the production selector at a +staging key; staged deployments need their own runtime-env store. + +### Local development + +The repository's Viceroy configuration uses the default logical app-config name +and key. Clear production overrides for the local push: ```bash -ts config push --adapter fastly --local +env -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME \ + -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY \ + ts config push --adapter fastly --local ``` +`--local` writes under `[local_server.config_stores.]` in the +tracked `fastly.toml`. If you customize Viceroy's service-scoped runtime selectors, +keep that local name and the pushed key aligned with them. Review the generated +diff and do not commit deployment-specific app-config entries. Credentials belong +in secret stores; the app-config blob contains their key references. + Rollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required. diff --git a/docs/guide/fastly.md b/docs/guide/fastly.md index f63b4f627..c1cf70097 100644 --- a/docs/guide/fastly.md +++ b/docs/guide/fastly.md @@ -267,7 +267,10 @@ Trusted Server keeps static app-config credentials under logical store ID as `ts_secrets`. Request-signing private keys remain in their separate, runtime-managed store. -Set the physical mapping before provisioning: +Create or select the service before provisioning a non-default mapping. Set its +ID in `fastly.toml` or `FASTLY_SERVICE_ID`; if both are set, they must agree. +Provisioning rejects non-default mappings without a service ID. Set the physical +mapping before provisioning, alongside any config-store or KV-store overrides: ```bash export EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets @@ -289,6 +292,12 @@ app config, so every startup and reload resolves static credentials from `ts_secrets` while the portable manifest continues to declare `trusted_server_secrets`. +The same runtime mapping mechanism applies to app-config stores. See +[Fastly runtime config stores](/guide/configuration#fastly-runtime-config-store) +for the initial provisioning and linking sequence, subsequent CLI pushes, and +precautions when changing a live mapping. A process-environment override alone +does not configure the Fastly runtime. + Create the separate request-signing store when that feature is enabled: ```bash diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index f70cd25b8..0eff61fca 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -80,6 +80,10 @@ the variables into your shell before starting the server. cp trusted-server.example.toml trusted-server.toml set -a && source .env.dev && set +a +# Use the repository's default app-config store name and key for this quickstart. +unset EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME +unset EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY + # Create the local blob-backed config-store entry. ts config push --adapter axum --local --yes export TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG="$( @@ -106,11 +110,24 @@ The server will be available at `http://localhost:8787`. Set `PORT=` befor | Config store value | `TRUSTED_SERVER_CONFIG_{STORE}_{KEY}` | `TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG=…` | | Secret store value | `TRUSTED_SERVER_SECRET_{STORE}_{KEY}` | `TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_PROXY_KEY=…` | +The repeated `TRUSTED_SERVER_CONFIG` segments in the example are intentional: +`TRUSTED_SERVER_CONFIG_` is the adapter prefix, followed by the resolved store +name and blob key. Both default to `[stores.config].default` in `edgezero.toml`, +currently `trusted_server_config`. The commands above assume that repository +default and clear any name/key overrides left in the shell. + +If you customize the defaults or use `EDGEZERO__STORES__CONFIG____NAME` or +`__KEY`, adjust the exported variable's store/key segments and the `jq` key to +match. Pass a matching `--key` to `ts config push` when overriding the runtime +key. The local JSON filename still uses the logical store ID, even with a +physical-name override. + The config-store value is the verified app-config blob. Secret-store values are looked up by the key names in that blob. Store names and key names are uppercased -with hyphens and dots replaced by underscores. The quick-start exports ephemeral -secret-store values only into the current shell; do not put secret values in the -TOML config, config-store blob, or a source-controlled environment file. +with hyphens, dots, and spaces replaced by underscores. The quick-start exports +ephemeral secret-store values only into the current shell; do not put secret +values in the TOML config, config-store blob, or a source-controlled environment +file. > **Dev server limitations:** The Axum adapter does not support KV store, > geo lookup, config/secret-store writes, or admin key-management routes. From c715f4ed7798bf68025b3e44a0a9a39a925e6657 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 24 Sep 2026 19:27:28 -0500 Subject: [PATCH 7/7] Address final app config review feedback --- .../src/app.rs | 20 ++++++++++++++++++- crates/trusted-server-core/build.rs | 11 ++++++---- docs/guide/configuration.md | 2 ++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 0383ff9da..5086f87d3 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -162,7 +162,11 @@ fn settings_from_cloudflare_config_json() -> Result Result<&str, CloudflareConfigEnvelopeError> { - match value.get(CONFIG_BLOB_KEY) { + 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 .as_str() .ok_or(CloudflareConfigEnvelopeError::NonString { @@ -876,6 +880,20 @@ mod tests { ); } + #[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!({}); diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index df790cb7f..47f5d5740 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -7,11 +7,14 @@ 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( + let crate_dir = PathBuf::from( env::var("CARGO_MANIFEST_DIR").expect("should receive CARGO_MANIFEST_DIR from Cargo"), - ) - .join("../..") - .join("edgezero.toml"); + ); + let manifest_path = crate_dir + .ancestors() + .nth(2) + .expect("should resolve the workspace root from CARGO_MANIFEST_DIR") + .join("edgezero.toml"); println!("cargo:rerun-if-changed={}", manifest_path.display()); let manifest = match ManifestLoader::from_path(&manifest_path) { diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 4be50630e..e55eddeaf 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2361,6 +2361,8 @@ keep that local name and the pushed key aligned with them. Review the generated diff and do not commit deployment-specific app-config entries. Credentials belong in secret stores; the app-config blob contains their key references. +### Rollback + Rollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required.