Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e15d3b2
Extract the protected CloudFront Geo baseline
ChristianPavilonis Nov 11, 2026
fb8b2a5
Carry the native toolchain lint correction into Geo
ChristianPavilonis Nov 11, 2026
6003fa6
Carry the startup probe correction through the compatible baseline
ChristianPavilonis Nov 11, 2026
affb0d0
Carry the browser CI host compatibility fix through the baseline
ChristianPavilonis Nov 11, 2026
9a0266f
Carry the tagged EdgeZero snapshot through the stack
ChristianPavilonis Nov 11, 2026
0ae96ef
Carry the monthly EdgeZero tag through the stack
ChristianPavilonis Nov 11, 2026
e0ee5ac
Carry protected CloudFront Geo onto the direct EdgeZero cutover
ChristianPavilonis Nov 12, 2026
38f68a5
Retain the owning native startup fixture correction
ChristianPavilonis Nov 12, 2026
633a0c7
Migrate current Fastly Geo fixtures to partial location data
ChristianPavilonis Nov 12, 2026
1a914d6
Keep the trace parity fixture honest about partial Geo
ChristianPavilonis Nov 12, 2026
ee27f39
Retain the concurrent cutover documentation closeout
ChristianPavilonis Nov 12, 2026
006fa9d
Retain prepared native development documentation
ChristianPavilonis Nov 12, 2026
efc416a
Retain current main validation and mediator fallback
ChristianPavilonis Nov 12, 2026
d9e7727
Retain the Cargo target-aware cache harness
ChristianPavilonis Nov 12, 2026
857611d
Retain target-aware GPT bundle lookup for the cache fixture
ChristianPavilonis Nov 12, 2026
6bc85ab
Carry hosted qualification fixes through PR 1532
ChristianPavilonis Nov 12, 2026
5c331c2
Carry hosted qualification fixes through PR 1532
ChristianPavilonis Nov 12, 2026
d95730c
Carry hosted qualification fixes through PR 1532
ChristianPavilonis Nov 12, 2026
713bdd9
Carry hosted qualification fixes through PR 1532
ChristianPavilonis Nov 12, 2026
852b875
Carry hosted qualification fixes through PR 1532
ChristianPavilonis Nov 12, 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
2 changes: 2 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ path = "src/main.rs"
default = []
aws-appconfig-agent = ["edgezero-adapter-axum/aws-appconfig-agent"]
aws-secrets-manager = ["edgezero-adapter-axum/aws-secrets-manager"]
geo-cloudfront = ["edgezero-adapter-axum/geo-cloudfront"]

[dependencies]
async-trait = { workspace = true }
Expand All @@ -30,6 +31,8 @@ edgezero-core = { workspace = true }
error-stack = { workspace = true }
futures = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
toml = { workspace = true }
simple_logger = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "time"] }
trusted-server-core = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ where
///
/// Mirrors the Fastly entry point: `EcContext::default()` leaves jurisdiction
/// Unknown, which fails the auction consent gate closed even for consented
/// users. Geo comes from the platform (a no-op on the local Axum dev server, so
/// jurisdiction stays Unknown there unless the request carries TCF consent). A
/// users. Geo comes from the immutable managed request snapshot. Off, untrusted
/// or unavailable results remain Unknown unless real consent supplies it. A
/// malformed consent string is logged and falls back to the default
/// (fail-closed) context rather than being silently swallowed.
fn build_ec_context(state: &AppState, services: &RuntimeServices, req: &Request) -> EcContext {
Expand Down
5 changes: 4 additions & 1 deletion crates/trusted-server-adapter-axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Native Axum adapter with strict prepared-store startup.
//!
//! Runtime settings and immutable config/secret snapshots are validated before
//! the framework listener becomes ready. Native geography is unavailable here.
//! the framework listener becomes ready. Optional geography comes from the
//! managed host's protected, immutable request outcome.

// Unit tests may buffer bodies with `Body::into_bytes`; production code must not.
#![cfg_attr(test, allow(clippy::disallowed_methods))]
Expand All @@ -10,6 +11,8 @@
pub mod app;
/// Request middleware for authentication and response finalization.
pub mod middleware;
/// Explicit native Geo boot profile and provider composition.
pub mod native_geo;
/// Native request-scoped platform services.
pub mod platform;
/// Prepared-store initialization and immutable native snapshots.
Expand Down
43 changes: 34 additions & 9 deletions crates/trusted-server-adapter-axum/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use edgezero_adapter_axum::dev_server::run_production_app_with_initializer;
use edgezero_adapter_axum::dev_server::{
AxumRunOptions, run_production_app_with_options_and_initializer,
};
use log::LevelFilter;
use trusted_server_adapter_axum::app::TrustedServerApp;
use trusted_server_adapter_axum::native_geo::load_selection;
use trusted_server_adapter_axum::startup::initialize_native;

/// Environment variable that sets the maximum log level.
Expand All @@ -26,20 +29,42 @@ fn main() {
eprintln!("warning: logger init failed: {e}");
}

if let Err(err) = run_production_app_with_initializer::<TrustedServerApp, _>(|app, stores| {
Box::pin(async move {
initialize_native(app, stores).await.inspect_err(|error| {
// RC classifies callback errors without their details. This safe
// application diagnostic tells operators which input to fix.
log::error!("Trusted Server initialization failed: {error}");
let options = match native_options() {
Ok(options) => options,
Err(error) => {
log::error!("Trusted Server native hosting configuration failed: {error}");
std::process::exit(1);
}
};
if let Err(err) = run_production_app_with_options_and_initializer::<TrustedServerApp, _>(
options,
|app, stores| {
Box::pin(async move {
initialize_native(app, stores).await.inspect_err(|error| {
// RC classifies callback errors without their details. This safe
// application diagnostic tells operators which input to fix.
log::error!("Trusted Server initialization failed: {error}");
})
})
})
}) {
},
) {
log::error!("trusted-server-adapter-axum failed: {err}");
std::process::exit(1);
}
}

/// Capture hosting inputs and validate the final provider selection before startup.
fn native_options() -> Result<AxumRunOptions, String> {
let selection = load_selection().map_err(|error| error.to_string())?;
let options = AxumRunOptions::from_env()
.map_err(|error| error.to_string())?
.with_geo(selection);
options
.geo_capability()
.map_err(|error| error.to_string())?;
Ok(options)
}

/// Parses an optional `RUST_LOG` value into the logger's maximum level.
///
/// Absent and blank values resolve to [`DEFAULT_LOG_LEVEL`]. Any other value
Expand Down
88 changes: 73 additions & 15 deletions crates/trusted-server-adapter-axum/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ use edgezero_core::error::EdgeError;
use edgezero_core::http::{HeaderValue, Response};
use edgezero_core::middleware::{Middleware, Next};
use trusted_server_core::auth::enforce_basic_auth;
use trusted_server_core::constants::HEADER_X_GEO_INFO_AVAILABLE;
use trusted_server_core::constants::{
HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY,
HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, HEADER_X_GEO_REGION,
};
use trusted_server_core::http_util::sanitize_trusted_client_ip_headers;
use trusted_server_core::platform::GeoInfo;
use trusted_server_core::settings::Settings;

use crate::platform::geo_from_outcome;

// ---------------------------------------------------------------------------
// SanitizeRequestMiddleware
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -52,8 +58,9 @@ impl Middleware for SanitizeRequestMiddleware {

/// Response-finalization middleware: injects all standard TS response headers.
///
/// Geo lookup is unavailable in the Axum dev server — `X-Geo-Info-Available: false`
/// is always emitted. `X-TS-Version` carries the compiled-in git version. The
/// Geo headers reflect the same immutable managed outcome used by handlers,
/// without resolving the provider during finalization. Missing detail is omitted.
/// `X-TS-Version` carries the compiled-in git version. The
/// Fastly-specific headers (`X-TS-Fastly-Version`, `X-TS-ENV`) are skipped
/// because the corresponding env vars are not set in a local dev context.
///
Expand All @@ -75,8 +82,9 @@ impl FinalizeResponseMiddleware {
#[async_trait(?Send)]
impl Middleware for FinalizeResponseMiddleware {
async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result<Response, EdgeError> {
let geo = geo_from_outcome(ctx.geo());
let mut response = next.run(ctx).await?;
apply_finalize_headers(&self.settings, &mut response);
apply_finalize_headers(&self.settings, geo.as_ref(), &mut response);
Ok(response)
}
}
Expand Down Expand Up @@ -125,16 +133,34 @@ impl Middleware for AuthMiddleware {

/// Applies standard Trusted Server response headers to the given response.
///
/// Unlike the Fastly variant, geo is always unavailable so `X-Geo-Info-Available: false`
/// is unconditionally emitted, followed by the compiled-in `X-TS-Version`.
/// Fastly-specific headers are omitted.
/// Clear stale upstream geographic headers, then emit only detail present in
/// the request snapshot. Fastly-specific headers are omitted.
/// Operator-configured `settings.response_headers` are applied last (with the
/// shared cookie cache-privacy hardening) and can override any managed header.
pub(crate) fn apply_finalize_headers(settings: &Settings, response: &mut Response) {
response.headers_mut().insert(
pub(crate) fn apply_finalize_headers(
settings: &Settings,
geo: Option<&GeoInfo>,
response: &mut Response,
) {
for name in [
HEADER_X_GEO_CITY,
HEADER_X_GEO_COUNTRY,
HEADER_X_GEO_CONTINENT,
HEADER_X_GEO_COORDINATES,
HEADER_X_GEO_METRO_CODE,
HEADER_X_GEO_REGION,
HEADER_X_GEO_INFO_AVAILABLE,
HeaderValue::from_static("false"),
);
] {
response.headers_mut().remove(name);
}
Comment on lines +145 to +155

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.

♻️ refactor: Geo header clearing now lives in two places with different coverage:

  • When geo is Some, this loop clears six headers and GeoInfo::set_response_headers clears the same six again.
  • Fastly's apply_finalize_headers (crates/trusted-server-adapter-fastly/src/middleware.rs:211-218) never clears them in its None branch, so an origin-supplied x-geo-country survives next to x-geo-info-available: false on Fastly but not on Axum.

One core helper used by both adapters would remove the duplication and the divergence:

// crates/trusted-server-core/src/geo.rs
pub fn apply_geo_response_headers(geo: Option<&GeoInfo>, response: &mut Response<EdgeBody>) {
    clear_geo_response_headers(response.headers_mut());
    match geo {
        Some(geo) => geo.insert_response_headers(response),
        None => {
            response.headers_mut().insert(
                HEADER_X_GEO_INFO_AVAILABLE,
                HeaderValue::from_static("false"),
            );
        }
    }
}

Apply manually: this can't be a one-click suggestion because it spans core and both adapters.

if let Some(geo) = geo {
geo.set_response_headers(response);
} else {
response.headers_mut().insert(
HEADER_X_GEO_INFO_AVAILABLE,
HeaderValue::from_static("false"),
);
}

trusted_server_core::version_header::apply_git_version_header(response);

Expand Down Expand Up @@ -213,7 +239,7 @@ mod tests {
let settings = settings_with_response_headers(vec![]);
let mut response = empty_response();

apply_finalize_headers(&settings, &mut response);
apply_finalize_headers(&settings, None, &mut response);

assert_eq!(
response
Expand All @@ -231,7 +257,7 @@ mod tests {
settings_with_response_headers(vec![("X-Geo-Info-Available", "operator-override")]);
let mut response = empty_response();

apply_finalize_headers(&settings, &mut response);
apply_finalize_headers(&settings, None, &mut response);

assert_eq!(
response
Expand All @@ -248,7 +274,7 @@ mod tests {
let settings = settings_with_response_headers(vec![("X-Custom-Header", "custom-value")]);
let mut response = empty_response();

apply_finalize_headers(&settings, &mut response);
apply_finalize_headers(&settings, None, &mut response);

assert_eq!(
response
Expand All @@ -260,6 +286,38 @@ mod tests {
);
}

#[test]
fn partial_geo_emits_no_invented_or_stale_detail() {
let settings = settings_with_response_headers(vec![]);
let mut response = empty_response();
response
.headers_mut()
.insert("x-geo-coordinates", HeaderValue::from_static("0,0"));
response
.headers_mut()
.insert("x-geo-city", HeaderValue::from_static("stale-origin-city"));
let geo = GeoInfo::country_region("US", Some("CA".to_owned()));
apply_finalize_headers(&settings, Some(&geo), &mut response);
assert_eq!(response.headers()["x-geo-info-available"], "true");
assert_eq!(response.headers()["x-geo-country"], "US");
assert_eq!(response.headers()["x-geo-region"], "CA");
for name in [
"x-geo-coordinates",
"x-geo-city",
"x-geo-continent",
"x-geo-metro-code",
] {
assert!(
!response.headers().contains_key(name),
"should omit unavailable detail"
);
}
apply_finalize_headers(&settings, None, &mut response);
assert_eq!(response.headers()["x-geo-info-available"], "false");
assert!(!response.headers().contains_key("x-geo-country"));
assert!(!response.headers().contains_key("x-geo-region"));
}

#[test]
fn sanitize_middleware_strips_configured_trust_headers_before_routing() {
let mut settings = settings_with_response_headers(vec![]);
Expand Down Expand Up @@ -295,7 +353,7 @@ mod tests {
#[test]
fn emits_git_version_header() {
let mut response = empty_response();
apply_finalize_headers(&settings_with_response_headers(vec![]), &mut response);
apply_finalize_headers(&settings_with_response_headers(vec![]), None, &mut response);
assert_eq!(
response
.headers()
Expand Down
Loading
Loading