Skip to content
1 change: 1 addition & 0 deletions crates/trusted-server-core/benches/html_processor_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn make_config() -> HtmlProcessorConfig {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down
42 changes: 41 additions & 1 deletion crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ fn validate_enabled_integrations(
validate_integration::<SourcepointConfig>(settings, "sourcepoint")?;
validate_integration::<OsanoConfig>(settings, "osano")?;
validate_integration::<GoogleTagManagerConfig>(settings, "google_tag_manager")?;
validate_integration::<DataDomeConfig>(settings, "datadome")?;
if let Some(config) = settings.integration_config::<DataDomeConfig>("datadome")? {
crate::integrations::datadome::DataDomeIntegration::validate_config_for_startup(config)?;
}
validate_integration::<GptConfig>(settings, "gpt")?;
validate_integration::<GptDiagnosticsConfig>(settings, "gpt_diagnostics")?;

Expand Down Expand Up @@ -404,6 +406,44 @@ password = "production-admin-password-32-bytes"
);
}

#[test]
fn deploy_validation_rejects_invalid_datadome_test_bypass() {
for (enable_protection, store, name, expected_message) in [
(
false,
"ts_secrets",
"datadome_test_bypass",
"requires enable_protection",
),
(true, "", "datadome_test_bypass", "credential_secret_store"),
(true, "ts_secrets", "", "credential_secret_name"),
] {
let mut settings = valid_settings();
settings
.integrations
.insert_config(
"datadome",
&serde_json::json!({
"enabled": true,
"enable_protection": enable_protection,
"protection_test_bypass": {
"enabled": true,
"credential_secret_store": store,
"credential_secret_name": name,
},
}),
)
.expect("should insert DataDome config");

let err = validate_settings_for_deploy(&settings)
.expect_err("should reject invalid DataDome test bypass");
assert!(
format!("{err:?}").contains(expected_message),
"error should mention the invalid bypass setting: {err:?}"
);
}
}

#[test]
fn validate_trait_reports_deploy_errors() {
let mut settings = valid_settings();
Expand Down
77 changes: 77 additions & 0 deletions crates/trusted-server-core/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use lol_html::{
text,
};

use crate::integrations::datadome::{DATADOME_INTEGRATION_ID, DataDomeClientTagSuppressed};
use crate::integrations::gpt_diagnostics::GptDiagnosticsRequestDecision;
use crate::integrations::{
AttributeRewriteOutcome, IntegrationAttributeContext, IntegrationDocumentState,
Expand Down Expand Up @@ -175,6 +176,8 @@ pub struct HtmlProcessorConfig {
pub max_buffered_body_bytes: usize,
/// Request-scoped conditional diagnostics delivery decision.
pub gpt_diagnostics: Option<GptDiagnosticsRequestDecision>,
/// Whether to omit Trusted Server's automatic `DataDome` client-side tag.
pub suppress_datadome_client_side_tag: bool,
}

impl HtmlProcessorConfig {
Expand All @@ -196,6 +199,7 @@ impl HtmlProcessorConfig {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: settings.publisher.max_buffered_body_bytes,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down Expand Up @@ -223,6 +227,13 @@ impl HtmlProcessorConfig {
self.gpt_diagnostics = decision;
self
}

/// Attach the request-scoped `DataDome` client-tag suppression decision.
#[must_use]
pub fn with_datadome_client_tag_suppression(mut self, suppress: bool) -> Self {
self.suppress_datadome_client_side_tag = suppress;
self
}
}

/// Create an HTML processor with URL replacement and integration hooks.
Expand All @@ -235,6 +246,9 @@ impl HtmlProcessorConfig {
pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcessor {
let post_processors = config.integrations.html_post_processors();
let document_state = IntegrationDocumentState::default();
if config.suppress_datadome_client_side_tag {
document_state.get_or_insert_with(DATADOME_INTEGRATION_ID, || DataDomeClientTagSuppressed);
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

// Simplified URL patterns structure - stores only core data and generates variants on-demand
struct UrlPatterns {
Expand Down Expand Up @@ -692,6 +706,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down Expand Up @@ -950,6 +965,62 @@ mod tests {
assert_eq!(config.request_scheme, "https");
}

#[test]
fn suppressed_datadome_tag_preserves_and_rewrites_publisher_tag() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(
"datadome",
&json!({
"enabled": true,
"client_side_key": "test-client-key",
}),
)
.expect("should configure DataDome integration");
let registry = IntegrationRegistry::new(&settings)
.expect("should create integration registry with DataDome");
let config = HtmlProcessorConfig::from_settings(
&settings,
&registry,
"origin.example.com",
"test.example.com",
"https",
)
.with_datadome_client_tag_suppression(true);
let mut processor = create_html_processor(config);

let output = processor
.process_chunk(
br#"<html><head><script id="publisher-datadome" src="https://js.datadome.co/tags.js"></script></head><body>content</body></html>"#,
true,
)
.expect("should process HTML");
let html = String::from_utf8(output).expect("should produce UTF-8 HTML");

assert!(
!html.contains("window.ddjskey"),
"should omit the DataDome client configuration"
);
assert!(
html.contains("id=\"publisher-datadome\""),
"should preserve the publisher-originated DataDome tag"
);
assert!(
html.contains("src=\"/integrations/datadome/tags.js\""),
"should rewrite the publisher-originated DataDome tag"
);
assert!(
!html.contains("https://js.datadome.co/tags.js"),
"should remove the original third-party DataDome URL"
);
assert_eq!(
html.matches("/integrations/datadome/tags.js").count(),
1,
"should leave exactly one publisher-originated DataDome tag"
);
}

#[test]
fn test_real_publisher_html() {
// Test with publisher HTML from test_publisher.html
Expand Down Expand Up @@ -1539,6 +1610,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1613,6 +1685,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1649,6 +1722,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
// Malformed HTML with two <body> elements (common in CMS template pages)
Expand Down Expand Up @@ -1684,6 +1758,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1737,6 +1812,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1764,6 +1840,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down
Loading
Loading