You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
js_asset_proxy is supposed to see every <script src> before any native integration's attribute rewriter. The builder table says so (crates/trusted-server-core/src/integrations/mod.rs:293):
// This must remain first: attribute rewriters chain replacements and short-circuit removals.
That is no longer true at runtime. IntegrationRegistry::with_plan registers Prebid and APS from the auction plan before it walks the builder table (integrations/registry.rs:815-831), and attribute rewriters run in registration order. Prebid's rewriter therefore runs first. APS registers no attribute rewriter, so Prebid is the only native rewriter ahead of js_asset_proxy today. With Prebid, js_asset_proxy and GPT enabled, the effective order is ["prebid", "js_asset_proxy", "gpt"].
As a result, an operator's [[integrations.js_asset_proxy.assets]] entry with proxy = "enabled" for a publisher Prebid.js URL never takes effect. This applies to any URL whose path matches Prebid's script_patterns, such as one ending in /prebid.min.js. Prebid removes the element before js_asset_proxy can rewrite it. The asset's first-party route is still registered, and no validation error, startup error or log line says the entry is dead. blocked and disabled entries give the same result in either order.
How it happened:
Add JavaScript asset proxy integration #742 (commit 22fbbf2, merged 2026-09-04) put js_asset_proxy at the top of the builder table, ahead of aps and prebid, and IntegrationRegistry::new built only from that table. During review, a maintainer pointed out that "rewrite_attribute chains Replace results and short-circuits on RemoveElement, so js_asset_proxy's precedence over native rewriters (GPT etc.) exists only because it is first in this list". That thread produced the comment above. The precedence tests already existed.
Add configuration-driven OpenRTB auction providers #1016 (commit dbc1012, merged 2026-09-08) removed aps and prebid from the table and added with_plan, which pushes their registrations first. Its review comments do not mention js_asset_proxy or rewriter order.
The precedence tests from Add JavaScript asset proxy integration #742 (js_asset_proxy.rs:845-947) use GPT URLs only. The shared test settings enable Prebid (test_support.rs:24-29), but no test uses a URL that both Prebid and js_asset_proxy match, so CI did not catch the change.
Why this is a bug, and which precedence is right. Prebid removes publisher Prebid.js on purpose. Trusted Server always injects its managed /integrations/prebid/bundle.js, and the interception "prevents duplicate Prebid instances" (docs/guide/integrations/prebid.md:415). For this one URL, today's outcome is the safe one: the publisher script is removed and the managed bundle stays. The ordering is still wrong, for four reasons:
An asset entry names one exact URL, while Prebid's patterns are suffix defaults. An explicit operator decision should not be discarded silently by a default.
The CLI offers this exact choice. For a third-party script whose host or path contains prebid, ts audit generate drafts a proxy = "disabled" entry with # Native integration may be preferable: [integrations.prebid] under # Generated by `ts audit`; review before enabling. Switching that entry to enabled silently does nothing.
Precedence that depends on registration position can move again. If a native rewriter that replaces URLs lands ahead of js_asset_proxy the same way, blocked entries stop working (see the probe below).
So the right precedence is js_asset_proxy first for every attribute rewriter, enforced in code instead of by list position. Prebid's removal protects the single-instance invariant, so an enabled asset that Prebid would remove is contradictory configuration: honoring it loads the publisher's Prebid.js next to the managed bundle (verified below). Validation should reject that combination instead of either order resolving it silently.
Steps to reproduce
Add these tests to mod tests in crates/trusted-server-core/src/integrations/js_asset_proxy.rs. The module already provides create_test_settings, json!, IntegrationRegistry and process_html_with_registry, and create_test_settings enables [integrations.prebid] with the default script_patterns.
fnprocess_prebid_script_with_asset_mode(mode:&str) -> String{// `create_test_settings` enables `[integrations.prebid]` with the default// `script_patterns`, so its attribute rewriter matches `/prebid.min.js`.letmut settings = create_test_settings();
settings
.integrations.insert_config(JS_ASSET_PROXY_INTEGRATION_ID,&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");let registry = IntegrationRegistry::new(&settings).expect("should build registry");let html = r#"<html><body><script src="https://cdn.example.com/prebid.min.js"></script></body></html>"#;process_html_with_registry(html, registry)}#[test]fnjs_asset_proxy_rewriter_takes_precedence_over_prebid_script_removal(){let processed = process_prebid_script_with_asset_mode("enabled");assert!(
processed.contains(r#"<script src="/assets/vendor-pb.js"></script>"#),"JS asset proxy should rewrite before Prebid removes the script: {processed}");}#[test]fnblocked_js_asset_proxy_entry_removes_prebid_script(){let processed = process_prebid_script_with_asset_mode("blocked");assert!(
!processed.contains("prebid.min.js") && !processed.contains("<script"),"blocked JS asset should remove the script element: {processed}");}#[test]fndisabled_js_asset_proxy_entry_lets_prebid_remove_script(){let processed = process_prebid_script_with_asset_mode("disabled");assert!(
!processed.contains("prebid.min.js") && !processed.contains("<script"),"disabled JS asset should defer to Prebid script interception: {processed}");}
test integrations::js_asset_proxy::tests::blocked_js_asset_proxy_entry_removes_prebid_script ... ok
test integrations::js_asset_proxy::tests::disabled_js_asset_proxy_entry_lets_prebid_remove_script ... ok
test integrations::js_asset_proxy::tests::js_asset_proxy_rewriter_takes_precedence_over_prebid_script_removal ... FAILED
JS asset proxy should rewrite before Prebid removes the script: <html><body></body></html>
(The filter also runs two existing Prebid tests, which pass.)
I also ran a probe in a scratch copy of a4e01eb. The copy has a test-only hook that can re-sort registrations to show what each order does. For the same enabled asset, rewrite_attribute("src", "https://cdn.example.com/prebid.min.js", ...) returned:
Setup
Attribute rewriter order
Result
Today
["prebid", "js_asset_proxy"]
RemoveElement
js_asset_proxy re-sorted first
["js_asset_proxy", "prebid"]
Replaced("/assets/vendor-pb.js")
Today, with Prebid script_patterns = []
["prebid", "js_asset_proxy"]
Replaced("/assets/vendor-pb.js")
Through publisher::stream_publisher_body, today's page keeps only the managed <script src="/integrations/prebid/bundle.js" defer></script> in <head>. With js_asset_proxy first, the page has that tag and <script src="/assets/vendor-pb.js"></script>, which means two Prebid.js loads.
Hypothetical blocked bypass. With a blocked asset for https://securepubads.g.doubleclick.net/tag/js/gpt.js and GPT enabled, today's order ["prebid", "js_asset_proxy", "gpt"] gives RemoveElement. With ["prebid", "gpt", "js_asset_proxy"] it gives Replaced("/integrations/gpt/script").
Expected behavior
js_asset_proxy's attribute rewriter runs before every other attribute rewriter, whatever order integrations register in.
For a URL that Prebid's script_patterns match, blocked removes the element and disabled lets Prebid remove it.
While [integrations.prebid] is enabled, config validation rejects an enabled asset for such a URL. The error names both tables.
Actual behavior
Prebid's attribute rewriter runs before js_asset_proxy.
An enabled asset for a publisher Prebid.js URL is removed from the page instead of being rewritten to its first-party path.
Its route is registered but unused, and nothing reports the conflict.
Root cause
with_plan pushes the plan registrations before the builder loop (integrations/registry.rs:815-831):
Attribute rewriters are appended in that order (registry.rs:886-888). rewrite_attribute threads replacements through them and stops at the first removal (registry.rs:1045-1058):
Prebid's register_for_plan includes its attribute rewriter (prebid.rs:1372-1379). That rewriter returns remove_element() when matches_script_url matches (prebid.rs:1459-1470). matches_script_url drops the query and fragment and checks the URL path against script_patterns (prebid.rs:1024-1051). The defaults are /prebid.js, /prebid.min.js, /prebidjs.js and /prebidjs.min.js (prebid.rs:757-762). The js_asset_proxy branch JsAssetProxyMode::Enabled => AttributeRewriteAction::replace(asset.path.clone()) (js_asset_proxy.rs:560) never runs for these URLs.
The only thing that enforces the rule at mod.rs:293 is the entry's position inside builders(). #1016 added registrations in front of that list.
Impact
Affected deployments: those with [integrations.prebid] enabled and a js_asset_proxy asset set to proxy = "enabled" whose origin_url path matches Prebid's script_patterns. ts audit generate leads operators here. For a third-party HTTPS script whose host or path contains prebid (crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs:204-205), it writes # Detected integration: prebid, # Native integration may be preferable: [integrations.prebid] and proxy = "disabled" (audit/generate/mod.rs:574-593 in the same directory).
What operators see: the publisher's Prebid.js tag disappears, and the page runs the managed bundle instead. The js_asset_proxy entry has no effect, and no warning appears. The page keeps working, so the severity today is low.
Latent risk: protection for blocked entries now depends on registration position. If a native rewriter that replaces URLs ends up ahead of js_asset_proxy, a blocked script is served from the publisher's origin through that integration's proxy instead of being removed (see the GPT probe above). Only Prebid, which removes, is ahead today, so blocked still holds. The open ordering spec in Define integrations crate split and ordered configuration #1194 would make hook order operator-configurable, which raises this risk.
Proposed fix
Enforce the precedence in with_plan. Do it after all registrations are applied, so it does not depend on push order:
// js_asset_proxy is the operator's per-URL override, so it runs before every// native attribute rewriter. The sort is stable for the others.
inner.html_rewriters.sort_by_key(|rewriter| {
rewriter.integration_id()
!= crate::integrations::js_asset_proxy::JS_ASSET_PROXY_INTEGRATION_ID});
Update the comment at mod.rs:293 to point to this line. Registering js_asset_proxy before prebid::register_for_plan would also work today, but a later change could undo it the same way Add configuration-driven OpenRTB auction providers #1016 did.
Reject the contradictory combination. While [integrations.prebid] is enabled, reject an enabled asset whose origin_url Prebid's interception would remove. The error should name both tables and the ways out:
proxy = "disabled" keeps today's page behavior, with Prebid intercepting the script.
proxy = "blocked".
Removing the pattern from script_patterns, which the Prebid guide calls "not recommended; may duplicate the managed bundle".
Put the check next to validate_js_asset_proxy_config (config.rs:280-299), which runs in both validate_settings_for_deploy (config.rs:247-260) and validate_settings_for_runtime (config.rs:268-278). It needs a crate-visible helper that applies Prebid's matches_script_url logic to a PrebidIntegrationConfig.
Add tests. Add the three tests above, plus a test that js_asset_proxy is the first attribute rewriter when Prebid and APS plan registrations are present.
I prototyped step 1 in a scratch copy of a4e01eb. With the sort, js_asset_proxy_rewriter_takes_precedence_over_prebid_script_removal passes, the blocked and disabled tests still pass, and every existing test in the native cargo test -p trusted-server-core --lib run passes, including the GPT precedence tests.
Compatibility: step 1 changes runtime behavior only for the contradictory configuration, where the publisher's Prebid.js would be proxied and load next to the managed bundle. Step 2 turns that case into a validation error. Because the check also runs at runtime, an already-deployed config with such an entry would stop starting until the entry is set to disabled or blocked. Say this in the release notes and ask operators to run ts config validate before upgrading. The check could instead run only at deploy time, with a warning at registry build, to avoid failing startup. In that case, an existing config that is never re-validated would start loading both Prebid.js copies.
Alternative, if maintainers want Prebid's interception to win on purpose: keep today's order but make it explicit. Document the exception at mod.rs:293 and in the js_asset_proxy docs, and add a test that pins Prebid ahead of js_asset_proxy. The step 2 validation is still worth adding, because an enabled entry that can never apply is a configuration error in either design. This keeps runtime behavior unchanged, but precedence stays tied to registration position.
Done when
js_asset_proxy's attribute rewriter runs first whenever it is registered, independent of registration order. A test asserts this with Prebid and APS plan registrations present.
The three Prebid-overlap tests above pass: enabled is rewritten to its first-party path, blocked is removed, and disabled is removed by Prebid.
Config validation rejects an enabled asset matched by the enabled Prebid integration's script_patterns. Tests cover the error and the accepted disabled and blocked variants.
The comment at integrations/mod.rs:293 states where the precedence is enforced.
Description
js_asset_proxyis supposed to see every<script src>before any native integration's attribute rewriter. The builder table says so (crates/trusted-server-core/src/integrations/mod.rs:293):// This must remain first: attribute rewriters chain replacements and short-circuit removals.That is no longer true at runtime.
IntegrationRegistry::with_planregisters Prebid and APS from the auction plan before it walks the builder table (integrations/registry.rs:815-831), and attribute rewriters run in registration order. Prebid's rewriter therefore runs first. APS registers no attribute rewriter, so Prebid is the only native rewriter ahead ofjs_asset_proxytoday. With Prebid,js_asset_proxyand GPT enabled, the effective order is["prebid", "js_asset_proxy", "gpt"].As a result, an operator's
[[integrations.js_asset_proxy.assets]]entry withproxy = "enabled"for a publisher Prebid.js URL never takes effect. This applies to any URL whose path matches Prebid'sscript_patterns, such as one ending in/prebid.min.js. Prebid removes the element beforejs_asset_proxycan rewrite it. The asset's first-party route is still registered, and no validation error, startup error or log line says the entry is dead.blockedanddisabledentries give the same result in either order.How it happened:
js_asset_proxyat the top of the builder table, ahead ofapsandprebid, andIntegrationRegistry::newbuilt only from that table. During review, a maintainer pointed out that "rewrite_attributechainsReplaceresults and short-circuits onRemoveElement, so js_asset_proxy's precedence over native rewriters (GPT etc.) exists only because it is first in this list". That thread produced the comment above. The precedence tests already existed.apsandprebidfrom the table and addedwith_plan, which pushes their registrations first. Its review comments do not mentionjs_asset_proxyor rewriter order.js_asset_proxy.rs:845-947) use GPT URLs only. The shared test settings enable Prebid (test_support.rs:24-29), but no test uses a URL that both Prebid andjs_asset_proxymatch, so CI did not catch the change.Why this is a bug, and which precedence is right. Prebid removes publisher Prebid.js on purpose. Trusted Server always injects its managed
/integrations/prebid/bundle.js, and the interception "prevents duplicate Prebid instances" (docs/guide/integrations/prebid.md:415). For this one URL, today's outcome is the safe one: the publisher script is removed and the managed bundle stays. The ordering is still wrong, for four reasons:mod.rs:293and in the Add JavaScript asset proxy integration #742 review is that the operator's per-URL decision comes first. Add configuration-driven OpenRTB auction providers #1016 changed that without discussion, only because of where registrations are pushed.prebid,ts audit generatedrafts aproxy = "disabled"entry with# Native integration may be preferable: [integrations.prebid]under# Generated by `ts audit`; review before enabling.Switching that entry toenabledsilently does nothing.js_asset_proxythe same way,blockedentries stop working (see the probe below).So the right precedence is
js_asset_proxyfirst for every attribute rewriter, enforced in code instead of by list position. Prebid's removal protects the single-instance invariant, so anenabledasset that Prebid would remove is contradictory configuration: honoring it loads the publisher's Prebid.js next to the managed bundle (verified below). Validation should reject that combination instead of either order resolving it silently.Steps to reproduce
Add these tests to
mod testsincrates/trusted-server-core/src/integrations/js_asset_proxy.rs. The module already providescreate_test_settings,json!,IntegrationRegistryandprocess_html_with_registry, andcreate_test_settingsenables[integrations.prebid]with the defaultscript_patterns.Run them on the host target:
Observed at a4e01eb:
(The filter also runs two existing Prebid tests, which pass.)
I also ran a probe in a scratch copy of a4e01eb. The copy has a test-only hook that can re-sort registrations to show what each order does. For the same
enabledasset,rewrite_attribute("src", "https://cdn.example.com/prebid.min.js", ...)returned:["prebid", "js_asset_proxy"]RemoveElementjs_asset_proxyre-sorted first["js_asset_proxy", "prebid"]Replaced("/assets/vendor-pb.js")script_patterns = []["prebid", "js_asset_proxy"]Replaced("/assets/vendor-pb.js")Through
publisher::stream_publisher_body, today's page keeps only the managed<script src="/integrations/prebid/bundle.js" defer></script>in<head>. Withjs_asset_proxyfirst, the page has that tag and<script src="/assets/vendor-pb.js"></script>, which means two Prebid.js loads.Hypothetical
blockedbypass. With ablockedasset forhttps://securepubads.g.doubleclick.net/tag/js/gpt.jsand GPT enabled, today's order["prebid", "js_asset_proxy", "gpt"]givesRemoveElement. With["prebid", "gpt", "js_asset_proxy"]it givesReplaced("/integrations/gpt/script").Expected behavior
js_asset_proxy's attribute rewriter runs before every other attribute rewriter, whatever order integrations register in.script_patternsmatch,blockedremoves the element anddisabledlets Prebid remove it.[integrations.prebid]is enabled, config validation rejects anenabledasset for such a URL. The error names both tables.Actual behavior
js_asset_proxy.enabledasset for a publisher Prebid.js URL is removed from the page instead of being rewritten to its first-party path.Root cause
with_planpushes the plan registrations before the builder loop (integrations/registry.rs:815-831):Attribute rewriters are appended in that order (
registry.rs:886-888).rewrite_attributethreads replacements through them and stops at the first removal (registry.rs:1045-1058):Prebid's
register_for_planincludes its attribute rewriter (prebid.rs:1372-1379). That rewriter returnsremove_element()whenmatches_script_urlmatches (prebid.rs:1459-1470).matches_script_urldrops the query and fragment and checks the URL path againstscript_patterns(prebid.rs:1024-1051). The defaults are/prebid.js,/prebid.min.js,/prebidjs.jsand/prebidjs.min.js(prebid.rs:757-762). Thejs_asset_proxybranchJsAssetProxyMode::Enabled => AttributeRewriteAction::replace(asset.path.clone())(js_asset_proxy.rs:560) never runs for these URLs.The only thing that enforces the rule at
mod.rs:293is the entry's position insidebuilders(). #1016 added registrations in front of that list.Impact
[integrations.prebid]enabled and ajs_asset_proxyasset set toproxy = "enabled"whoseorigin_urlpath matches Prebid'sscript_patterns.ts audit generateleads operators here. For a third-party HTTPS script whose host or path containsprebid(crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs:204-205), it writes# Detected integration: prebid,# Native integration may be preferable: [integrations.prebid]andproxy = "disabled"(audit/generate/mod.rs:574-593in the same directory).js_asset_proxyentry has no effect, and no warning appears. The page keeps working, so the severity today is low.blockedentries now depends on registration position. If a native rewriter that replaces URLs ends up ahead ofjs_asset_proxy, ablockedscript is served from the publisher's origin through that integration's proxy instead of being removed (see the GPT probe above). Only Prebid, which removes, is ahead today, soblockedstill holds. The open ordering spec in Define integrations crate split and ordered configuration #1194 would make hook order operator-configurable, which raises this risk.Proposed fix
Enforce the precedence in
with_plan. Do it after all registrations are applied, so it does not depend on push order:Update the comment at
mod.rs:293to point to this line. Registeringjs_asset_proxybeforeprebid::register_for_planwould also work today, but a later change could undo it the same way Add configuration-driven OpenRTB auction providers #1016 did.Reject the contradictory combination. While
[integrations.prebid]is enabled, reject anenabledasset whoseorigin_urlPrebid's interception would remove. The error should name both tables and the ways out:proxy = "disabled"keeps today's page behavior, with Prebid intercepting the script.proxy = "blocked".script_patterns, which the Prebid guide calls "not recommended; may duplicate the managed bundle".Put the check next to
validate_js_asset_proxy_config(config.rs:280-299), which runs in bothvalidate_settings_for_deploy(config.rs:247-260) andvalidate_settings_for_runtime(config.rs:268-278). It needs a crate-visible helper that applies Prebid'smatches_script_urllogic to aPrebidIntegrationConfig.Add tests. Add the three tests above, plus a test that
js_asset_proxyis the first attribute rewriter when Prebid and APS plan registrations are present.I prototyped step 1 in a scratch copy of a4e01eb. With the sort,
js_asset_proxy_rewriter_takes_precedence_over_prebid_script_removalpasses, theblockedanddisabledtests still pass, and every existing test in the nativecargo test -p trusted-server-core --librun passes, including the GPT precedence tests.Compatibility: step 1 changes runtime behavior only for the contradictory configuration, where the publisher's Prebid.js would be proxied and load next to the managed bundle. Step 2 turns that case into a validation error. Because the check also runs at runtime, an already-deployed config with such an entry would stop starting until the entry is set to
disabledorblocked. Say this in the release notes and ask operators to runts config validatebefore upgrading. The check could instead run only at deploy time, with a warning at registry build, to avoid failing startup. In that case, an existing config that is never re-validated would start loading both Prebid.js copies.Alternative, if maintainers want Prebid's interception to win on purpose: keep today's order but make it explicit. Document the exception at
mod.rs:293and in thejs_asset_proxydocs, and add a test that pins Prebid ahead ofjs_asset_proxy. The step 2 validation is still worth adding, because anenabledentry that can never apply is a configuration error in either design. This keeps runtime behavior unchanged, but precedence stays tied to registration position.Done when
js_asset_proxy's attribute rewriter runs first whenever it is registered, independent of registration order. A test asserts this with Prebid and APS plan registrations present.enabledis rewritten to its first-party path,blockedis removed, anddisabledis removed by Prebid.enabledasset matched by the enabled Prebid integration'sscript_patterns. Tests cover the error and the accepteddisabledandblockedvariants.integrations/mod.rs:293states where the precedence is enforced.Affected area
Integrations (prebid, lockr, permutive, etc.)
Version
main at a4e01eb
Related
js_asset_proxyfirst in the builder table, the "must remain first" comment and the GPT-only precedence tests.js_asset_proxyis not first today and that a URL-replacing rewriter placed ahead of it bypassesblockedentries.