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
e40a490
Retain provider-local HTTP opt-in in the demo baseline
ChristianPavilonis Nov 11, 2026
fe54116
Carry the baseline toolchain correction into HTTP opt-in
ChristianPavilonis Nov 11, 2026
4aa2ebd
Format the retained HTTP provider documentation
ChristianPavilonis Nov 11, 2026
70a8945
Carry the startup probe correction through the compatible baseline
ChristianPavilonis Nov 11, 2026
1cfba70
Carry the browser CI host compatibility fix through the baseline
ChristianPavilonis Nov 11, 2026
8003251
Carry the tagged EdgeZero snapshot through the stack
ChristianPavilonis Nov 11, 2026
6501eed
Carry the monthly EdgeZero tag through the stack
ChristianPavilonis Nov 11, 2026
ade11a5
Retain per-provider HTTP opt-in alongside main's loopback rule
ChristianPavilonis Nov 12, 2026
656f0b3
Retain the owning partial Geo fixture continuation
ChristianPavilonis Nov 12, 2026
cd02c26
Retain the owning partial-Geo parity fixture correction
ChristianPavilonis Nov 12, 2026
2791483
Retain the concurrent cutover documentation closeout
ChristianPavilonis Nov 12, 2026
577bc74
Retain prepared native development documentation
ChristianPavilonis Nov 12, 2026
2cbff6d
Retain current main validation and mediator fallback
ChristianPavilonis Nov 12, 2026
25a8ea5
Retain the Cargo target-aware cache harness
ChristianPavilonis Nov 12, 2026
98e5ba1
Retain target-aware GPT bundle lookup for the cache fixture
ChristianPavilonis Nov 12, 2026
4685d91
Carry hosted qualification fixes through PR 1533
ChristianPavilonis Nov 12, 2026
95a4c50
Carry hosted qualification fixes through PR 1533
ChristianPavilonis Nov 12, 2026
4b1fc64
Carry hosted qualification fixes through PR 1533
ChristianPavilonis Nov 12, 2026
c8bfc0b
Carry hosted qualification fixes through PR 1533
ChristianPavilonis Nov 12, 2026
82d9a0e
Carry hosted qualification fixes through PR 1533
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
1 change: 1 addition & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ async fn aps_profile_serves_renderer_through_adapter_fallback() {
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ mod tests {
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
Expand Down Expand Up @@ -938,6 +939,7 @@ mod tests {
(
id.parse().expect("should parse provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: format!("https://{id}.example/openrtb"),
Expand Down Expand Up @@ -982,6 +984,7 @@ mod tests {
(
id.parse().expect("should parse provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: format!("https://{id}.example/openrtb"),
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,7 @@ mod tests {
.parse()
.expect("should parse provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: format!("https://bid.example:{}/openrtb", 9000 + index),
Expand Down Expand Up @@ -2086,6 +2087,7 @@ mod tests {
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ mod tests {
(
id.parse().expect("should parse provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: format!("https://{id}.example/openrtb"),
Expand All @@ -1096,6 +1097,7 @@ mod tests {
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
trusted_server_core::auction::ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ mod tests {
providers: BTreeMap::from([(
ProviderId::from_str("pbs-primary").expect("should parse provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: "https://pbs.example.test/openrtb".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ mod plan_sharing_tests {
settings.auction.providers = std::collections::BTreeMap::from([(
"aps-main".parse().expect("should parse APS provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction/openrtb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ mod routing_metadata_tests {
providers: BTreeMap::from([(
provider_id.clone(),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: profile.to_string(),
endpoint: if profile == "aps" {
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction/openrtb/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn config_with_endpoint(profile: &str, profile_config: Value, endpoint: &str) ->
providers: BTreeMap::from([(
provider_id.clone(),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: profile.to_string(),
endpoint: endpoint.to_string(),
Expand Down
4 changes: 4 additions & 0 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,7 @@ mod tests {
(
ProviderId::from_str(id).expect("should parse fictional provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: PLANNED_ENDPOINT.to_string(),
Expand Down Expand Up @@ -1776,6 +1777,7 @@ mod tests {
(
ProviderId::from_str(id).expect("should parse fictional provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: format!("https://{id}.example.test/openrtb"),
Expand Down Expand Up @@ -1812,6 +1814,7 @@ mod tests {
(
ProviderId::from_str(id).expect("should parse fictional provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: APS_ENDPOINT.to_string(),
Expand Down Expand Up @@ -1903,6 +1906,7 @@ mod tests {
ProviderId::from_str(id)
.expect("should parse fictional provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: (*endpoint).to_string(),
Expand Down
91 changes: 87 additions & 4 deletions crates/trusted-server-core/src/auction/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ pub struct ProviderConfig {
pub profile: String,
/// Fixed provider endpoint.
pub endpoint: String,
/// Permit non-loopback unencrypted HTTP for this provider only. Defaults to false.
/// The existing literal-loopback exception remains. Use this opt-in only for
/// explicitly approved demo/test endpoints.
#[serde(default, skip_serializing_if = "is_false")]
Comment on lines +143 to +144

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 — Nothing logs when a provider compiles with allow_insecure_http = true. A one-time log::warn! during AuctionPlan::compile naming the provider would make an accidentally enabled opt-in in a non-demo config visible in startup logs. Not blocking; it would sit near the canonicalize_endpoint call (outside this hunk, so no suggestion).

pub allow_insecure_http: bool,
/// Optional profile-default timeout override.
#[serde(default)]
pub timeout_ms: Option<u32>,
Expand Down Expand Up @@ -316,7 +321,12 @@ impl AuctionPlan {
"provider `{id}` profile_config must be an object"
)));
}
let endpoint = canonicalize_endpoint(&id, registration.id, &raw.endpoint)?;
let endpoint = canonicalize_endpoint(
&id,
registration.id,
&raw.endpoint,
raw.allow_insecure_http,
)?;
Comment on lines +324 to +329

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: Nothing tells the operator that a provider is running in plaintext.

I ran an opted-in prebid-server provider end to end at this head on Axum and on Fastly (Viceroy). ts config validate --strict and ts config push printed only their success lines, and neither adapter logged anything about it, while the stub bidder received a plaintext POST /openrtb2/auction (Fastly registered backend_http_127_0_0_1_…_p_pbs-main_…, no TLS) carrying the client IP (X-Forwarded-For and device.ip), the User-Agent, the TCF string, the raw euconsent-v2 cookie header, and on Fastly device.geo with lat/lon and city. Its bid came back and was rendered, so responses (price, adm, nurl/burl) can be rewritten in transit too.

Both branches already flag the equivalent TLS weakening on Fastly with an INSECURE: warning (warn_if_certificate_check_disabled at crates/trusted-server-adapter-fastly/src/app.rs:242, and backend.rs:251). Logging at plan compile covers every adapter from one place. It fires during runtime validation (config_payload.rs:140) and again in each adapter's build_state, so expect it twice per instance start. The CLI installs no logger, so it won't show at ts config validate (see the 📌 in the review body). Same finding as #1525 r1, with the INSECURE: prefix added to match.

Suggested change
let endpoint = canonicalize_endpoint(
&id,
registration.id,
&raw.endpoint,
raw.allow_insecure_http,
)?;
let endpoint = canonicalize_endpoint(
&id,
registration.id,
&raw.endpoint,
raw.allow_insecure_http,
)?;
if endpoint.url().scheme() == "http" {
// This logs a configured provider identifier, not request data or secrets.
log::warn!(
"INSECURE: provider `{id}` uses unencrypted HTTP; its requests carry client IP, geo, consent signals and identifiers in cleartext, and its responses can be altered in transit"
);
}

let timeout_ms = raw
.timeout_ms
.unwrap_or(match registration.default_timeout {
Expand Down Expand Up @@ -537,20 +547,23 @@ fn canonicalize_endpoint(
provider_id: &ProviderId,
profile_id: &str,
value: &str,
allow_insecure_http: bool,
) -> Result<CanonicalProviderEndpoint, Report<TrustedServerError>> {
let mut endpoint = Url::parse(value).map_err(|error| {
configuration_error(format!(
"provider `{provider_id}` endpoint must be absolute HTTPS, or plain HTTP to 127.0.0.1 or [::1]: {error}"
"provider `{provider_id}` endpoint must be absolute HTTPS, or plain HTTP to 127.0.0.1 or [::1], unless this provider explicitly enables allow_insecure_http: {error}"
))
})?;
if !(endpoint.scheme() == "https" || is_loopback_http_endpoint(&endpoint))
if !(endpoint.scheme() == "https"
|| is_loopback_http_endpoint(&endpoint)
|| (allow_insecure_http && endpoint.scheme() == "http"))
|| endpoint.host_str().is_none()
|| !endpoint.username().is_empty()
|| endpoint.password().is_some()
|| endpoint.fragment().is_some()
{
return Err(configuration_error(format!(
"provider `{provider_id}` endpoint must be absolute HTTPS, or plain HTTP to 127.0.0.1 or [::1], with a host and no credentials or fragment"
"provider `{provider_id}` endpoint must be absolute HTTPS, or plain HTTP to 127.0.0.1 or [::1], unless this provider explicitly enables allow_insecure_http; a host and no credentials or fragment are always required"
)));
}
if profile_id == "aps"
Expand All @@ -569,6 +582,10 @@ fn canonicalize_endpoint(
Ok(CanonicalProviderEndpoint(endpoint))
}

fn is_false(value: &bool) -> bool {

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: settings.rs:2629 already defines the same is_false serde predicate, with a doc comment. Make that one pub(crate) and point this field at it, so the two can't drift:

// settings.rs
/// Serde predicate for omitting `false` flags from serialized config blobs.
pub(crate) fn is_false(value: &bool) -> bool {
    !*value
}
// plan.rs
#[serde(default, skip_serializing_if = "crate::settings::is_false")]
pub allow_insecure_http: bool,

Then delete this copy. Apply manually: the fix touches settings.rs, which is outside this diff.

!*value
}

fn normalize_prebid_server_endpoint(endpoint: &mut Url) {
match endpoint.path() {
"" | "/" => endpoint.set_path("/openrtb2/auction"),
Expand Down Expand Up @@ -615,6 +632,7 @@ mod tests {

fn provider(profile: &str) -> ProviderConfig {
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: profile.to_string(),
endpoint: "https://bid.example/openrtb2/auction".to_string(),
Expand Down Expand Up @@ -1022,6 +1040,71 @@ mod tests {
);
}

#[test]
fn insecure_http_requires_an_explicit_per_provider_opt_in() {
let mut demo = provider("prebid-server");
demo.endpoint = "http://pbs.example.com:8000".to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err()
);
demo.allow_insecure_http = true;
let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())])))
.expect("should compile an explicitly approved demo HTTP endpoint");
assert_eq!(
plan.providers()[0].endpoint.as_str(),
"http://pbs.example.com:8000/openrtb2/auction"
);
let mut other = provider("standard");
other.endpoint = "http://other.example/bid".to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([
(id("demo"), demo),
(id("other"), other)
])))
.is_err()
);
Comment on lines +1047 to +1065

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 tests don't check how an opted-in provider is dialed, or that the flag leaves HTTPS alone.

I ran 9 mutants of plan.rs against the full trusted-server-core lib suite (3005 tests) at this head. The scope, scheme, credential, fragment and serde mutants are all killed, but two survive:

  • backend_spec_with_transport_timeout hard-coding scheme: "https". Every opted-in HTTP provider would then dial TLS (Fastly would register an https_… backend with enable_ssl instead of the backend_http_… one I saw under Viceroy), and CI stays green.
  • canonicalize_endpoint calling set_scheme("http") when the flag is set, which silently downgrades an opted-in HTTPS endpoint. Nothing asserts that the flag leaves HTTPS alone.

With this suggestion applied, both mutants fail this test (should dial the opted-in provider without TLS on its configured port and should not downgrade HTTPS when the provider opts in to HTTP). It also gives the bare asserts the messages AGENTS.md asks for, and checks backend prediction on all four targets. prk-Jr's expect() suggestion on the wire-shape test below leaves its asserts without messages; worth adding them there too.

Suggested change
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err()
);
demo.allow_insecure_http = true;
let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())])))
.expect("explicit demo HTTP endpoint should compile");
assert_eq!(
plan.providers()[0].endpoint.as_str(),
"http://pbs.example.com:8000/openrtb2/auction"
);
let mut other = provider("standard");
other.endpoint = "http://other.example/bid".to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([
(id("demo"), demo),
(id("other"), other)
])))
.is_err()
);
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err(),
"should reject HTTP without the provider's explicit opt-in"
);
demo.allow_insecure_http = true;
let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())])))
.expect("should compile an explicitly opted-in HTTP provider");
assert_eq!(
plan.providers()[0].endpoint.as_str(),
"http://pbs.example.com:8000/openrtb2/auction",
"should normalize the opted-in Prebid Server endpoint"
);
let spec = plan.providers()[0].backend_spec();
assert_eq!(
(spec.scheme.as_str(), spec.port),
("http", Some(8000)),
"should dial the opted-in provider without TLS on its configured port"
);
for target in [
AuctionTargetId::Fastly,
AuctionTargetId::Axum,
AuctionTargetId::Cloudflare,
AuctionTargetId::Spin,
] {
plan.validate_for_target(target)
.expect("should predict a backend for the opted-in HTTP provider");
}
let mut secure = demo.clone();
secure.endpoint = "https://pbs.example.com".to_string();
let secure_plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), secure)])))
.expect("should compile an opted-in HTTPS provider");
assert_eq!(
secure_plan.providers()[0].endpoint.as_str(),
"https://pbs.example.com/openrtb2/auction",
"should not downgrade HTTPS when the provider opts in to HTTP"
);
let mut other = provider("standard");
other.endpoint = "http://other.example/bid".to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([
(id("demo"), demo),
(id("other"), other)
])))
.is_err(),
"should not inherit another provider's HTTP opt-in"
);

Comment on lines +1047 to +1065

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 — The new assertions have no messages, and the last is_err() would also pass if demo were the provider rejected. AGENTS.md asks for descriptive assertion messages; expect_err plus a check on the provider ID pins the rejection to the provider without the opt-in.

Suggested change
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err()
);
demo.allow_insecure_http = true;
let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())])))
.expect("should compile an explicitly approved demo HTTP endpoint");
assert_eq!(
plan.providers()[0].endpoint.as_str(),
"http://pbs.example.com:8000/openrtb2/auction"
);
let mut other = provider("standard");
other.endpoint = "http://other.example/bid".to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([
(id("demo"), demo),
(id("other"), other)
])))
.is_err()
);
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err(),
"should reject non-loopback HTTP without the per-provider opt-in"
);
demo.allow_insecure_http = true;
let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())])))
.expect("should compile an explicitly approved demo HTTP endpoint");
assert_eq!(
plan.providers()[0].endpoint.as_str(),
"http://pbs.example.com:8000/openrtb2/auction",
"should keep HTTP and normalize the Prebid Server path"
);
let mut other = provider("standard");
other.endpoint = "http://other.example/bid".to_string();
let error = AuctionPlan::compile(config(BTreeMap::from([
(id("demo"), demo),
(id("other"), other),
])))
.expect_err("should reject HTTP for a provider that did not opt in");
assert!(
error.to_string().contains("provider `other`"),
"should attribute the rejection to the provider without the opt-in: {error:?}"
);

}

#[test]
fn insecure_http_opt_in_does_not_allow_credentials_fragments_or_other_schemes() {
for endpoint in [
"http://user:pass@pbs.example/bid",
"http://pbs.example/bid#fragment",
"ftp://pbs.example/bid",
"/relative",
] {
let mut raw = provider("prebid-server");
raw.allow_insecure_http = true;
raw.endpoint = endpoint.to_string();
assert!(
AuctionPlan::compile(config(BTreeMap::from([(id("demo"), raw)]))).is_err(),
"should reject {endpoint}"
);
}
}

#[test]
fn insecure_http_default_preserves_existing_provider_wire_shape() {
let legacy = serde_json::to_value(provider("prebid-server"))
.expect("should serialize the existing provider wire shape");
assert!(legacy.get("allow_insecure_http").is_none());
assert!(
!serde_json::from_value::<ProviderConfig>(legacy)
.expect("should deserialize an existing provider without the opt-in")
.allow_insecure_http
);
let mut opted_in = provider("prebid-server");
opted_in.allow_insecure_http = true;
let encoded =
serde_json::to_value(opted_in).expect("should serialize the explicit HTTP opt-in");
assert_eq!(encoded["allow_insecure_http"], true);
assert!(
serde_json::from_value::<ProviderConfig>(encoded)
.expect("should deserialize the explicit HTTP opt-in")
.allow_insecure_http
);
Comment on lines +1090 to +1105

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 — These assertions also have no messages (AGENTS.md asks for descriptive assertion messages). This adds one to each so a failure says which part of the wire-shape contract broke.

Suggested change
assert!(legacy.get("allow_insecure_http").is_none());
assert!(
!serde_json::from_value::<ProviderConfig>(legacy)
.expect("should deserialize an existing provider without the opt-in")
.allow_insecure_http
);
let mut opted_in = provider("prebid-server");
opted_in.allow_insecure_http = true;
let encoded =
serde_json::to_value(opted_in).expect("should serialize the explicit HTTP opt-in");
assert_eq!(encoded["allow_insecure_http"], true);
assert!(
serde_json::from_value::<ProviderConfig>(encoded)
.expect("should deserialize the explicit HTTP opt-in")
.allow_insecure_http
);
assert!(
legacy.get("allow_insecure_http").is_none(),
"should omit the default opt-in from the serialized wire shape"
);
assert!(
!serde_json::from_value::<ProviderConfig>(legacy)
.expect("should deserialize an existing provider without the opt-in")
.allow_insecure_http,
"should default the opt-in to false when the field is absent"
);
let mut opted_in = provider("prebid-server");
opted_in.allow_insecure_http = true;
let encoded =
serde_json::to_value(opted_in).expect("should serialize the explicit HTTP opt-in");
assert_eq!(
encoded["allow_insecure_http"], true,
"should serialize an enabled opt-in"
);
assert!(
serde_json::from_value::<ProviderConfig>(encoded)
.expect("should deserialize the explicit HTTP opt-in")
.allow_insecure_http,
"should round-trip an enabled opt-in"
);

}

#[test]
fn compiler_normalizes_only_prebid_server_origin_and_canonical_paths() {
for (configured, expected) in [
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ mod tests {

fn provider(profile: &str, routing: RoutingMode) -> ProviderConfig {
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: profile.to_string(),
endpoint: format!("https://{profile}.example.test/openrtb"),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/auction_config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ impl AuctionConfig {
(
id,
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "standard".to_string(),
endpoint: format!("https://{name}.example/openrtb2/auction"),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ formats = [{ width = 300, height = 250 }]
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example.com/e/pb/bid".to_string(),
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/src/creative_opportunities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2352,6 +2352,7 @@ mod tests {
providers: BTreeMap::from([(
ProviderId::from_str("pbs-primary").expect("should parse provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: "https://pbs.example.test/openrtb".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-core/src/integrations/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,7 @@ pub fn register(
providers: BTreeMap::from([(
"aps".parse().expect("should parse APS provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: default_endpoint(),
Expand Down Expand Up @@ -3095,6 +3096,7 @@ mod tests {
providers: BTreeMap::from([(
"aps".parse().expect("should parse APS provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: default_endpoint(),
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-core/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5546,6 +5546,7 @@ external_bundle_sri = "sha384-AAAA"
(
ProviderId::from_str("pbs-primary").expect("should parse provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: "https://primary.example.test/openrtb".to_string(),
Expand All @@ -5558,6 +5559,7 @@ external_bundle_sri = "sha384-AAAA"
(
ProviderId::from_str("pbs-secondary").expect("should parse provider ID"),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: "https://secondary.example.test/openrtb".to_string(),
Expand Down Expand Up @@ -9067,6 +9069,7 @@ set = { networkId = 42 }
providers: BTreeMap::from([(
provider_id.clone(),
ProviderConfig {
allow_insecure_http: false,
protocol: "openrtb-2.6".to_string(),
profile: "prebid-server".to_string(),
endpoint: "https://pbs.example/openrtb2/auction".to_string(),
Expand Down
25 changes: 16 additions & 9 deletions docs/guide/auction-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,15 +775,22 @@ registered integration selected by `[auction].mediator`.

Common provider fields and defaults:

| Field | Default | Meaning |
| ---------------- | --------------- | -------------------------------------------------------------- |
| `protocol` | Required | `openrtb-2.6` |
| `profile` | `standard` | Typed OpenRTB behavior |
| `endpoint` | Required | Fixed absolute HTTPS endpoint |
| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout |
| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles |
| `profile_config` | `{}` | Profile-owned typed settings |
| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats |
| Field | Default | Meaning |
| --------------------- | --------------- | ---------------------------------------------------------------------------------- |
| `protocol` | Required | `openrtb-2.6` |
| `profile` | `standard` | Typed OpenRTB behavior |
| `endpoint` | Required | Fixed absolute HTTPS endpoint |

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 row still says the endpoint must be HTTPS, but the next row and the new paragraph add HTTP exceptions. A pointer avoids the contradiction.

Suggested change
| `endpoint` | Required | Fixed absolute HTTPS endpoint |
| `endpoint` | Required | Fixed absolute HTTPS endpoint; see the HTTP exceptions below |

| `allow_insecure_http` | `false` | Explicit per-provider HTTP opt-in for approved demos/tests; traffic is unencrypted |
| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout |
| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles |
| `profile_config` | `{}` | Profile-owned typed settings |
| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats |

Comment on lines +782 to 788

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: The endpoint row two lines up still reads as HTTPS-only, and "traffic is unencrypted" understates what an operator opts into: client IP, geo, consent signals and identifiers leave in cleartext, and responses can be altered. A short warning block keeps the table cell readable. Cell widths are sized so prettier leaves the other rows alone.

Suggested change
| `endpoint` | Required | Fixed absolute HTTPS endpoint |
| `allow_insecure_http` | `false` | Explicit per-provider HTTP opt-in for approved demos/tests; traffic is unencrypted |
| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout |
| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles |
| `profile_config` | `{}` | Profile-owned typed settings |
| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats |
| `endpoint` | Required | Fixed absolute HTTPS endpoint; plain HTTP only with `allow_insecure_http` |
| `allow_insecure_http` | `false` | Per-provider plain HTTP opt-in for approved demo/test endpoints; see warning below |
| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout |
| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles |
| `profile_config` | `{}` | Profile-owned typed settings |
| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats |
::: warning Plaintext provider traffic
`allow_insecure_http = true` sends that provider's requests without TLS. Their
headers and OpenRTB bodies, including client IP, User-Agent, geo, consent
signals and identifiers such as the EC ID and EIDs, are readable on the
network, and bid prices, creatives and notification URLs in its responses can
be altered in transit. The flag does not restrict which hosts are reachable.
:::

HTTPS remains the default. The existing plain-HTTP exception for literal
`127.0.0.1` and `[::1]` endpoints remains available without the opt-in.
`localhost` and `127.0.0.2` do not qualify for that exception. Other HTTP endpoints
require that provider's `allow_insecure_http = true`. Credentials, fragments and
non-HTTP schemes remain invalid even with the opt-in.

A local test bidder's `endpoint` may instead be plain HTTP to `127.0.0.1` or
`[::1]`.
Comment on lines 794 to 796

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 — The new paragraph above already covers the 127.0.0.1 / [::1] exception, so this older sentence repeats it. Suggest dropping it.

Suggested change
A local test bidder's `endpoint` may instead be plain HTTP to `127.0.0.1` or
`[::1]`.

Expand Down
Loading