Rolling main documentation
Built from commit 3d54bbd5905df26165f29b8bcbc056ded8ad7718. Content may describe unreleased behavior.
Creative Processing
Trusted Server rewrites ad creative HTML and CSS to route resources through first-party domains.
Overview
Creative processing has separate auction-response and proxied-response paths. When rewriting is enabled for a path, it rewrites URLs in third-party ad creatives so resources are fetched through the publisher's first-party domain. This provides:
- First-party delivery: eligible, non-excluded external resource URLs route through the publisher's domain
- First-party context: cookies and storage use the publisher's domain
- EC ID integration: configurable ID forwarding to downstream endpoints
- Tamper resistance: validated, signed URLs prevent in-flight URL substitution
- Configurable data sharing: forwarding to vendors is per the deployer's configuration
How It Works
┌──────────────────────────────────────────────────────┐
│ Original Creative HTML │
│ <img src="https://tracker.com/pixel.gif"> │
│ <iframe src="https://cdn.com/ad.html"> │
│ <style> .bg { background: url(cdn.com/bg.jpg); } │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Trusted Server Processing (rewrite-enabled) │
│ 1. Sanitize POST /auction adm when opted in │
│ 2. Parse HTML with streaming processor │
│ 3. Detect absolute/protocol-relative URLs │
│ 4. Generate signed proxy URLs │
│ 5. Rewrite in-place and inject TSJS │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Rewritten Creative HTML │
│ <img src="/first-party/proxy?tsurl=...&tstoken=sig">│
│ <iframe src="/first-party/proxy?tsurl=...&token=sig">│
│ <style> .bg { background: url(/first-party/proxy...) }│
└──────────────────────────────────────────────────────┘Processing Triggers
The creative rewriters are invoked by independent delivery paths:
- Auction
adm: Winning-bid HTML returned byPOST /auctionis optionally sanitized ([auction].sanitize_creatives, opt-in, defaultfalse) and rewritten ([auction].rewrite_creatives, defaulttrue). Click-through links are wrapped according to[auction].rewrite_clicks, which followsrewrite_creativeswhen unset. - First-party proxy: Non-streaming
text/htmlandtext/cssresponses fetched through/first-party/proxyare rewritten independently of the auction setting. An explicitly set[auction].rewrite_clicksalso applies to links in proxied HTML. - Integration processing: Publisher HTML integrations use their own registration and configuration controls.
Streaming Mode
When with_streaming() is enabled in ProxyRequestConfig, proxied HTML/CSS processing is skipped to preserve origin compression and reduce latency. This does not change the POST /auction sanitizer or rewrite setting.
Auction Rewrite Control
Three auction settings control the processing applied to winning-bid adm returned by POST /auction and delivered through the publisher SSAT/page-bids path. Sanitization is opt-in (default false) and asset rewriting is enabled by default. Click wrapping is controlled by rewrite_clicks, which follows rewrite_creatives when unset; see Assets and clicks.
[auction]
sanitize_creatives = false
rewrite_creatives = true
# rewrite_clicks unset: follows rewrite_creativesRegardless of mode, a creative larger than the 1 MiB per-creative cap is rejected and its adm is dropped.
sanitize_creatives | rewrite_creatives | Auction winning-bid adm behavior |
|---|---|---|
false (default) | false | Asset URLs stay direct. With rewrite_clicks unset or false, deliver the creative exactly as the bidder returned it (subject to the size cap); with rewrite_clicks = true, wrap links as described in Assets and clicks. |
true | false | Strip executable markup (script/object/embed/form, event handlers) with its inner content, then deliver without asset rewriting. Sanitizer-accepted external resource and inline CSS URLs remain direct; links follow rewrite_clicks and stay direct when it is unset or false. |
false | true (default) | Rewrite eligible resource/CSS URLs in the raw bidder markup to signed first-party endpoints, dropping any bidder <base> URL. Links are wrapped unless rewrite_clicks = false. Executable markup is preserved. |
true | true | Sanitize first, then rewrite. POST /auction emits root-relative endpoints and injects creative TSJS exactly once, whether or not the bidder supplied a <body>; SSAT/page-bids emits absolute endpoints for its foreign-origin renderer and does not inject the bundle. |
Sanitization blanks script-based creatives
Sanitization removes script/object/embed/form and similar elements together with their inner content, which destroys script-based creatives — the majority of programmatic display. Enable sanitize_creatives when creatives render in a context that shares the publisher's origin; leave it disabled when creatives render in a foreign-origin frame (for example the Prebid Universal Creative inside the ad server's iframe), where the markup cannot reach the publisher origin. Sanitizer acceptance is not a host allowlist; ordinary accepted HTTP(S) URLs may cause the browser to contact external creative hosts directly.
Assets and clicks
rewrite_creatives governs asset URLs. rewrite_clicks governs click-through links and follows rewrite_creatives when unset:
[auction]
rewrite_creatives = true
# Unset: follows rewrite_creatives. Set false to keep landing links direct.
# rewrite_clicks = falseAssets (rewrite_creatives) | Clicks (rewrite_clicks) | Asset URLs | Links | <base> | TSJS on POST /auction |
|---|---|---|---|---|---|
true | true or unset | /first-party/proxy | /first-party/click + data-tsclick | href dropped | injected |
true | false | /first-party/proxy | direct | href dropped | injected |
false | true | direct | /first-party/click + data-tsclick | href dropped | injected |
false | false or unset | direct | direct | kept | not injected |
SSAT/page-bids follows the same table with absolute URLs and never injects TSJS. The <base> column assumes sanitize_creatives = false; the sanitizer removes <base> entirely before the rewrite pass. exclude_domains applies to both assets and links.
The client-side tsCreativeConfig.clickGuard flag is separate. rewrite_clicks decides whether the server emits signed click URLs; clickGuard decides whether the creative runtime re-signs them after creative script changes their query parameters. With rewrite_clicks off and rewrite_creatives on, the injected guard installs its listeners but never acts, because no link carries data-tsclick.
Both switches also decide where the visitor's EC ID goes. /first-party/click appends the request's ts-ec value to the landing URL and logs the click with it, and /first-party/proxy appends it to proxied asset requests. With rewrite_clicks = false, creative links stop forwarding it on every path; with rewrite_clicks = true and rewrite_creatives = false, landing pages receive it while assets load direct. Neither switch is a consent control: EC issuance is consent-gated, and both endpoints forward whatever EC ID the request carries.
With rewrite_creatives = false and rewrite_clicks = true, POST /auction still injects TSJS, but this does not reopen asset proxying in practice: /auction creatives render in a sandbox without allow-same-origin, so the render guard skips /first-party/sign and script-inserted assets load direct. /first-party/sign itself is not gated by rewrite_creatives.
Whenever either switch is on, a bidder <base> loses its href and keeps target; a <base> without target is removed. With rewrite_creatives = false and rewrite_clicks = true, relative asset URLs therefore no longer resolve against the bidder base, so bidders should use absolute asset URLs. With sanitize_creatives = true the sanitizer removes <base> entirely first.
On POST /auction and in HTML fetched through /first-party/proxy, the injected click guard navigates a data-tsclick link using only the link's own target, not a kept <base target>. A creative that relies on <base target="_blank"> can therefore open wrapped links in the frame and unwrapped links in a new tab; give links an explicit target to avoid this.
Runtime protections inside the sandboxed creative iframe
Creatives rendered by Trusted Server's own path run in a sandboxed iframe without allow-same-origin, i.e. an opaque origin. The injected creative runtime's click guard recovers mutated clicks there via a GET /first-party/proxy-rebuild navigation (302 chain).
Two capabilities are unavailable in that context. CORS-mode subresources — ES modules, crossorigin fonts, fetch/XHR — cannot load through /first-party/proxy, because that endpoint deliberately sends no Access-Control-Allow-Origin: it is a generic signed fetcher that forwards the EC ID and client-derived headers, so letting an opaque creative frame read its responses would turn it into a readable bidder-controlled proxy. Ordinary subresources (<img>, <script src>, stylesheets, media) are unaffected. A separately constrained asset capability is tracked in #982.
The second is dynamic resource signing, which rewrites URLs on elements a creative inserts at runtime. It is installed only when renderGuard is enabled in tsCreativeConfig, and that is false by default — deployments using the default configuration are unaffected. Where it is enabled, runtime-inserted <img>/<iframe> URLs cannot be signed from an opaque origin (the signing request is blocked by CORS), so they load directly from third parties; the sandbox still isolates them from the publisher origin, but they are not first-party proxied. A same-origin parent postMessage broker restoring dynamic signing is tracked in #982. URLs rewritten server-side are unaffected.
PBS Cache coordinates and processing
On the publisher SSAT/page-bids path, hb_cache_host/hb_cache_path let the client fetch and render the cached bid, whose adm is the bidder's original markup and therefore bypasses every server-side processing policy. They are emitted only for bids that supplied no creative of their own, where the cache is the sole render source. A bid that supplied a creative — whether processing accepted it, or rejected it as empty, unparseable, or over the size limit — ships without them, so a processed or refused creative can never be re-fetched raw.
rewrite_creatives and sanitize_creatives do not affect HTML/CSS rewriting under /first-party/proxy; an explicit rewrite_clicks does apply to links there. The publisher SSAT/page-bids path is a production creative path and follows all three settings. [debug].inject_adm_for_testing controls only the additional debug_bid diagnostic blob and testing-only direct GAM replacement; it does not control whether the processed adm is present.
Rewritten Elements
Images
Elements: <img>, <input type="image">
Attributes:
src- Primary image sourcedata-src- Lazy-loading sourcesrcset- Responsive image sourcesimagesrcset- Image set (used in<link>)
Example:
<!-- Original -->
<img
src="https://cdn.example.com/banner.jpg"
srcset="
https://cdn.example.com/banner@1x.jpg 1x,
https://cdn.example.com/banner@2x.jpg 2x
"
/>
<!-- Rewritten -->
<img
src="/first-party/proxy?tsurl=https://cdn.example.com/banner.jpg&tstoken=sig1"
srcset="
/first-party/proxy?tsurl=https://cdn.example.com/banner@1x.jpg&tstoken=sig2 1x,
/first-party/proxy?tsurl=https://cdn.example.com/banner@2x.jpg&tstoken=sig3 2x
"
/>Scripts
Elements: <script>
Attributes:
src- Script source URL
Example:
<!-- Original -->
<script src="https://cdn.example.com/tracker.js"></script>
<!-- Rewritten -->
<script src="/first-party/proxy?tsurl=https://cdn.example.com/tracker.js&tstoken=sig"></script>Media Elements
Elements: <video>, <audio>, <source>
Attributes:
src- Media source URL
Example:
<!-- Original -->
<video>
<source src="https://cdn.example.com/video.mp4" type="video/mp4" />
</video>
<!-- Rewritten -->
<video>
<source
src="/first-party/proxy?tsurl=https://cdn.example.com/video.mp4&tstoken=sig"
type="video/mp4"
/>
</video>Embedded Objects
Elements: <object>, <embed>
Attributes:
data- Object data source (<object>)src- Embed source (<embed>)
Example:
<!-- Original -->
<object data="https://example.com/flash.swf"></object>
<embed src="https://example.com/media.swf" />
<!-- Rewritten -->
<object
data="/first-party/proxy?tsurl=https://example.com/flash.swf&tstoken=sig"
></object>
<embed
src="/first-party/proxy?tsurl=https://example.com/media.swf&tstoken=sig"
/>Iframes
Elements: <iframe>
Attributes:
src- Frame source URL
Example:
<!-- Original -->
<iframe src="https://advertiser.com/creative.html"></iframe>
<!-- Rewritten -->
<iframe
src="/first-party/proxy?tsurl=https://advertiser.com/creative.html&tstoken=sig"
></iframe>Nested Iframes
If the iframe content itself contains HTML, it will be processed recursively. Each level of nesting gets its own URL rewriting pass.
Links
Elements: <link>
Attributes:
href- Link target (stylesheets, preload, prefetch)imagesrcset- Responsive images in link preload
Conditions: Only rewritten when rel attribute matches:
stylesheetpreloadprefetch
Example:
<!-- Original -->
<link rel="stylesheet" href="https://cdn.example.com/styles.css" />
<link rel="preload" href="https://cdn.example.com/font.woff2" as="font" />
<!-- Rewritten -->
<link
rel="stylesheet"
href="/first-party/proxy?tsurl=https://cdn.example.com/styles.css&tstoken=sig"
/>
<link
rel="preload"
href="/first-party/proxy?tsurl=https://cdn.example.com/font.woff2&tstoken=sig"
as="font"
/>Anchors (Click Tracking)
Elements: <a>, <area>
Attributes:
href- Link destination
Rewrite Mode: Uses /first-party/click for direct redirects
Controlled by: [auction].rewrite_clicks (follows rewrite_creatives when unset; HTML fetched through /first-party/proxy keeps wrapping links when unset)
Example:
<!-- Original -->
<a href="https://advertiser.example.com/product?id=123">Buy Now</a>
<!-- Rewritten -->
<a
href="/first-party/click?tsurl=https://advertiser.example.com/product&id=123&tstoken=sig"
data-tsclick="/first-party/click?tsurl=https://advertiser.example.com/product&id=123&tstoken=sig"
>Buy Now</a
>Click vs Proxy
Anchors (<a>) use /first-party/click for 302 redirects, avoiding content downloads. Other elements use /first-party/proxy to fetch and potentially rewrite content.
SVG Elements
Elements: SVG <image>, <use>
Attributes:
href- SVG 2.0 syntaxxlink:href- SVG 1.1 legacy syntax
Example:
<!-- Original -->
<svg>
<image href="https://cdn.example.com/icon.svg" />
<use xlink:href="https://cdn.example.com/sprite.svg#icon" />
</svg>
<!-- Rewritten -->
<svg>
<image
href="/first-party/proxy?tsurl=https://cdn.example.com/icon.svg&tstoken=sig"
/>
<use
xlink:href="/first-party/proxy?tsurl=https://cdn.example.com/sprite.svg&tstoken=sig#icon"
/>
</svg>Inline Styles
Attributes: style attribute on any element
Patterns: Rewrites url(...) values in CSS
Example:
<!-- Original -->
<div style="background: url(https://cdn.example.com/bg.png) no-repeat;">
<!-- Rewritten -->
<div
style="background: url(/first-party/proxy?tsurl=https://cdn.example.com/bg.png&tstoken=sig) no-repeat;"
></div>
</div>Style Blocks
Elements: <style>
Patterns: Rewrites all url(...) occurrences in CSS
Example:
<!-- Original -->
<style>
.header {
background-image: url(https://cdn.example.com/header.jpg);
}
.logo {
background: url('https://cdn.example.com/logo.png');
}
</style>
<!-- Rewritten -->
<style>
.header {
background-image: url(/first-party/proxy?tsurl=https://cdn.example.com/header.jpg&tstoken=sig);
}
.logo {
background: url('/first-party/proxy?tsurl=https://cdn.example.com/logo.png&tstoken=sig');
}
</style>URL Detection Rules
Absolute URLs
Pattern: Starts with http:// or https://
Rewritten: ✅ Yes
Examples:
✅ https://cdn.example.com/image.png
✅ http://tracker.example.com/pixel.gif
❌ /relative/path.jpg (relative)
❌ ../images/logo.png (relative)Protocol-Relative URLs
Pattern: Starts with //
Rewritten: ✅ Yes (normalized to https://)
Examples:
Original: //cdn.example.com/script.js
Normalized: https://cdn.example.com/script.js
Rewritten: /first-party/proxy?tsurl=https://cdn.example.com/script.js&tstoken=sigRelative URLs
Patterns:
- Starts with
/(absolute path) - Starts with
./or../(relative path) - No scheme prefix (relative)
Rewritten: ❌ No
Examples:
❌ /assets/image.png
❌ ./local.jpg
❌ ../parent/file.css
❌ image.pngWhy Skip Relative URLs?
Relative URLs already point to your domain (publisher origin). Rewriting them would create unnecessary proxy loops and break functionality.
Non-Network Schemes
Skipped Schemes:
data:- Data URIs (inline content)javascript:- JavaScript executionmailto:- Email linkstel:- Phone numbersblob:- Blob objectsabout:- Browser internal pages
Rewritten: ❌ No
Examples:
❌ data:image/png;base64,iVBORw0KGgo...
❌ javascript:void(0)
❌ mailto:contact@example.com
❌ tel:+1234567890
❌ blob:https://example.com/uuid
❌ about:blankSrcset Processing
Srcset Syntax
Srcset attributes contain comma-separated candidates with optional descriptors:
Format: url descriptor, url descriptor, ...
Descriptors:
1x,2x,3x- Pixel density100w,200w- Width in pixels
Parsing Rules
Robust Comma Handling:
- Splits on commas with or without spaces
- Preserves
data:URIs (doesn't split on internal commas) - Handles irregular spacing
Example:
<!-- Various spacing patterns -->
srcset="url1.jpg 1x, url2.jpg 2x"
<!-- standard -->
srcset="url1.jpg 1x,url2.jpg 2x"
<!-- no space after comma -->
srcset="url1.jpg 1x , url2.jpg 2x"
<!-- extra spaces -->Descriptor Preservation
Descriptors are preserved exactly as written:
<!-- Original -->
<img
srcset="
https://cdn.com/small.jpg 480w,
https://cdn.com/medium.jpg 800w,
https://cdn.com/large.jpg 1200w
"
/>
<!-- Rewritten -->
<img
srcset="
/first-party/proxy?tsurl=https://cdn.com/small.jpg&tstoken=sig1 480w,
/first-party/proxy?tsurl=https://cdn.com/medium.jpg&tstoken=sig2 800w,
/first-party/proxy?tsurl=https://cdn.com/large.jpg&tstoken=sig3 1200w
"
/>Mixed URL Types
Srcset can mix absolute and relative URLs:
<!-- Original -->
<img srcset="/local/small.jpg 1x, https://cdn.com/large.jpg 2x" />
<!-- Rewritten (only absolute URL) -->
<img
srcset="
/local/small.jpg 1x,
/first-party/proxy?tsurl=https://cdn.com/large.jpg&tstoken=sig 2x
"
/>CSS URL Rewriting
url() Syntax Variations
CSS url() values support multiple quote styles:
/* No quotes */
url(https://example.com/image.png)
/* Single quotes */
url('https://example.com/image.png')
/* Double quotes */
url("https://example.com/image.png")
/* With spaces */
url( "https://example.com/image.png" )All are rewritten correctly, preserving the original quote style.
CSS Properties
Common properties with url() values:
/* Background images */
background: url(https://cdn.com/bg.jpg);
background-image: url(https://cdn.com/pattern.png);
/* Borders */
border-image: url(https://cdn.com/border.svg);
/* List styles */
list-style-image: url(https://cdn.com/bullet.png);
/* Cursors */
cursor: url(https://cdn.com/cursor.cur), pointer;
/* Masks */
mask-image: url(https://cdn.com/mask.svg);
/* Filters */
filter: url(https://cdn.com/filter.svg#blur);All url() occurrences are rewritten regardless of property.
Multiple url() Values
Properties can have multiple url() values:
/* Original */
.element {
background:
url(https://cdn.com/top.png) top,
url(https://cdn.com/bottom.png) bottom;
}
/* Rewritten */
.element {
background:
url(/first-party/proxy?tsurl=https://cdn.com/top.png&tstoken=sig1) top,
url(/first-party/proxy?tsurl=https://cdn.com/bottom.png&tstoken=sig2) bottom;
}@import Rules
CSS @import with URLs:
/* Original */
@import url(https://fonts.googleapis.com/css?family=Roboto);
/* Rewritten */
@import url(/first-party/proxy?tsurl=https://fonts.googleapis.com/css?family=Roboto&tstoken=sig);Exclude Domains
Configuration
Prevent specific domains from being rewritten:
[rewrite]
exclude_domains = [
"*.cdn.trusted-partner.com", # Wildcard pattern
"first-party.example.com", # Exact match
"localhost", # Development
]Pattern Matching
Matching is case-insensitive. Entries are trimmed and lowercased at load.
Wildcard Patterns: *. matches the base domain and any subdomain
Pattern: *.cdn.example.com
Matches:
✅ assets.cdn.example.com
✅ images.cdn.example.com
✅ cdn.example.com (apex also matches)
❌ cdn.example.com.evil.com (different domain)Matching is case-insensitive.
Exact Patterns: No * requires exact host match
Pattern: api.example.com
Matches:
✅ api.example.com
❌ www.api.example.com
❌ api.example.com.evil.comUse Cases
Trusted Partners:
exclude_domains = ["*.trusted-cdn.com"]Skip rewriting for partners already providing first-party scripts.
Development:
exclude_domains = ["localhost", "127.0.0.1"]Avoid proxying local development servers.
Same-Origin Resources:
exclude_domains = ["assets.publisher.com"]Skip resources already on your domain.
Include Domains
Limit asset rewriting to hosts you list:
[rewrite]
include_domains = ["assets.example.com", "*.img.example.com"]- When
include_domainsis empty (the default), every eligible asset URL is proxied. - When it is non-empty, only asset URLs whose host matches an entry are rewritten to
/first-party/proxy. Other assets keep their original URL, load directly, and do not receive the EC ID that proxied fetches append. exclude_domainswins when a host matches both lists.- Click-through links (
<a href>,<area href>) are not affected. Whether they are wrapped in/first-party/clickdepends only onrewrite_clicks(which followsrewrite_creativeson the auction paths when unset) andexclude_domains. - HTML and CSS fetched through
/first-party/proxygo through the same rewrite pass and follow the same list. /first-party/signdeclines off-list hosts with a non-403error, so the creative runtime loads them directly.
Patterns use the same syntax as exclude_domains. Entries that can never equal a URL host, such as ones with a scheme, port, path or non-ASCII name, fail validation; see Pattern Matching.
Integration Hooks
Attribute Rewriters
Integrations can override attribute rewriting:
Example: Next.js integration rewrites origin URLs
impl IntegrationAttributeRewriter for NextJsIntegration {
fn rewrite(&self, attr_name: &str, attr_value: &str, ctx: &Context)
-> AttributeRewriteAction
{
if attr_name == "href" && attr_value.contains(&ctx.origin_host) {
let rewritten = attr_value.replace(&ctx.origin_host, &ctx.request_host);
return AttributeRewriteAction::replace(rewritten);
}
AttributeRewriteAction::keep()
}
}Actions:
keep()- Leave attribute unchangedreplace(value)- Change attribute valueremove_element()- Delete entire element
Script Rewriters
Integrations can modify <script> content:
Example: Next.js rewrites __NEXT_DATA__ JSON
impl IntegrationScriptRewriter for NextJsIntegration {
fn selector(&self) -> &'static str {
"script#__NEXT_DATA__"
}
fn rewrite(&self, content: &str, ctx: &Context) -> ScriptRewriteAction {
let rewritten = rewrite_next_data_urls(content, ctx);
ScriptRewriteAction::replace(rewritten)
}
}Actions:
keep()- Preserve the current working text fragmentreplace(content)- Pass replacement text to subsequent matching stagesremove_node()- Suppress this text fragment, not the<script>element
Matching script rewriters compose in registration order. Each receives the previous stage's output, including empty suppressed text, with the original final-fragment flag. Downstream stages still run after remove_node() and may flush buffered text. Only the composed result is applied to the parser fragment; script replacements are emitted as raw source, not HTML-entity-escaped text. Selectors must match <script> elements; non-script text hooks are unsupported.
Head Injectors
Integrations can inject HTML snippets at the start of <head>, immediately after the unified TSJS bundle:
Example: An integration injects configuration that runs after the TSJS API is available
impl IntegrationHeadInjector for MyIntegration {
fn integration_id(&self) -> &'static str { "my_integration" }
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
vec![format!(
r#"<script>tsjs.setConfig({{ host: "{}" }});</script>"#,
ctx.request_host
)]
}
}Behavior:
- Snippets are prepended into
<head>after the TSJS bundle tag - Called once per HTML response
- Multiple integrations can each contribute snippets
- If no snippets are returned, no extra markup is added
See Integration Guide for creating custom rewriters.
TSJS Injection
Automatic Injection
The Trusted Server JavaScript (TSJS) library is automatically injected:
Location: Start of <head> element
Tag:
<script
src="/static/tsjs=tsjs-unified.min.js?v=<hash>"
id="trustedserver-js"
></script>Timing: Injected once per HTML response before any other scripts.
Integration bundles
The integration registry selects TSJS modules by enabled integration ID. A registered ID with a compiled module is included in the immediate unified bundle unless its builder calls .with_deferred_js(); deferred modules are served separately as /static/tsjs=tsjs-<id>.min.js. Builders can call .without_js() when the Rust integration must not select a TSJS module.
The always-present creative module and all immediate integration modules are served through /static/tsjs=tsjs-unified.min.js. Trusted Server does not accept an arbitrary asset name from an integration registration.
Bundle Types
Each integration is built as a separate IIFE at compile time (into Cargo's OUT_DIR; npm run build writes the same files to crates/trusted-server-js/dist/):
tsjs-core.js— Core API (always included)tsjs-creative.js— Creative click-guard and tracking- Prebid is built externally with
build-prebid-external.mjsand served through/integrations/prebid/bundle.js tsjs-lockr.js,tsjs-permutive.js,tsjs-didomi.js,tsjs-datadome.js,tsjs-testlight.js— Other integrations
At runtime, the server concatenates tsjs-core.js + the modules for enabled integrations. The URL stays /static/tsjs=tsjs-unified.min.js?v=<hash> for backward compatibility.
Performance Optimization
Streaming Processing
HTML is processed in chunks (default 8192 bytes):
Benefits:
- Low memory footprint
- Handles large creatives
- Incremental output
- Fast first byte
Trade-offs:
- Cannot access full DOM
- Element-by-element processing
- No look-ahead
Compression
Buffered Mode (with rewriting):
Origin Response (gzipped)
↓ Decompress
Processing
↓ Rewrite URLs
Response (uncompressed)
↓ Fastly edge can re-compress
ClientStreaming Mode (no rewriting):
Origin Response (gzipped)
↓ Passthrough
Client (stays gzipped)Use streaming for binary/large files to preserve compression.
Caching
Rewritten creatives can be cached:
Cache Key Components:
- Original creative URL
- Publisher domain
- Integration configuration
Headers to Set:
Cache-Control: public, max-age=3600
Vary: Accept-EncodingDebugging
Logging
Enable debug logging for rewrite operations:
log::debug!("creative: rewriting {} -> {}", original_url, proxy_url);
log::debug!("creative: excluded domain {}", url);
log::debug!("creative: skipped non-network scheme {}", url);Testing Rewrites
Manual Testing:
- Save original creative HTML to file
- Pass through rewrite function
- Compare output
let original = "<img src=\"https://tracker.com/pixel.gif\">";
let rewritten = rewrite_creative_html(&settings, original);
assert!(rewritten.contains("/first-party/proxy"));Integration Tests:
#[test]
fn test_image_src_rewrite() {
let html = r#"<img src="https://cdn.example.com/banner.jpg">"#;
let result = rewrite_creative_html(&test_settings(), html);
assert!(result.contains("/first-party/proxy?tsurl="));
assert!(result.contains("&tstoken="));
}Common Issues
Relative URLs Not Working:
- The rewriter drops a
<base>href, so relative URLs do not resolve against it - Convert relative URLs to absolute URLs before rewriting
Data URIs Being Rewritten:
- Should be automatically skipped
- Check for malformed
data:scheme
Srcset Parsing Errors:
- Verify comma-separated format
- Check for unclosed quotes
Security Considerations
URL Validation
All rewritten URLs are validated:
- Scheme Check: Only
http://andhttps:// - Signature: HMAC-SHA256 token required
- Expiration: Optional
tsexptimestamp - Exclusion List: Configurable domain blacklist
Content Security Policy
Recommended CSP headers for rewritten creatives:
Content-Security-Policy:
default-src 'self';
img-src 'self' /first-party/proxy;
script-src 'self' /static/tsjs;
style-src 'self' 'unsafe-inline';
frame-src 'self' /first-party/proxy;Injection Prevention
Automatic Protection:
- URLs are properly encoded
- No raw user input in rewrites
- Signature prevents tampering
Manual Checks:
- Validate origin creative sources
- Sanitize user-generated content
- Monitor for suspicious patterns
Best Practices
Configuration
✅ Do:
- Use strong
proxy_secret(32+ bytes random) - Exclude trusted first-party domains
- Set appropriate cache headers
- Test rewrites before production
❌ Don't:
- Hardcode secrets in source
- Rewrite same-origin URLs unnecessarily
- Skip signature validation
- Disable TSJS injection without reason
Performance
✅ Do:
- Use streaming for large/binary responses
- Enable compression at edge
- Cache rewritten creatives
- Monitor rewrite latency
❌ Don't:
- Buffer entire response unnecessarily
- Rewrite on every request (cache!)
- Process non-HTML/CSS with rewriter
- Chain multiple rewrites
Monitoring
Track these metrics:
- Rewrite operations - Count of rewrites per request
- Excluded domains - Frequency of exclusions
- Processing time - Latency added by rewriting
- Cache hit rate - Effectiveness of caching
- TSJS injection - Verify library loads
Next Steps
- Learn about First-Party Proxy for URL handling
- Review Integration Guide for custom rewriters
- Set up Configuration for your creatives
- Explore Edge Cookies for identity management