Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** `x-ts-version` now reports the deployed git version — the release tag, else the branch, else the first 6 characters of the commit — compiled in from the build-time `TRUSTED_SERVER_GIT_VERSION` (set by the deploy pipeline) or local git, and is sent by every adapter, including on the Fastly `GET /health` probe. The Fastly service version it previously carried is now `x-ts-fastly-version`; update dashboards, monitors, and scripts that read `x-ts-version` as the Fastly version number. Builds with neither the override nor git omit the header.
- **Breaking:** Auction providers and bidder routes now use the configuration-first `[auction.providers.<id>]` and `[auction.bidders.<id>]` maps. The removed `[auction].providers = [...]` list and removed server fields under `[integrations.prebid]` and `[integrations.aps]` are rejected even when those integrations are disabled, and `ts config push` rejects the old shape before publication. Move PBS `server_url` to provider `endpoint`, server timeout to provider `timeout_ms`, request controls and bidder-parameter overrides to the `prebid-server` `profile_config`, notification suppression to `notifications`, and each former server bidder to an `[auction.bidders.<id>]` route. Move APS endpoint, timeout, account, inventory, debug, and creative controls to an `aps` provider and its `profile_config`. Browser Prebid settings remain under `[integrations.prebid]`; values such as timeout and debug that previously affected both browser and server behavior must now be configured for each owner. Provider endpoints must be absolute HTTPS URLs. Only bidder codes present in `[auction.bidders]` are folded into Trusted Server requests; unlisted publisher bids remain native browser demand. Provider response names now use the configured provider ID, such as `pbs-main`, instead of the legacy literal `prebid`; audit consumers that match `AuctionResponse.provider`. This schema has no mixed-version-safe deployment order: old binaries reject the maps and new binaries reject the retired fields, so activate the new binary and config blob together. Rollbacks must restore an old-schema blob together with the old binary.
- **Breaking** — Admin Basic-auth coverage now includes `GET /_ts/admin/ec`, `GET /_ts/admin/ec/{id}`, and `GET /_ts/admin/eids`. Existing configurations whose `[[handlers]]` patterns protect only the key-management endpoints now fail startup; broaden coverage before deploying, preferably with a namespace-boundary pattern such as `^/_ts/admin(?:/|$)`. Coverage of the dynamic `/_ts/admin/ec/{id}` route is no longer inferred from ID-shaped samples: the router accepts any segment after `/_ts/admin/ec/` and Basic Auth runs on the raw path before routing, so patterns anchored to the EC ID grammar (for example `^/_ts/admin/ec/[a-f0-9]{64}[.][A-Za-z0-9]{6}$`) are rejected in favor of a prefix-level matcher. Placeholder and well-known weak handler passwords (`changeme`, `password`, `admin`, `replace-with-…`) now fail startup on every handler rather than only on handlers inferred to cover an admin endpoint, because first-match-wins handler selection lets a narrow handler shadow the admin namespace.
- Prebid Server provider endpoints now normalize origin-only legacy `server_url` values to `/openrtb2/auction`. Query parameters are preserved, the canonical path loses a trailing slash, and configured non-root custom paths remain exact.
Expand Down
24 changes: 21 additions & 3 deletions crates/trusted-server-adapter-axum/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,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. Fastly-specific headers (`X-TS-Version`, `X-TS-ENV`) are
/// skipped because the corresponding env vars are not set in a local dev context.
/// is always emitted. `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.
///
/// Registered directly inside [`SanitizeRequestMiddleware`] and ahead of
/// [`AuthMiddleware`] so that every outgoing response — including auth-rejected
Expand Down Expand Up @@ -125,7 +126,8 @@ 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. Fastly-specific headers are omitted.
/// is unconditionally emitted, followed by the compiled-in `X-TS-Version`.
/// 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) {
Expand All @@ -134,6 +136,8 @@ pub(crate) fn apply_finalize_headers(settings: &Settings, response: &mut Respons
HeaderValue::from_static("false"),
);

trusted_server_core::version_header::apply_git_version_header(response);

// Cookie-bearing responses stay private to shared caches and operator
// headers cannot re-enable caching for uncacheable per-user payloads.
trusted_server_core::response_privacy::apply_response_headers_with_cache_privacy(
Expand Down Expand Up @@ -287,4 +291,18 @@ mod tests {
"should remove both configured trust headers before the handler"
);
}

#[test]
fn emits_git_version_header() {
let mut response = empty_response();
apply_finalize_headers(&settings_with_response_headers(vec![]), &mut response);
assert_eq!(
response
.headers()
.get("x-ts-version")
.and_then(|v| v.to_str().ok()),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version as x-ts-version"
);
}
}
32 changes: 32 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,3 +938,35 @@ async fn nextjs_auction_output_holds_until_the_structural_body_close() {
"should not leak generated placeholders: {html}"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn health_reports_git_version() {
let mut service = make_service();
let request = Request::builder()
.method("GET")
.uri("/health")
.body(AxumBody::empty())
.expect("should build health request");

let response = service
.ready()
.await
.expect("should be ready")
.call(request)
.await
.expect("should serve health");

assert_eq!(
response.status().as_u16(),
200,
"should return 200 on /health"
);
assert_eq!(
response
.headers()
.get("x-ts-version")
.and_then(|v| v.to_str().ok()),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version on /health"
);
}
20 changes: 20 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ pub(crate) fn apply_finalize_headers(
HeaderValue::from_static(if geo_available { "true" } else { "false" }),
);

trusted_server_core::version_header::apply_git_version_header(response);

// Cloudflare is a real shared cache: cookie-bearing responses must stay
// private and operator headers must not re-enable caching for uncacheable
// per-user payloads.
Expand Down Expand Up @@ -320,4 +322,22 @@ mod tests {
"should remove both configured trust headers before the handler"
);
}

#[test]
fn emits_git_version_header() {
let mut response = empty_response();
apply_finalize_headers(
&settings_with_response_headers(vec![]),
false,
&mut response,
);
assert_eq!(
response
.headers()
.get("x-ts-version")
.and_then(|v| v.to_str().ok()),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version as x-ts-version"
);
}
}
26 changes: 25 additions & 1 deletion crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use fastly::http::Method as FastlyMethod;
use fastly::{Request as FastlyRequest, Response as FastlyResponse};

use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::constants::HEADER_X_TS_VERSION;
use trusted_server_core::ec::device::DeviceSignals;
use trusted_server_core::ec::finalize::ec_finalize_response;
use trusted_server_core::ec::kv::KvIdentityGraph;
Expand All @@ -28,6 +29,7 @@ use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::proxy::{AssetProxyCachePolicy, stream_asset_body};
use trusted_server_core::response_privacy::TerminalPrivateResponse;
use trusted_server_core::settings::Settings;
use trusted_server_core::version_header::git_version_header_value;

mod app;
mod backend;
Expand Down Expand Up @@ -64,7 +66,12 @@ fn open_trusted_server_config_store(store_name: &str) -> Result<ConfigStoreHandl

fn health_response(req: &FastlyRequest) -> Option<FastlyResponse> {
if req.get_method() == FastlyMethod::GET && req.get_path() == "/health" {
return Some(FastlyResponse::from_status(200).with_body_text_plain("ok"));
let mut response = FastlyResponse::from_status(200).with_body_text_plain("ok");
// Compiled-in constant: keeps the probe free of settings and app construction.
if let Some(version) = git_version_header_value() {
response.set_header(HEADER_X_TS_VERSION, version);
}
return Some(response);
}

None
Expand Down Expand Up @@ -558,6 +565,23 @@ mod tests {
);
}

#[test]
fn health_response_reports_git_version_only() {
let req = FastlyRequest::get("https://example.com/health");

let response = health_response(&req).expect("should build health response");

assert_eq!(
response.get_header_str("x-ts-version"),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version on /health"
);
assert!(
response.get_header("x-ts-fastly-version").is_none(),
"should keep x-ts-fastly-version off the /health fast path"
);
}

#[test]
fn health_response_ignores_non_health_paths() {
let req = FastlyRequest::get("https://example.com/auction");
Expand Down
35 changes: 31 additions & 4 deletions crates/trusted-server-adapter-fastly/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ use std::net::IpAddr;
use trusted_server_core::auth::enforce_basic_auth;
use trusted_server_core::constants::{
ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_GEO_INFO_AVAILABLE,
HEADER_X_TS_ENV, HEADER_X_TS_VERSION,
HEADER_X_TS_ENV, HEADER_X_TS_FASTLY_VERSION,
};
use trusted_server_core::geo::GeoInfo;
use trusted_server_core::platform::{ClientInfo, PlatformGeo};
use trusted_server_core::settings::Settings;
use trusted_server_core::version_header::apply_git_version_header;

pub(crate) const HEADER_X_TS_FINALIZED: &str = "x-ts-finalized";

Expand All @@ -49,7 +50,8 @@ pub(crate) const HEADER_X_TS_FINALIZED: &str = "x-ts-finalized";
///
/// Headers are written in this order (last write wins):
/// 1. Geo headers (or `X-Geo-Info-Available: false` when geo is unavailable)
/// 2. `X-TS-Version` from `FASTLY_SERVICE_VERSION` env var
/// 2. `X-TS-Version` from the compiled-in git version (`TS_GIT_VERSION`), and
/// `X-TS-Fastly-Version` from the `FASTLY_SERVICE_VERSION` env var
/// 3. `X-TS-ENV: staging` when `FASTLY_IS_STAGING == "1"`
/// 4. Operator-configured `settings.response_headers` (can override any managed header)
pub struct FinalizeResponseMiddleware {
Expand Down Expand Up @@ -187,7 +189,8 @@ where
///
/// Header write order (last write wins):
/// 1. Geo headers (`x-geo-*`) — or `X-Geo-Info-Available: false` when absent
/// 2. `X-TS-Version` from `FASTLY_SERVICE_VERSION` env var
/// 2. `X-TS-Version` from the compiled-in git version (`TS_GIT_VERSION`), and
/// `X-TS-Fastly-Version` from the `FASTLY_SERVICE_VERSION` env var
/// 3. `X-TS-ENV: staging` when `FASTLY_IS_STAGING == "1"`
/// 4. Set-Cookie cache privacy — strip surrogate cache headers and downgrade
/// `Cache-Control` to `private, max-age=0` on cookie-bearing responses
Expand All @@ -208,9 +211,13 @@ pub(crate) fn apply_finalize_headers(
);
}

apply_git_version_header(response);

if let Ok(v) = std::env::var(ENV_FASTLY_SERVICE_VERSION) {
if let Ok(value) = HeaderValue::from_str(&v) {
response.headers_mut().insert(HEADER_X_TS_VERSION, value);
response
.headers_mut()
.insert(HEADER_X_TS_FASTLY_VERSION, value);
} else {
log::warn!("Skipping invalid FASTLY_SERVICE_VERSION response header value");
}
Expand Down Expand Up @@ -813,4 +820,24 @@ mod tests {
"should reach the handler when auth is not required"
);
}

#[test]
fn version_headers_split_git_and_fastly_versions() {
let settings = settings_with_response_headers(vec![]);
let mut response = empty_response();

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

let header = |name: &str| response.headers().get(name).and_then(|v| v.to_str().ok());
assert_eq!(
header("x-ts-version"),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version as x-ts-version"
);
assert_eq!(
header("x-ts-fastly-version"),
std::env::var(ENV_FASTLY_SERVICE_VERSION).ok().as_deref(),
"should report FASTLY_SERVICE_VERSION as x-ts-fastly-version"
);
}
}
7 changes: 7 additions & 0 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,13 @@ mod tests {
200,
"GET /health must return 200 from the startup fallback"
);
assert_eq!(
resp.headers()
.get("x-ts-version")
.and_then(|v| v.to_str().ok()),
trusted_server_core::constants::TS_GIT_VERSION,
"startup-fallback /health should report the compiled-in git version"
);
let body = resp.into_body().into_bytes().unwrap_or_default();
assert_eq!(
&body[..],
Expand Down
20 changes: 20 additions & 0 deletions crates/trusted-server-adapter-spin/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ pub(crate) fn apply_finalize_headers(
HeaderValue::from_static(if geo_available { "true" } else { "false" }),
);

trusted_server_core::version_header::apply_git_version_header(response);

// Cookie-bearing responses stay private to shared caches and operator
// headers cannot re-enable caching for uncacheable per-user payloads.
trusted_server_core::response_privacy::apply_response_headers_with_cache_privacy(
Expand Down Expand Up @@ -347,4 +349,22 @@ mod tests {
"should remove both configured trust headers before the handler"
);
}

#[test]
fn emits_git_version_header() {
let mut response = empty_response();
apply_finalize_headers(
&settings_with_response_headers(vec![]),
false,
&mut response,
);
assert_eq!(
response
.headers()
.get("x-ts-version")
.and_then(|v| v.to_str().ok()),
trusted_server_core::constants::TS_GIT_VERSION,
"should report the compiled-in git version as x-ts-version"
);
}
}
Loading
Loading