diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index 85abef617..e5790869c 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -52,7 +52,10 @@ use crate::integrations::IntegrationRegistry; use crate::platform::{GeoInfo, PlatformBackendSpec, PlatformHttpRequest, RuntimeServices}; use crate::price_bucket::{PriceGranularity, price_bucket}; use crate::rsc_flight::RscFlightUrlRewriter; -use crate::settings::Settings; +use crate::settings::{ + AUCTION_DEBUG_METADATA_ALLOWLIST, AuctionDebugCommentOptions, AuctionDebugCommentVerbosity, + Settings, +}; use crate::streaming_processor::{Compression, PipelineConfig, StreamProcessor, StreamingPipeline}; use crate::streaming_replacer::create_url_replacer; @@ -867,24 +870,6 @@ pub(crate) fn write_bids_to_state( /// enabled cannot bloat every page render without bound. const MAX_AUCTION_DEBUG_DUMP_BYTES: usize = 256 * 1024; -/// Provider-metadata keys safe to surface in the on-page `ts-debug` dump. -/// -/// Fail-closed allowlist: any key not listed — notably `debug`, which carries -/// the resolved `OpenRTB` request (EC ID, `user.ext.eids`, the TC consent string, -/// `device.ip`, and `device.geo`) plus per-bidder `httpcalls` — is dropped so a -/// visitor's identity graph cannot reach the client-readable DOM even when -/// `[integration.prebid].debug` is also enabled. Full debug detail remains -/// available server-side via `log::trace!`. -const DEBUG_DUMP_METADATA_ALLOWLIST: &[&str] = &[ - "error_type", - "status", - "message", - "responsetimemillis", - "errors", - "warnings", - "bidstatus", -]; - /// Per-bid creative preview length (in bytes) in the `ts-debug` dump. Mirrors /// the 512-byte upstream-body preview the prebid provider logs on an HTTP error /// (`integrations/prebid.rs`): enough to identify a creative without copying @@ -902,19 +887,44 @@ fn truncate_with_marker(value: &str, max: usize) -> String { format!("{}…(truncated {} bytes)", &value[..end], value.len() - end) } -/// Build a redacted JSON view of a single provider response for the `ts-debug` -/// dump: only [`DEBUG_DUMP_METADATA_ALLOWLIST`] metadata keys survive, and each -/// bid's creative is previewed to [`MAX_BID_CREATIVE_DUMP_BYTES`]. +/// Build a redacted JSON view of a single provider response for the +/// `ts-debug` dump. In [`AuctionDebugCommentVerbosity::Redacted`], only keys +/// in `options.metadata_keys ∩ AUCTION_DEBUG_METADATA_ALLOWLIST` survive and +/// each bid's creative is previewed to [`MAX_BID_CREATIVE_DUMP_BYTES`]. In +/// [`AuctionDebugCommentVerbosity::Full`], metadata and creatives pass +/// through unfiltered. fn redact_response_for_dump( response: &crate::auction::types::AuctionResponse, + options: &AuctionDebugCommentOptions, ) -> serde_json::Value { - let metadata: serde_json::Map = response - .metadata - .iter() - .filter(|(key, _)| DEBUG_DUMP_METADATA_ALLOWLIST.contains(&key.as_str())) - .map(|(key, value)| (key.clone(), value.clone())) - .collect(); - let bids: Vec = response.bids.iter().map(redact_bid_for_dump).collect(); + let metadata: serde_json::Map = match options.verbosity { + AuctionDebugCommentVerbosity::Redacted => response + .metadata + .iter() + .filter(|(key, _)| { + options + .metadata_keys + .iter() + .any(|configured| configured == *key) + && AUCTION_DEBUG_METADATA_ALLOWLIST.contains(&key.as_str()) + }) + .map(|(key, value)| (key.clone(), value.clone())) + .collect(), + AuctionDebugCommentVerbosity::Full => response + .metadata + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(), + }; + let bids: Vec = if options.include_bids { + response + .bids + .iter() + .map(|bid| redact_bid_for_dump(bid, options)) + .collect() + } else { + Vec::new() + }; serde_json::json!({ "provider": response.provider, "status": response.status, @@ -924,23 +934,30 @@ fn redact_response_for_dump( }) } -/// Build a redacted JSON view of a single bid: every field except `creative`, -/// which is previewed to [`MAX_BID_CREATIVE_DUMP_BYTES`]. -fn redact_bid_for_dump(bid: &crate::auction::types::Bid) -> serde_json::Value { +/// Build a redacted JSON view of a single bid. In `Redacted` verbosity, +/// `creative` is previewed to [`MAX_BID_CREATIVE_DUMP_BYTES`]; in `Full`, it +/// passes through untruncated. +fn redact_bid_for_dump( + bid: &crate::auction::types::Bid, + options: &AuctionDebugCommentOptions, +) -> serde_json::Value { let mut value = serde_json::to_value(bid).unwrap_or(serde_json::Value::Null); - if let Some(creative) = &bid.creative { + if options.verbosity == AuctionDebugCommentVerbosity::Redacted + && let Some(creative) = &bid.creative + { value["creative"] = serde_json::Value::String(truncate_with_marker(creative, MAX_BID_CREATIVE_DUMP_BYTES)); } value } -/// Prepend a `` HTML comment carrying a redacted view of -/// the auction result — pipeline stats plus, per provider, its status, bids -/// (each creative previewed to [`MAX_BID_CREATIVE_DUMP_BYTES`]), and allowlisted -/// metadata — onto the shared `ad_bids_state` so it lands directly before the -/// injected bids `