@@ -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,19 @@ 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) => {
293+ self . handle_script_handler ( )
294+ }
295+ Method :: GET if path == ROUTE_FIRST_PARTY_AD => {
296+ self . handle_first_party_ad ( settings, req) . await
297+ }
298+ Method :: POST if path == ROUTE_THIRD_PARTY_AD => {
299+ self . handle_third_party_ad ( settings, req) . await
300+ }
301+ _ => Err ( Report :: new ( Self :: error ( format ! (
274302 "Unsupported Prebid route: {path}"
275- ) ) ) )
303+ ) ) ) ) ,
276304 }
277305 }
278306}
@@ -691,6 +719,7 @@ mod tests {
691719 bidders : vec ! [ "exampleBidder" . to_string( ) ] ,
692720 auto_configure : true ,
693721 debug : false ,
722+ script_handler : None ,
694723 }
695724 }
696725
@@ -957,4 +986,132 @@ mod tests {
957986 ) ) ;
958987 assert ! ( !is_prebid_script_url( "https://cdn.com/app.js" ) ) ;
959988 }
989+
990+ #[ test]
991+ fn test_script_handler_config_parsing ( ) {
992+ let toml_str = r#"
993+ [publisher]
994+ domain = "test-publisher.com"
995+ cookie_domain = ".test-publisher.com"
996+ origin_url = "https://origin.test-publisher.com"
997+ proxy_secret = "test-secret"
998+
999+ [synthetic]
1000+ counter_store = "test-counter-store"
1001+ opid_store = "test-opid-store"
1002+ secret_key = "test-secret-key"
1003+ template = "{{client_ip}}:{{user_agent}}"
1004+
1005+ [integrations.prebid]
1006+ enabled = true
1007+ server_url = "https://prebid.example"
1008+ script_handler = "/prebid.js"
1009+ "# ;
1010+
1011+ let settings = Settings :: from_toml ( toml_str) . expect ( "should parse TOML" ) ;
1012+ let config = settings
1013+ . integration_config :: < PrebidIntegrationConfig > ( "prebid" )
1014+ . expect ( "should get config" )
1015+ . expect ( "should be enabled" ) ;
1016+
1017+ assert_eq ! ( config. script_handler, Some ( "/prebid.js" . to_string( ) ) ) ;
1018+ }
1019+
1020+ #[ test]
1021+ fn test_script_handler_none_by_default ( ) {
1022+ let toml_str = r#"
1023+ [publisher]
1024+ domain = "test-publisher.com"
1025+ cookie_domain = ".test-publisher.com"
1026+ origin_url = "https://origin.test-publisher.com"
1027+ proxy_secret = "test-secret"
1028+
1029+ [synthetic]
1030+ counter_store = "test-counter-store"
1031+ opid_store = "test-opid-store"
1032+ secret_key = "test-secret-key"
1033+ template = "{{client_ip}}:{{user_agent}}"
1034+
1035+ [integrations.prebid]
1036+ enabled = true
1037+ server_url = "https://prebid.example"
1038+ "# ;
1039+
1040+ let settings = Settings :: from_toml ( toml_str) . expect ( "should parse TOML" ) ;
1041+ let config = settings
1042+ . integration_config :: < PrebidIntegrationConfig > ( "prebid" )
1043+ . expect ( "should get config" )
1044+ . expect ( "should be enabled" ) ;
1045+
1046+ assert_eq ! ( config. script_handler, None ) ;
1047+ }
1048+
1049+ #[ test]
1050+ fn test_script_handler_returns_empty_js ( ) {
1051+ let config = PrebidIntegrationConfig {
1052+ enabled : true ,
1053+ server_url : "https://prebid.example" . to_string ( ) ,
1054+ timeout_ms : 1000 ,
1055+ bidders : vec ! [ ] ,
1056+ auto_configure : false ,
1057+ debug : false ,
1058+ script_handler : Some ( "/prebid.js" . to_string ( ) ) ,
1059+ } ;
1060+ let integration = PrebidIntegration :: new ( config) ;
1061+
1062+ let response = integration
1063+ . handle_script_handler ( )
1064+ . expect ( "should return response" ) ;
1065+
1066+ assert_eq ! ( response. get_status( ) , StatusCode :: OK ) ;
1067+
1068+ let content_type = response
1069+ . get_header_str ( header:: CONTENT_TYPE )
1070+ . expect ( "should have content-type" ) ;
1071+ assert_eq ! ( content_type, "application/javascript; charset=utf-8" ) ;
1072+
1073+ let cache_control = response
1074+ . get_header_str ( header:: CACHE_CONTROL )
1075+ . expect ( "should have cache-control" ) ;
1076+ assert ! ( cache_control. contains( "max-age=31536000" ) ) ;
1077+ assert ! ( cache_control. contains( "immutable" ) ) ;
1078+
1079+ let body = response. into_body_str ( ) ;
1080+ assert ! ( body. contains( "// Script overridden by Trusted Server" ) ) ;
1081+ }
1082+
1083+ #[ test]
1084+ fn test_routes_includes_script_handler ( ) {
1085+ let config = PrebidIntegrationConfig {
1086+ enabled : true ,
1087+ server_url : "https://prebid.example" . to_string ( ) ,
1088+ timeout_ms : 1000 ,
1089+ bidders : vec ! [ ] ,
1090+ auto_configure : false ,
1091+ debug : false ,
1092+ script_handler : Some ( "/prebid.js" . to_string ( ) ) ,
1093+ } ;
1094+ let integration = PrebidIntegration :: new ( config) ;
1095+
1096+ let routes = integration. routes ( ) ;
1097+
1098+ // Should have 3 routes: first-party ad, third-party ad, and script handler
1099+ assert_eq ! ( routes. len( ) , 3 ) ;
1100+
1101+ let has_script_route = routes
1102+ . iter ( )
1103+ . any ( |r| r. path == "/prebid.js" && r. method == Method :: GET ) ;
1104+ assert ! ( has_script_route, "should register script handler route" ) ;
1105+ }
1106+
1107+ #[ test]
1108+ fn test_routes_without_script_handler ( ) {
1109+ let config = base_config ( ) ; // Has script_handler: None
1110+ let integration = PrebidIntegration :: new ( config) ;
1111+
1112+ let routes = integration. routes ( ) ;
1113+
1114+ // Should only have 2 routes: first-party ad and third-party ad
1115+ assert_eq ! ( routes. len( ) , 2 ) ;
1116+ }
9601117}
0 commit comments