@@ -45,6 +45,8 @@ pub struct PrebidIntegrationConfig {
4545 pub auto_configure : bool ,
4646 #[ serde( default ) ]
4747 pub debug : bool ,
48+ #[ serde( default ) ]
49+ pub script_handler : Option < String > ,
4850}
4951
5052impl IntegrationConfig for PrebidIntegrationConfig {
@@ -159,6 +161,18 @@ impl PrebidIntegration {
159161 handle_prebid_auction ( settings, req, & self . config ) . await
160162 }
161163
164+ fn handle_script_handler ( & self ) -> Result < Response , Report < TrustedServerError > > {
165+ let body = "// Script overridden by Trusted Server\n " ;
166+
167+ Ok ( Response :: from_status ( StatusCode :: OK )
168+ . with_header (
169+ header:: CONTENT_TYPE ,
170+ "application/javascript; charset=utf-8" ,
171+ )
172+ . with_header ( header:: CACHE_CONTROL , "public, max-age=31536000, immutable" )
173+ . with_body ( body) )
174+ }
175+
162176 async fn handle_first_party_ad (
163177 & self ,
164178 settings : & Settings ,
@@ -251,10 +265,19 @@ pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
251265#[ async_trait( ?Send ) ]
252266impl IntegrationProxy for PrebidIntegration {
253267 fn routes ( & self ) -> Vec < IntegrationEndpoint > {
254- vec ! [
268+ let mut routes = vec ! [
255269 IntegrationEndpoint :: get( ROUTE_FIRST_PARTY_AD ) ,
256270 IntegrationEndpoint :: post( ROUTE_THIRD_PARTY_AD ) ,
257- ]
271+ ] ;
272+
273+ if let Some ( script_path) = & self . config . script_handler {
274+ // We need to leak the string to get a 'static str for IntegrationEndpoint
275+ // This is safe because the config lives for the lifetime of the application
276+ let static_path: & ' static str = Box :: leak ( script_path. clone ( ) . into_boxed_str ( ) ) ;
277+ routes. push ( IntegrationEndpoint :: get ( static_path) ) ;
278+ }
279+
280+ routes
258281 }
259282
260283 async fn handle (
@@ -265,14 +288,17 @@ impl IntegrationProxy for PrebidIntegration {
265288 let path = req. get_path ( ) . to_string ( ) ;
266289 let method = req. get_method ( ) . clone ( ) ;
267290
268- if method == Method :: GET && path == ROUTE_FIRST_PARTY_AD {
269- self . handle_first_party_ad ( settings, req) . await
270- } else if method == Method :: POST && path == ROUTE_THIRD_PARTY_AD {
271- self . handle_third_party_ad ( settings, req) . await
272- } else {
273- Err ( Report :: new ( Self :: error ( format ! (
291+ match method {
292+ Method :: GET if self . config . script_handler . as_ref ( ) == Some ( & path) => self . handle_script_handler ( ) ,
293+ Method :: GET if path == ROUTE_FIRST_PARTY_AD => {
294+ self . handle_first_party_ad ( settings, req) . await
295+ }
296+ Method :: POST if path == ROUTE_THIRD_PARTY_AD => {
297+ self . handle_third_party_ad ( settings, req) . await
298+ }
299+ _ => Err ( Report :: new ( Self :: error ( format ! (
274300 "Unsupported Prebid route: {path}"
275- ) ) ) )
301+ ) ) ) ) ,
276302 }
277303 }
278304}
@@ -691,6 +717,7 @@ mod tests {
691717 bidders : vec ! [ "exampleBidder" . to_string( ) ] ,
692718 auto_configure : true ,
693719 debug : false ,
720+ script_handler : None ,
694721 }
695722 }
696723
@@ -957,4 +984,132 @@ mod tests {
957984 ) ) ;
958985 assert ! ( !is_prebid_script_url( "https://cdn.com/app.js" ) ) ;
959986 }
987+
988+ #[ test]
989+ fn test_script_handler_config_parsing ( ) {
990+ let toml_str = r#"
991+ [publisher]
992+ domain = "test-publisher.com"
993+ cookie_domain = ".test-publisher.com"
994+ origin_url = "https://origin.test-publisher.com"
995+ proxy_secret = "test-secret"
996+
997+ [synthetic]
998+ counter_store = "test-counter-store"
999+ opid_store = "test-opid-store"
1000+ secret_key = "test-secret-key"
1001+ template = "{{client_ip}}:{{user_agent}}"
1002+
1003+ [integrations.prebid]
1004+ enabled = true
1005+ server_url = "https://prebid.example"
1006+ script_handler = "/prebid.js"
1007+ "# ;
1008+
1009+ let settings = Settings :: from_toml ( toml_str) . expect ( "should parse TOML" ) ;
1010+ let config = settings
1011+ . integration_config :: < PrebidIntegrationConfig > ( "prebid" )
1012+ . expect ( "should get config" )
1013+ . expect ( "should be enabled" ) ;
1014+
1015+ assert_eq ! ( config. script_handler, Some ( "/prebid.js" . to_string( ) ) ) ;
1016+ }
1017+
1018+ #[ test]
1019+ fn test_script_handler_none_by_default ( ) {
1020+ let toml_str = r#"
1021+ [publisher]
1022+ domain = "test-publisher.com"
1023+ cookie_domain = ".test-publisher.com"
1024+ origin_url = "https://origin.test-publisher.com"
1025+ proxy_secret = "test-secret"
1026+
1027+ [synthetic]
1028+ counter_store = "test-counter-store"
1029+ opid_store = "test-opid-store"
1030+ secret_key = "test-secret-key"
1031+ template = "{{client_ip}}:{{user_agent}}"
1032+
1033+ [integrations.prebid]
1034+ enabled = true
1035+ server_url = "https://prebid.example"
1036+ "# ;
1037+
1038+ let settings = Settings :: from_toml ( toml_str) . expect ( "should parse TOML" ) ;
1039+ let config = settings
1040+ . integration_config :: < PrebidIntegrationConfig > ( "prebid" )
1041+ . expect ( "should get config" )
1042+ . expect ( "should be enabled" ) ;
1043+
1044+ assert_eq ! ( config. script_handler, None ) ;
1045+ }
1046+
1047+ #[ test]
1048+ fn test_script_handler_returns_empty_js ( ) {
1049+ let config = PrebidIntegrationConfig {
1050+ enabled : true ,
1051+ server_url : "https://prebid.example" . to_string ( ) ,
1052+ timeout_ms : 1000 ,
1053+ bidders : vec ! [ ] ,
1054+ auto_configure : false ,
1055+ debug : false ,
1056+ script_handler : Some ( "/prebid.js" . to_string ( ) ) ,
1057+ } ;
1058+ let integration = PrebidIntegration :: new ( config) ;
1059+
1060+ let response = integration
1061+ . handle_script_handler ( )
1062+ . expect ( "should return response" ) ;
1063+
1064+ assert_eq ! ( response. get_status( ) , StatusCode :: OK ) ;
1065+
1066+ let content_type = response
1067+ . get_header_str ( header:: CONTENT_TYPE )
1068+ . expect ( "should have content-type" ) ;
1069+ assert_eq ! ( content_type, "application/javascript; charset=utf-8" ) ;
1070+
1071+ let cache_control = response
1072+ . get_header_str ( header:: CACHE_CONTROL )
1073+ . expect ( "should have cache-control" ) ;
1074+ assert ! ( cache_control. contains( "max-age=31536000" ) ) ;
1075+ assert ! ( cache_control. contains( "immutable" ) ) ;
1076+
1077+ let body = response. into_body_str ( ) ;
1078+ assert ! ( body. contains( "// Script overridden by Trusted Server" ) ) ;
1079+ }
1080+
1081+ #[ test]
1082+ fn test_routes_includes_script_handler ( ) {
1083+ let config = PrebidIntegrationConfig {
1084+ enabled : true ,
1085+ server_url : "https://prebid.example" . to_string ( ) ,
1086+ timeout_ms : 1000 ,
1087+ bidders : vec ! [ ] ,
1088+ auto_configure : false ,
1089+ debug : false ,
1090+ script_handler : Some ( "/prebid.js" . to_string ( ) ) ,
1091+ } ;
1092+ let integration = PrebidIntegration :: new ( config) ;
1093+
1094+ let routes = integration. routes ( ) ;
1095+
1096+ // Should have 3 routes: first-party ad, third-party ad, and script handler
1097+ assert_eq ! ( routes. len( ) , 3 ) ;
1098+
1099+ let has_script_route = routes
1100+ . iter ( )
1101+ . any ( |r| r. path == "/prebid.js" && r. method == Method :: GET ) ;
1102+ assert ! ( has_script_route, "should register script handler route" ) ;
1103+ }
1104+
1105+ #[ test]
1106+ fn test_routes_without_script_handler ( ) {
1107+ let config = base_config ( ) ; // Has script_handler: None
1108+ let integration = PrebidIntegration :: new ( config) ;
1109+
1110+ let routes = integration. routes ( ) ;
1111+
1112+ // Should only have 2 routes: first-party ad and third-party ad
1113+ assert_eq ! ( routes. len( ) , 2 ) ;
1114+ }
9601115}
0 commit comments