Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
00de4e9
feat: add native secret-store config resolution
ChristianPavilonis Aug 17, 2026
0fdfcbe
Fix platform secret-store startup configuration
ChristianPavilonis Aug 18, 2026
2783e00
Resolve static credentials through typed config
ChristianPavilonis Aug 24, 2026
3dc7370
Update EdgeZero static-secret support revision
ChristianPavilonis Aug 24, 2026
0db2e11
Align EdgeZero deployment mapping revision
ChristianPavilonis Aug 24, 2026
3e2b3d2
Fix secret reference validation and guidance
ChristianPavilonis Aug 25, 2026
b47ced8
Address secret configuration review findings
ChristianPavilonis Aug 26, 2026
58730d7
Make Edge Cookie partner API tokens optional
ChristianPavilonis Aug 26, 2026
1b17ce0
Merge branch 'main' into edgezero-secrets
aram356 Aug 27, 2026
76f6f13
Address remaining secret configuration review feedback
ChristianPavilonis Aug 28, 2026
44e9eaf
feat: add native secret-store config resolution
ChristianPavilonis Aug 17, 2026
3f6e29d
Fix platform secret-store startup configuration
ChristianPavilonis Aug 18, 2026
d71c660
Resolve static credentials through typed config
ChristianPavilonis Aug 24, 2026
6c35ce9
Update EdgeZero static-secret support revision
ChristianPavilonis Aug 24, 2026
43c7577
Align EdgeZero deployment mapping revision
ChristianPavilonis Aug 24, 2026
edfbcb0
Fix secret reference validation and guidance
ChristianPavilonis Aug 25, 2026
22176da
Address secret configuration review findings
ChristianPavilonis Aug 26, 2026
3b2c64c
Make Edge Cookie partner API tokens optional
ChristianPavilonis Aug 26, 2026
c7c382a
Address remaining secret configuration review feedback
ChristianPavilonis Aug 28, 2026
55f0a23
Merge commit 'refs/pull/1036/head' of github.com:IABTechLab/trusted-s…
ChristianPavilonis Aug 31, 2026
e45ff8e
Correct proxy secret length guidance
ChristianPavilonis Aug 31, 2026
9831ee4
Correct proxy secret length guidance
ChristianPavilonis Aug 31, 2026
3c48d39
Merge commit 'refs/pull/1036/head' of github.com:IABTechLab/trusted-s…
ChristianPavilonis Sep 1, 2026
71df1aa
Merge branch 'main' of github.com:IABTechLab/trusted-server into edge…
ChristianPavilonis Sep 1, 2026
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
4 changes: 4 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Non-secret development overlays used while generating the Axum config blob.
# Sourcing this file alone does not configure the Axum server: also export the
# blob and referenced secret-store values as shown in docs/guide/getting-started.md.

# [publisher]
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:9090
Comment on lines 5 to 6

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.

🔧 wrench — The Axum quick-start cannot complete, because the starter config's publisher domains are reserved placeholders.

trusted-server.example.toml:57-59 ships domain = "example.com" and cookie_domain = ".example.com", and both are in the reject lists at settings.rs:131-132 (PLACEHOLDER_DOMAINS / PLACEHOLDER_COOKIE_DOMAINS). validate_non_secret_deploy_placeholders raises TrustedServerError::InsecureDefault (config.rs:320-325) through validate_settings_for_deployTrustedServerAppConfig::validate()validate_excluding_secrets, which run_config_push_typed calls. The error is keyed at the root field trusted_server (config.rs:110-115), and prune_secret_leaf only descends secret paths — so it survives pruning and both ts config validate and ts config push fail. reject_placeholder_secrets re-checks the same three fields at runtime (settings.rs:3071-3079), so even a hand-forced blob would not start. .env.dev currently overrides only ORIGIN_URL.

It fails loudly rather than silently, but a new user has no way to know example.com is reserved, and the block at getting-started.md:73-79 is presented as a complete copy-paste quick-start. This PR's own harness confirms the diagnosis at scripts/template-cache-local-test.sh:184-186, with the comment "The example publisher domains are reserved placeholders that validation rejects."

localhost/localhost is the pair fixtures/configs/trusted-server.integration.toml:6-7 already uses, and validate_cookie_domain (settings.rs:3345) rejects only ;, \n and \r, so the dotless form is fine. The env overlay replaces existing scalar leaves only, and both keys exist in the template, so the override lands.

Suggested change
# [publisher]
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:9090
# [publisher]
# The starter config ships `example.com`/`.example.com`, which are reserved
# placeholders that both `ts config push` and runtime validation reject.
TRUSTED_SERVER__PUBLISHER__DOMAIN=localhost
TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=localhost
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:9090


Expand Down
16 changes: 11 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
# Trusted Server Environment Variables
# Copy this file to .env.dev, .env.staging, or .env.production and fill in values
# See docs/guide/configuration.md for details
# Trusted Server development environment variables
# Copy this file to .env.dev, .env.staging, or .env.production and fill in
# non-secret values. App-config secrets are key names in the pushed blob and
# their values belong in the platform secret store; see the configuration guide.
# For Axum runtime loading, export the config blob as:
# TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG=<blob-envelope-json>
# and export one secret per key name as:
# TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_<KEY_NAME>=<secret-value>
# The commented examples below are CLI overlays for ordinary fields only.

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.

nitpick — This ordering reads backwards.

The line says "The commented examples below are CLI overlays for ordinary fields only", and :10-11 immediately follow with an EDGEZERO__STORES__SECRETS__…__NAME store-mapping overlay — which is neither below the line in spirit nor an ordinary field. Moving :10-11 above this line fixes it.

# Fastly example: map logical app-config secrets to physical `ts_secrets`.
# EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets

# =============================================================================
# Publisher Settings
# =============================================================================
TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=<your-proxy-secret>

# =============================================================================
# Synthetic ID Settings
# =============================================================================
TRUSTED_SERVER__SYNTHETIC__COUNTER_STORE=counter_store
TRUSTED_SERVER__SYNTHETIC__OPID_STORE=opid_store
TRUSTED_SERVER__SYNTHETIC__SECRET_KEY=<your-synthetic-secret>
# Template variables: client_ip, user_agent, first_party_id, auth_user_id, publisher_domain, accept_language
TRUSTED_SERVER__SYNTHETIC__TEMPLATE={{ client_ip }}:{{ user_agent }}:{{ first_party_id }}

Expand Down
57 changes: 41 additions & 16 deletions Cargo.lock

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

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ criterion = { version = "0.5", default-features = false, features = ["cargo_benc
derive_more = { version = "2.0", features = ["display", "error"] }
directories = "5"
ed25519-dalek = { version = "2.2", features = ["rand_core"] }
edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4", default-features = false }
edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4", default-features = false }
edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4", default-features = false }
edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4", default-features = false }
edgezero-cli = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4" }
edgezero-core = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.4", default-features = false }
edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34", default-features = false }
edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34", default-features = false }
edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34", default-features = false }
edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34", default-features = false }
edgezero-cli = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34" }
edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "0d6ebf9b0250efa5f7031a93ec7b7f09f2c9bf34", default-features = false }
env_logger = "0.11"
error-stack = "0.6"
esi = "0.7.2"
Expand Down
11 changes: 8 additions & 3 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use trusted_server_core::settings_data::{
use trusted_server_core::platform::RuntimeServices;

use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware, SanitizeRequestMiddleware};
use crate::platform::{AxumPlatformConfigStore, build_runtime_services};
use crate::platform::{AxumPlatformConfigStore, AxumPlatformSecretStore, build_runtime_services};

// ---------------------------------------------------------------------------
// AppState
Expand All @@ -60,8 +60,13 @@ pub struct AppState {
fn build_state() -> Result<Arc<AppState>, Report<TrustedServerError>> {
let store_name = default_config_store_name();
let config_key = default_config_key();
let settings =
get_settings_from_config_store(&AxumPlatformConfigStore, &store_name, &config_key)?;
let settings = get_settings_from_config_store(
&AxumPlatformConfigStore,
&AxumPlatformSecretStore,
&store_name,
&config_key,
&trusted_server_core::settings_data::default_secret_store_name(),

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.

nitpick — This reaches for a fully-qualified path while its two siblings at :61-62 use the plain imported names from the use at :34-36.

Apply manually — the import and the call site are in two different hunks, and the import alone would be an unused import that fails clippy -D warnings, so this cannot be delivered as a suggestion. Both edits together:

:34-36 becomes:

use trusted_server_core::settings_data::{
    default_config_key, default_config_store_name, default_secret_store_name,
    get_settings_from_config_store,
};

and this line becomes:

        &default_secret_store_name(),

)?;
build_state_with_settings(settings)
}

Expand Down
41 changes: 34 additions & 7 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::cache_policy::EdgeCacheHeader;
#[cfg(target_arch = "wasm32")]
use trusted_server_core::config_payload::settings_from_config_blob;
use trusted_server_core::config_payload::{DEFAULT_SECRET_STORE_ID, settings_from_config_blob};
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::{
admin_ec_lookup_not_supported as core_admin_ec_lookup_not_supported,
Expand All @@ -22,6 +22,8 @@ use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::platform::RuntimeServices;
#[cfg(target_arch = "wasm32")]
use trusted_server_core::platform::StoreName;
use trusted_server_core::proxy::{
handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild,
handle_first_party_proxy_sign,
Expand All @@ -44,11 +46,23 @@ use crate::platform::build_runtime_services;
// ---------------------------------------------------------------------------

#[cfg(target_arch = "wasm32")]
static CLOUDFLARE_CONFIG_JSON: std::sync::OnceLock<String> = std::sync::OnceLock::new();
thread_local! {
static CLOUDFLARE_CONFIG_JSON: std::cell::OnceCell<String> = const { std::cell::OnceCell::new() };
static CLOUDFLARE_ENV: std::cell::OnceCell<worker::Env> = const { std::cell::OnceCell::new() };

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.

🤔 thinking — Cloudflare resolves every secret on every request, against request #1's pinned Env. Worth a comment here and a sentence in the PR body.

In the pinned edgezero rev, edgezero-adapter-cloudflare/src/lib.rs:111 calls A::build_app() inside run_app — that is, per fetch. So routes()build_state()settings_from_cloudflare_config_json() → N env.secret() calls run on every request, and with let _ = slot.set(env) at :63 every request after the first resolves against the first request's worker::Env.

I checked and this is not a correctness bug: get_binding is a synchronous Reflect::get rather than I/O, so a stale Env still yields the right string and will not trip "I/O on behalf of a different request". But it is a real cross-adapter divergence that the PR body describes as uniform startup resolution: Fastly is per-instance-per-request by platform design, Axum and Spin are genuinely once-at-startup, and Cloudflare is per-request against a frozen binding object. A // per-request: edgezero's run_app rebuilds the app each fetch note here would stop a later reader assuming CF caches state.

Aside: worker::Env carries unsafe impl Send/Sync (worker-0.8.5 src/env.rs:26-27), so the move from static OnceLock to thread_local! OnceCell was not forced by the type. Harmless on single-threaded wasm, just unexplained.

}

#[cfg(target_arch = "wasm32")]
pub fn set_cloudflare_config_json(value: String) {
let _ = CLOUDFLARE_CONFIG_JSON.set(value);
CLOUDFLARE_CONFIG_JSON.with(|slot| {
let _ = slot.set(value);
});
}

#[cfg(target_arch = "wasm32")]
pub fn set_cloudflare_env(env: worker::Env) {
CLOUDFLARE_ENV.with(|slot| {
let _ = slot.set(env);
});
}

/// Application state built once at startup and shared across all requests.
Expand Down Expand Up @@ -76,18 +90,22 @@ fn load_startup_settings() -> Result<Settings, Report<TrustedServerError>> {

#[cfg(not(target_arch = "wasm32"))]
fn load_startup_settings() -> Result<Settings, Report<TrustedServerError>> {
Settings::from_toml(include_str!("../../../trusted-server.example.toml"))
Err(Report::new(TrustedServerError::Configuration {
message: "Cloudflare startup settings require a Worker config binding".to_string(),
})
.attach("use TrustedServerApp::routes_with_settings for host tests"))
}

#[cfg(target_arch = "wasm32")]
fn settings_from_cloudflare_config_json() -> Result<Settings, Report<TrustedServerError>> {
let raw_config = CLOUDFLARE_CONFIG_JSON.get().ok_or_else(|| {
let raw_config = CLOUDFLARE_CONFIG_JSON.with(|slot| slot.get().cloned());
let raw_config = raw_config.ok_or_else(|| {
Report::new(TrustedServerError::Configuration {
message: "Cloudflare TRUSTED_SERVER_CONFIG is required".to_string(),
})
.attach("set TRUSTED_SERVER_CONFIG to JSON containing the app_config blob envelope")
})?;
let value: serde_json::Value = serde_json::from_str(raw_config).map_err(|error| {
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(),
})
Expand All @@ -101,7 +119,16 @@ fn settings_from_cloudflare_config_json() -> Result<Settings, Report<TrustedServ
message: "Cloudflare TRUSTED_SERVER_CONFIG missing app_config".to_string(),
})
})?;
settings_from_config_blob(envelope)
let env = CLOUDFLARE_ENV
.with(|slot| slot.get().cloned())
.ok_or_else(|| {
Report::new(TrustedServerError::Configuration {
message: "Cloudflare Worker environment is unavailable during startup".to_string(),
})
})?;
let secret_store = crate::platform::CloudflareSecretStoreAdapter { env };
let default_secret_store = StoreName::from(DEFAULT_SECRET_STORE_ID);

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.

🤔 thinking — Cloudflare is the only adapter with no logical→physical store mapping, and nothing here says whether that is deliberate.

It is functionally correct: CloudflareSecretStoreAdapter::get_bytes (platform.rs:556-560) takes _store_name and calls self.env.secret(key), and in worker 0.8.5 Secret is a re-export of StringBinding (the same type as Var), so Workers bindings are a flat namespace with no store concept to map into. But a reader comparing the four adapter startup paths cannot tell that from the code, and the other three all map.

Suggested change
let default_secret_store = StoreName::from(DEFAULT_SECRET_STORE_ID);
// Workers bindings are a flat namespace: `env.secret()` ignores the store
// name, so the logical id is passed through unmapped.
let default_secret_store = StoreName::from(DEFAULT_SECRET_STORE_ID);

settings_from_config_blob(envelope, &secret_store, &default_secret_store)
}

/// Build the application state from explicit settings.
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-adapter-cloudflare/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
if let Ok(config) = env.var("TRUSTED_SERVER_CONFIG") {
app::set_cloudflare_config_json(config.to_string());
}
app::set_cloudflare_env(env.clone());

match edgezero_adapter_cloudflare::run_app::<app::TrustedServerApp>(req, env, ctx).await {
Ok(resp) => Ok(resp),
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-adapter-cloudflare/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,8 +547,8 @@ impl PlatformHttpClient for CloudflareHttpClient {
/// Bridges [`worker::Env`] secrets to [`PlatformSecretStore`] by calling
/// `env.secret(key)` synchronously. Writes and deletes return errors.
#[cfg(target_arch = "wasm32")]
struct CloudflareSecretStoreAdapter {
env: worker::Env,
pub(crate) struct CloudflareSecretStoreAdapter {
pub(crate) env: worker::Env,
}

#[cfg(target_arch = "wasm32")]
Expand Down
9 changes: 9 additions & 0 deletions crates/trusted-server-adapter-cloudflare/wrangler.ci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ id = "ci-local-kv"
# Placeholder replaced by the integration test harness with a JSON object that
# contains the runtime Trusted Server app-config blob envelope.
TRUSTED_SERVER_CONFIG = "{}"

# Fictitious integration-only secret values. `worker::Env::secret` reads these
# string bindings in local Wrangler runs; production values are provisioned with
# `wrangler secret put` instead of being committed to a manifest.
integration_admin_password = "integration-admin-password-32-bytes-ok"
integration_proxy_secret = "integration-test-proxy-secret-32-bytes-ok"
integration_ec_passphrase = "integration-test-ec-secret-padded-32"
integration_partner_token_alpha = "integration-test-token-alpha-32-bytes-ok"
integration_partner_token_bravo = "integration-test-token-bravo-32-bytes-ok"
4 changes: 4 additions & 0 deletions crates/trusted-server-adapter-cloudflare/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"
# invalid placeholder with JSON containing an `app_config` blob envelope before
# deploying or running `wrangler dev` against real traffic.
TRUSTED_SERVER_CONFIG = '{"app_config":""}'

# App-config secret values are provisioned as Worker secrets with
# `wrangler secret put <key-name>`. The pushed blob contains only those key
# names; never add secret values to this file.
Loading
Loading