Creative Processing
Learn how 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 transforms third-party ad creatives by rewriting URLs to go through your first-party domain. This provides:
- Privacy Control - Eligible, non-excluded external resources load through your domain
- First-Party Context - Cookies and storage use your domain
- EC ID Integration - Automatic ID forwarding to trackers
- Security - Validated, signed URLs prevent tampering
- GDPR Compliance - Controlled data sharing
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 first for POST /auction adm │
│ 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 always sanitized, then rewritten by default. This pass is controlled by[auction].rewrite_creatives. - First-party proxy: Non-streaming
text/htmlandtext/cssresponses fetched through/first-party/proxyare rewritten independently of the auction setting. - 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
Use the default-true auction setting to control the rewrite pass applied to winning-bid adm returned by POST /auction and delivered through the publisher SSAT/page-bids path:
[auction]
rewrite_creatives = true| Setting | Auction winning-bid adm behavior |
|---|---|
Omitted or true | Sanitize and rewrite eligible resource/CSS and click URLs to signed first-party endpoints. POST /auction emits root-relative endpoints and injects creative TSJS; SSAT/page-bids emits absolute endpoints for its foreign-origin renderer and does not inject the bundle. |
false | Sanitize, then deliver the sanitized HTML without rewriting in either path. Sanitizer-accepted external resource, click, and inline CSS URLs remain direct. |
Sanitization remains mandatory
Setting rewrite_creatives = false returns sanitized but not rewritten HTML, not byte-for-byte upstream markup. It does not restore scripts, stylesheets, style blocks, forms, event handlers, or other content removed by sanitization. The client render bridge still creates and sizes its sandboxed iframe from bid or ad-unit dimensions. On POST /auction, the creative TSJS runtime and its click/resource protections are not injected. The SSAT renderer never injects that bundle and continues to render the sanitized inline adm. Sanitizer acceptance is not a host allowlist; ordinary accepted HTTP(S) URLs may cause the browser to contact external creative hosts directly.
This setting does not affect HTML/CSS response rewriting under /first-party/proxy. The publisher SSAT/page-bids path is a production creative path and follows this setting. [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 sanitized 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
Example:
<!-- Original -->
<a href="https://advertiser.com/product?id=123">Buy Now</a>
<!-- Rewritten -->
<a
href="/first-party/click?tsurl=https://advertiser.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
Wildcard Patterns: * matches any subdomain
Pattern: *.cdn.example.com
Matches:
✅ assets.cdn.example.com
✅ images.cdn.example.com
❌ cdn.example.com (no subdomain)
❌ cdn.example.com.evil.com (different domain)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.
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()- Leave script unchangedreplace(content)- Replace script contentremove_node()- Delete script element
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
async
src="/static/tsjs-core.min.js"
data-tsjs-integration="core"
></script>Timing: Injected once per HTML response before any other scripts.
Integration Bundles
Integrations can request additional bundles:
IntegrationRegistration::builder("my_integration")
.with_asset("my_integration") // Requests tsjs-my_integration.min.js
.build()Result:
<head>
<script
async
src="/static/tsjs-core.min.js"
data-tsjs-integration="core"
></script>
<script
async
src="/static/tsjs-my_integration.min.js"
data-tsjs-integration="my_integration"
></script>
<!-- Rest of head content -->
</head>Bundle Types
Each integration is built as a separate IIFE at compile time (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:
- Ensure origin response includes proper
<base>tag - Or convert 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