Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** With `[integrations.prebid]` enabled, JS asset entries with
Comment thread
dhruv8sh marked this conversation as resolved.
Resolved
`proxy = "enabled"` whose `origin_url` matches Prebid `script_patterns`
now fail deploy validation and runtime startup. Before upgrading the
server, use the new `ts` CLI to run `ts config validate`, change overlapping
assets to `proxy = "disabled"` (preserves Prebid interception) or
`proxy = "blocked"`, and publish the corrected config with `ts config push`.
JS Asset Proxy now rewrites script tags before every other integration, and
its `enabled` replacement is final: no later integration rewrites or removes
the proxied tag.
- The attested Fastly wasm reports `x-ts-version` as the full 40-character source commit; deploy-pipeline builds keep the tag, branch or short-commit value described below.
- **Breaking:** `ts --version` now prints `ts package=<version> source=<source> target=<triple> variant=default` instead of `ts <version>`. `source` is the full commit in attested release binaries, and otherwise the development ref, short commit or `unknown`. Update scripts that parse the old output.
- **Breaking:** Creative rewrite proxy, click, CSS, `srcset`, sign-response, rebuild-response, and injected creative TSJS URLs are now absolute on `publisher.public_origin`. Review the [default authority, cookie scope, and coordinated upgrade/rollback warning for `public_origin`](docs/guide/configuration.md#publisher-configuration) before deployment. Existing root-relative signed proxy and click inputs remain accepted. TSJS bundle selection and hash calculation are unchanged.
Expand Down
80 changes: 68 additions & 12 deletions crates/trusted-server-core/src/auction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,21 @@ mod plan_sharing_tests {
use crate::integrations::IntegrationRegistry;
use crate::test_support::tests::create_test_settings;

fn aps_provider_map() -> std::collections::BTreeMap<ProviderId, ProviderConfig> {
std::collections::BTreeMap::from([(
"aps-main".parse().expect("should parse APS provider ID"),
ProviderConfig {
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
timeout_ms: None,
routing: RoutingMode::AllEligible,
notifications: NotificationConfig::default(),
profile_config: serde_json::json!({"account_id":"example-account"}),
},
)])
}

#[test]
fn orchestrator_and_registry_share_the_compiled_plan_allocation() {
let settings = create_test_settings();
Expand Down Expand Up @@ -212,22 +227,63 @@ mod plan_sharing_tests {
#[test]
fn aps_profile_registers_renderer_without_browser_aps_config() {
let mut settings = create_test_settings();
settings.auction.providers = std::collections::BTreeMap::from([(
"aps-main".parse().expect("should parse APS provider ID"),
ProviderConfig {
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
timeout_ms: None,
routing: RoutingMode::AllEligible,
notifications: NotificationConfig::default(),
profile_config: serde_json::json!({"account_id":"example-account"}),
},
)]);
settings.auction.providers = aps_provider_map();
let plan = Arc::new(compile_auction_plan(&settings).expect("should compile APS plan"));
let registry = IntegrationRegistry::with_plan(&settings, plan)
.expect("should build APS renderer registry");

assert!(registry.has_route(&http::Method::GET, "/integrations/aps/renderer"));
}

#[test]
fn js_asset_proxy_rewriter_runs_before_plan_registered_rewriters() {
// `create_test_settings` enables Prebid; add an APS profile, GPT and
// `js_asset_proxy` so plan registrations precede the builder table.
let mut settings = create_test_settings();
settings.auction.providers = aps_provider_map();
settings
.integrations
.insert_config("gpt", &serde_json::json!({ "enabled": true }))
.expect("should insert GPT config");
settings
.integrations
.insert_config(
"js_asset_proxy",
&serde_json::json!({
"enabled": true,
"assets": [{
"path": "/assets/vendor.js",
"origin_url": "https://cdn.example.com/vendor.js"
}]
}),
)
.expect("should insert JS asset proxy config");
let plan = Arc::new(compile_auction_plan(&settings).expect("should compile APS plan"));

let registry =
IntegrationRegistry::with_plan(&settings, plan).expect("should build registry");

assert!(
registry.integration_enabled("aps") && registry.integration_enabled("prebid"),
"should register APS and Prebid from the plan"
);
let ids = registry.attribute_rewriter_ids();
assert_eq!(
ids.first(),
Some(&"js_asset_proxy"),
"js_asset_proxy should be the first attribute rewriter: {ids:?}"
);
let prebid_index = ids
.iter()
.position(|id| *id == "prebid")
.expect("should register Prebid attribute rewriter");
let gpt_index = ids
.iter()
.position(|id| *id == "gpt")
.expect("should register GPT attribute rewriter");
assert!(
prebid_index < gpt_index,
"other rewriters should keep registration order: {ids:?}"
);
}
}
167 changes: 165 additions & 2 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::integrations::{
google_tag_manager::GoogleTagManagerConfig,
gpt::GptConfig,
gpt_diagnostics::{GPT_DIAGNOSTICS_INTEGRATION_ID, GptDiagnosticsConfig},
js_asset_proxy::{JS_ASSET_PROXY_INTEGRATION_ID, JsAssetProxyConfig},
js_asset_proxy::{JS_ASSET_PROXY_INTEGRATION_ID, JsAssetProxyConfig, JsAssetProxyMode},
lockr::LockrConfig,
nextjs::NextJsIntegrationConfig,
osano::OsanoConfig,
Expand Down Expand Up @@ -385,7 +385,46 @@ fn validate_js_asset_proxy_config(settings: &Settings) -> Result<(), Report<Trus
"integration startup failed for `{JS_ASSET_PROXY_INTEGRATION_ID}`: {error}"
),
})
})
})?;
validate_js_asset_proxy_prebid_overlap(settings, &config)
}

/// Rejects `enabled` JS assets that Prebid's script interception would remove.
///
/// Prebid removes publisher Prebid.js tags to keep a single Prebid instance next
/// to the managed bundle. JS Asset Proxy decides first and its replacement is
/// terminal, so an `enabled` asset for such a URL would load a second Prebid.js
/// copy. Every conflicting asset is reported in one error.
fn validate_js_asset_proxy_prebid_overlap(
settings: &Settings,
config: &JsAssetProxyConfig,
) -> Result<(), Report<TrustedServerError>> {
if !config.enabled {
return Ok(());
}
let Some(prebid_config) =
settings.integration_config::<prebid::PrebidIntegrationConfig>("prebid")?
else {
return Ok(());
};
let conflicts: Vec<String> = config
.assets
.iter()
.filter(|asset| {
asset.proxy == JsAssetProxyMode::Enabled
&& prebid_config.matches_script_url(&asset.origin_url)
})
.map(|asset| format!("`{}` (origin_url `{}`)", asset.path, asset.origin_url))
.collect();
if conflicts.is_empty() {
return Ok(());
}
Err(Report::new(TrustedServerError::Configuration {
message: format!(
"integration startup failed for `{JS_ASSET_PROXY_INTEGRATION_ID}`: assets {} have proxy = \"enabled\" in [[integrations.js_asset_proxy.assets]], but [integrations.prebid] script_patterns intercept those URLs to prevent duplicate Prebid instances; set proxy = \"disabled\" to keep Prebid's interception, set proxy = \"blocked\" to remove the script, or remove the matching pattern from [integrations.prebid] script_patterns (not recommended; may duplicate the managed bundle)",
conflicts.join(", ")
),
}))
}

fn validate_enabled_integrations(
Expand Down Expand Up @@ -1881,6 +1920,130 @@ password = "production-admin-password-32-bytes"
);
}

fn settings_with_prebid_script_asset(mode: &str) -> Settings {
// `valid_settings` enables `[integrations.prebid]` with the default
// `script_patterns`, which match `/prebid.min.js`.
let mut settings = valid_settings();
settings
.integrations
.insert_config(
JS_ASSET_PROXY_INTEGRATION_ID,
&serde_json::json!({
"enabled": true,
"assets": [{
"path": "/assets/vendor-pb.js",
"origin_url": "https://cdn.example.com/prebid.min.js",
"proxy": mode
}]
}),
)
.expect("should insert JS asset proxy config");
settings
}

#[test]
fn validation_rejects_enabled_js_asset_intercepted_by_prebid() {
let settings = settings_with_prebid_script_asset("enabled");

for result in [
validate_settings_for_deploy(&settings),
validate_settings_for_runtime(&settings),
] {
let message = result
.expect_err("should reject enabled asset intercepted by Prebid")
.to_string();
assert!(
message.contains("[[integrations.js_asset_proxy.assets]]")
&& message.contains("[integrations.prebid]"),
"error should name both tables: {message}"
);
assert!(
message.contains("/assets/vendor-pb.js"),
"error should name the conflicting asset: {message}"
);
}
}

#[test]
fn validation_accepts_disabled_and_blocked_js_assets_intercepted_by_prebid() {
for mode in ["disabled", "blocked"] {
let settings = settings_with_prebid_script_asset(mode);

validate_settings_for_deploy(&settings)
.expect("should accept non-enabled asset intercepted by Prebid");
}
}

#[test]
fn validation_accepts_enabled_prebid_script_asset_when_prebid_disabled() {
let mut settings = settings_with_prebid_script_asset("enabled");
settings
.integrations
.insert_config("prebid", &serde_json::json!({ "enabled": false }))
.expect("should insert disabled prebid config");

validate_settings_for_deploy(&settings)
.expect("should accept enabled asset when Prebid interception is off");
}
Comment thread
dhruv8sh marked this conversation as resolved.
Resolved

#[test]
fn validation_accepts_enabled_prebid_script_asset_when_js_asset_proxy_disabled() {
let mut settings = valid_settings();
settings
.integrations
.insert_config(
JS_ASSET_PROXY_INTEGRATION_ID,
&serde_json::json!({
"enabled": false,
"assets": [{
"path": "/assets/vendor-pb.js",
"origin_url": "https://cdn.example.com/prebid.min.js",
"proxy": "enabled"
}]
}),
)
.expect("should insert disabled JS asset proxy config");

validate_settings_for_deploy(&settings)
.expect("should accept a staged enabled asset while JS asset proxy is disabled");
}

#[test]
fn validation_reports_every_enabled_js_asset_intercepted_by_prebid() {
let mut settings = valid_settings();
settings
.integrations
.insert_config(
JS_ASSET_PROXY_INTEGRATION_ID,
&serde_json::json!({
"enabled": true,
"assets": [
{
"path": "/assets/vendor-pb.js",
"origin_url": "https://cdn.example.com/prebid.js",
"proxy": "enabled"
},
{
"path": "/assets/vendor-pb-min.js",
"origin_url": "https://cdn.example.com/prebid.min.js",
"proxy": "enabled"
}
]
}),
)
.expect("should insert JS asset proxy config");

let message = validate_settings_for_deploy(&settings)
.expect_err("should reject enabled assets intercepted by Prebid")
.to_string();

assert!(
message.contains("/assets/vendor-pb.js")
&& message.contains("/assets/vendor-pb-min.js"),
"error should name every conflicting asset: {message}"
);
}

#[test]
fn deploy_validation_requires_external_bundle_url_for_enabled_prebid() {
let mut settings = valid_settings();
Expand Down
Loading
Loading