@@ -76,6 +76,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
7676 } ) ;
7777
7878 let injected_tsjs = Rc :: new ( Cell :: new ( false ) ) ;
79+ let injected_permutive_shim = Rc :: new ( Cell :: new ( false ) ) ;
7980
8081 fn is_prebid_script_url ( url : & str ) -> bool {
8182 let lower = url. to_ascii_lowercase ( ) ;
@@ -114,15 +115,23 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
114115 let patterns = patterns. clone( ) ;
115116 let rewrite_prebid = config. enable_prebid;
116117 let rewrite_permutive = config. enable_permutive;
118+ let injected_permutive_shim = injected_permutive_shim. clone( ) ;
117119 move |el| {
118120 if let Some ( href) = el. get_attribute( "href" ) {
119121 // If Prebid auto-config is enabled and this looks like a Prebid script href, rewrite to our extension
120122 if rewrite_prebid && is_prebid_script_url( & href) {
121123 let ext_src = tsjs:: ext_script_src( ) ;
122124 el. set_attribute( "href" , & ext_src) ?;
123125 } else if rewrite_permutive && is_permutive_sdk_url( & href) {
124- let permutive_src = tsjs:: permutive_script_src( ) ;
125- el. set_attribute( "href" , & permutive_src) ?;
126+ // Rewrite SDK URL to load through our proxy
127+ let sdk_src = tsjs:: permutive_sdk_script_src( ) ;
128+ el. set_attribute( "href" , & sdk_src) ?;
129+ // Also inject our shim script after the SDK
130+ if !injected_permutive_shim. get( ) {
131+ let shim_tag = tsjs:: permutive_shim_script_tag( ) ;
132+ el. after( & shim_tag, ContentType :: Html ) ;
133+ injected_permutive_shim. set( true ) ;
134+ }
126135 } else {
127136 let new_href = href
128137 . replace( & patterns. https_origin( ) , & patterns. replacement_url( ) )
@@ -140,14 +149,22 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
140149 let patterns = patterns. clone( ) ;
141150 let rewrite_prebid = config. enable_prebid;
142151 let rewrite_permutive = config. enable_permutive;
152+ let injected_permutive_shim = injected_permutive_shim. clone( ) ;
143153 move |el| {
144154 if let Some ( src) = el. get_attribute( "src" ) {
145155 if rewrite_prebid && is_prebid_script_url( & src) {
146156 let ext_src = tsjs:: ext_script_src( ) ;
147157 el. set_attribute( "src" , & ext_src) ?;
148158 } else if rewrite_permutive && is_permutive_sdk_url( & src) {
149- let permutive_src = tsjs:: permutive_script_src( ) ;
150- el. set_attribute( "src" , & permutive_src) ?;
159+ // Rewrite SDK URL to load through our proxy
160+ let sdk_src = tsjs:: permutive_sdk_script_src( ) ;
161+ el. set_attribute( "src" , & sdk_src) ?;
162+ // Also inject our shim script after the SDK
163+ if !injected_permutive_shim. get( ) {
164+ let shim_tag = tsjs:: permutive_shim_script_tag( ) ;
165+ el. after( & shim_tag, ContentType :: Html ) ;
166+ injected_permutive_shim. set( true ) ;
167+ }
151168 } else {
152169 let new_src = src
153170 . replace( & patterns. https_origin( ) , & patterns. replacement_url( ) )
@@ -344,7 +361,7 @@ mod tests {
344361 </head><body></body></html>"# ;
345362
346363 let mut config = create_test_config ( ) ;
347- config. enable_permutive = true ; // enable rewriting of Permutive URLs
364+ config. enable_permutive = true ; // enable Permutive SDK rewriting and shim injection
348365 let processor = create_html_processor ( config) ;
349366 let pipeline_config = PipelineConfig {
350367 input_compression : Compression :: None ,
@@ -358,9 +375,15 @@ mod tests {
358375 assert ! ( result. is_ok( ) ) ;
359376 let processed = String :: from_utf8_lossy ( & output) ;
360377 assert ! ( processed. contains( "/static/tsjs=tsjs-core.min.js" ) ) ;
361- // Permutive references are rewritten to our permutive bundle when auto-configure is on
362- assert ! ( processed. contains( "/static/tsjs=tsjs-permutive.min .js" ) ) ;
378+ // Permutive SDK URL should be rewritten to first-party proxy
379+ assert ! ( processed. contains( "/static/tsjs=tsjs-permutive-sdk .js" ) ) ;
363380 assert ! ( !processed. contains( "edge.permutive.app" ) ) ;
381+ // Shim script should be injected after the SDK
382+ assert ! ( processed. contains( "/static/tsjs=tsjs-permutive-shim.min.js" ) ) ;
383+ // Verify shim comes after SDK
384+ let sdk_pos = processed. find ( "tsjs-permutive-sdk.js" ) . unwrap ( ) ;
385+ let shim_pos = processed. find ( "tsjs-permutive-shim.min.js" ) . unwrap ( ) ;
386+ assert ! ( shim_pos > sdk_pos, "Shim should be injected after SDK" ) ;
364387 }
365388
366389 #[ test]
@@ -383,8 +406,11 @@ mod tests {
383406 let result = pipeline. process ( Cursor :: new ( html. as_bytes ( ) ) , & mut output) ;
384407 assert ! ( result. is_ok( ) ) ;
385408 let processed = String :: from_utf8_lossy ( & output) ;
386- assert ! ( processed. contains( "/static/tsjs=tsjs-permutive.min.js" ) ) ;
409+ // Permutive SDK URL should be rewritten to first-party proxy
410+ assert ! ( processed. contains( "/static/tsjs=tsjs-permutive-sdk.js" ) ) ;
387411 assert ! ( !processed. contains( "cdn.permutive.com" ) ) ;
412+ // Shim should be injected
413+ assert ! ( processed. contains( "/static/tsjs=tsjs-permutive-shim.min.js" ) ) ;
388414 }
389415
390416 #[ test]
@@ -412,6 +438,96 @@ mod tests {
412438 assert ! ( !processed. contains( "tsjs-permutive" ) ) ;
413439 }
414440
441+ #[ test]
442+ fn test_permutive_sdk_injection_rewrites_and_adds_shim ( ) {
443+ let html = r#"<html>
444+ <head>
445+ <script src="https://workspace-id.edge.permutive.app/org-id-web.js"></script>
446+ </head>
447+ <body></body>
448+ </html>"# ;
449+
450+ let mut config = create_test_config ( ) ;
451+ config. enable_permutive = true ;
452+ let processor = create_html_processor ( config) ;
453+ let pipeline_config = PipelineConfig {
454+ input_compression : Compression :: None ,
455+ output_compression : Compression :: None ,
456+ chunk_size : 8192 ,
457+ } ;
458+ let mut pipeline = StreamingPipeline :: new ( pipeline_config, processor) ;
459+
460+ let mut output = Vec :: new ( ) ;
461+ let result = pipeline. process ( Cursor :: new ( html. as_bytes ( ) ) , & mut output) ;
462+ assert ! ( result. is_ok( ) ) ;
463+ let processed = String :: from_utf8_lossy ( & output) ;
464+
465+ // SDK URL should be rewritten to first-party proxy
466+ assert ! (
467+ processed. contains( "/static/tsjs=tsjs-permutive-sdk.js" ) ,
468+ "SDK URL should be rewritten to first-party"
469+ ) ;
470+ assert ! (
471+ !processed. contains( "edge.permutive.app" ) ,
472+ "Original SDK domain should not be present"
473+ ) ;
474+
475+ // Should add our shim script after it
476+ assert ! (
477+ processed. contains( "/static/tsjs=tsjs-permutive-shim.min.js" ) ,
478+ "Shim script should be injected"
479+ ) ;
480+
481+ // Shim should come after SDK
482+ let sdk_pos = processed. find ( "tsjs-permutive-sdk.js" ) . unwrap ( ) ;
483+ let shim_pos = processed. find ( "tsjs-permutive-shim.min.js" ) . unwrap ( ) ;
484+ assert ! (
485+ shim_pos > sdk_pos,
486+ "Shim should be injected after SDK script"
487+ ) ;
488+ }
489+
490+ #[ test]
491+ fn test_permutive_multiple_sdks_only_injects_once ( ) {
492+ let html = r#"<html>
493+ <head>
494+ <script src="https://workspace1.edge.permutive.app/org1-web.js"></script>
495+ <link rel="preload" as="script" href="https://workspace2.edge.permutive.app/org2-web.js" />
496+ <script src="https://cdn.permutive.com/workspace3-web.js"></script>
497+ </head>
498+ <body></body>
499+ </html>"# ;
500+
501+ let mut config = create_test_config ( ) ;
502+ config. enable_permutive = true ;
503+ let processor = create_html_processor ( config) ;
504+ let pipeline_config = PipelineConfig {
505+ input_compression : Compression :: None ,
506+ output_compression : Compression :: None ,
507+ chunk_size : 8192 ,
508+ } ;
509+ let mut pipeline = StreamingPipeline :: new ( pipeline_config, processor) ;
510+
511+ let mut output = Vec :: new ( ) ;
512+ let result = pipeline. process ( Cursor :: new ( html. as_bytes ( ) ) , & mut output) ;
513+ assert ! ( result. is_ok( ) ) ;
514+ let processed = String :: from_utf8_lossy ( & output) ;
515+
516+ // All SDK URLs should be rewritten to first-party
517+ assert ! ( processed. contains( "/static/tsjs=tsjs-permutive-sdk.js" ) ) ;
518+ assert ! ( !processed. contains( "workspace1.edge.permutive.app" ) ) ;
519+ assert ! ( !processed. contains( "workspace2.edge.permutive.app" ) ) ;
520+ assert ! ( !processed. contains( "cdn.permutive.com" ) ) ;
521+
522+ // Shim should only be injected once
523+ let shim_count = processed. matches ( "tsjs-permutive-shim.min.js" ) . count ( ) ;
524+ assert_eq ! (
525+ shim_count, 1 ,
526+ "Shim script should only be injected once, found {} times" ,
527+ shim_count
528+ ) ;
529+ }
530+
415531 #[ test]
416532 fn test_create_html_processor_url_replacement ( ) {
417533 let config = create_test_config ( ) ;
0 commit comments