Skip to content

Configuration

Learn how to configure Trusted Server for your deployment.

Overview

Trusted Server uses a flexible configuration system based on:

  1. TOML Files - trusted-server.toml for ordinary configuration and secret key names
  2. Environment Variables - Typed CLI overrides with the TRUSTED_SERVER__ prefix
  3. EdgeZero Stores - Config and secret stores for the pushed blob and runtime secret values

Quick Start

Minimal Configuration

Create trusted-server.toml in your project root. Generate both secret values first with openssl rand -base64 32; the placeholders below are intentionally rejected until replaced.

toml
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"

[ec]
passphrase = "ec_passphrase"

Environment Variable Overrides

Environment variables are merged into existing TOML values by the typed ts config validate, ts config diff, and ts config push flows. They are not read by the deployed application at request time.

bash
# Format: TRUSTED_SERVER__SECTION__FIELD
export TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
# Secret overrides, when needed, are key names—not secret values.
export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
export TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase

# Replace the rejected placeholder values in trusted-server.toml, then validate.
ts config validate
ts config push --adapter fastly

Static secret references

Static app-config credentials contain stable key names only. This includes publisher, trusted-client-IP, EC, handler, Tinybird, DataDome, and S3 fields:

  • publisher.proxy_secret
  • trusted_client_ip.shared_secret, when trusted client-IP forwarding is configured
  • ec.passphrase
  • ec.partners[*].api_token, when inbound identify or batch sync is used
  • ec.partners[*].ts_pull_token, when pull sync is enabled
  • handlers[*].password
  • tinybird.auction_token_secret, when Tinybird auction telemetry is enabled
  • integrations.datadome.server_side_key_secret_name, when protection is enabled
  • integrations.datadome.protection_test_bypass.credential_secret_name, when the bypass is enabled
  • proxy.asset_routes[*].auth.access_key_id, secret_access_key, and optional session_token

Their values belong in the logical trusted_server_secrets store and are resolved only while an instance builds runtime settings. An adapter can map the logical ID to a different physical name. For example, Fastly commonly maps trusted_server_secrets to physical store ts_secrets.

Prepare an initial reference-based deployment in this order:

  1. Create the physical store and configure its trusted_server_secrets mapping.
  2. Choose a stable key name for each active credential field in the app config.
  3. Write each credential value under its referenced key without exposing it in command arguments, shell history, logs, or CI output.
  4. Run ts config validate, then ts config push --adapter fastly.
  5. Start or deploy instances after the store and pushed config are both ready.

Changing a store value does not alter already-built state. Restart or redeploy instances when rotating static credentials.

Keep publisher.proxy_secret and ec.passphrase stable unless intentionally rotating signed URLs or EC identifiers. On Spin, the app-config blob is stored under the trusted_server_config key in Spin's built-in default key-value store. Set the corresponding CLI store mapping before pushing so the write matches the runtime lookup:

bash
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default
ts config push --adapter spin

For local Spin development, add --local to the push command. Also declare a component variable for each chosen secret key name using the encoder documented in spin.toml. Missing stores, keys, invalid UTF-8, and empty values fail closed; inline plaintext fallback is not supported.

Tinybird auction telemetry

Tinybird uses the same typed secret-reference path as the other static credentials. Do not configure a feature-specific store:

toml
[tinybird]
enabled = true
api_host = "api.example.com"
auction_dataset = "auction_events_raw"
auction_token_secret = "tinybird_auction_append_token"

Store the APPEND token value under tinybird_auction_append_token in the physical store mapped from trusted_server_secrets. The token is resolved once at startup. Disabled Tinybird telemetry does not require or resolve the token. The legacy tinybird.secret_store field is accepted for one migration release, but it is ignored and omitted from newly pushed config.

Generate Secure Secrets

Generate values locally and write them directly to the platform secret store; do not put the generated output in trusted-server.toml or the app-config blob.

bash
openssl rand -base64 32

Strict Key Validation

Trusted Server rejects unknown TOML keys in runtime configuration. Before pushing or upgrading config, remove stale fields and typos; otherwise config loading can fail and the service will return its startup-error response.

Configuration Files

FilePurpose
trusted-server.tomlMain application configuration
fastly.tomlFastly Compute service settings
.env.devLocal development overrides

Key Sections

SectionPurpose
[publisher]Domain, origin, proxy settings
[trusted_client_ip]Authenticated client-IP forwarding
[ec]Edge Cookie (EC) ID generation
[tester_cookie]Optional tester-cookie endpoint
[proxy]Proxy SSRF allowlist and asset routes
[cache]Static/rehosted asset cache policy rules
[image_optimizer]Reusable Image Optimizer profile sets
[request_signing]Ed25519 request signing
[auction]Auction orchestration
[integrations.*]Partner integrations (Prebid, Next.js, etc.)

Example: Production Setup

Generate and substitute every replace-with-* value before validation or deployment.

toml
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"

[ec]
passphrase = "ec_passphrase"

[request_signing]
enabled = true
config_store_id = "01GXXX"
secret_store_id = "01GYYY"

[integrations.prebid]
enabled = true
client_side_bidders = ["example-browser-bidder"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"

[proxy]
allowed_domains = ["assets.example.com"]

[auction]
enabled = true
timeout_ms = 2000

[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
timeout_ms = 1200
routing = "explicit"

[auction.providers.pbs-main.profile_config]
debug = false

[auction.bidders.example-server-bidder]
provider = "pbs-main"

Detailed Reference

The sections below consolidate the full configuration reference on this page.

Environment Variable Overrides (Typed CLI)

Environment variables with the TRUSTED_SERVER__ prefix are merged into the base TOML configuration by ts config validate, ts config diff, and ts config push. The resolved values are validated and, for config push, stored in the app-config blob. Changing an environment variable requires rerunning validation and pushing the resolved config, not rebuilding the binary.

The pinned EdgeZero loader only overrides leaves that already exist in the parsed TOML; it does not create missing fields. Add newly introduced defaulted fields to an existing config before relying on their environment overrides. Secret overlays still contain key names, never secret values. Pass --no-env to use file values without the overlay.

Format

TRUSTED_SERVER__SECTION__SUBSECTION__FIELD

Rules:

  • Prefix: TRUSTED_SERVER
  • Separator: __ (double underscore)
  • Case: UPPERCASE
  • Sections: Match TOML hierarchy
  • Map keys: Preserve TOML punctuation. For example, provider key pbs-main uses the PBS-MAIN segment, not PBS_MAIN.

Shell assignment syntax cannot contain a hyphenated variable name. Use env to apply a provider override to a command:

bash
env 'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
  ts config validate

This example changes an existing scalar leaf. Edit TOML and run ts config validate followed by ts config push when changing an array, table, map, or rule.

Publisher Configuration

Core publisher settings for domain, origin, and proxy configuration.

[publisher]

FieldTypeRequiredDescription
domainStringYesPublisher's apex domain name
cookie_domainStringYesDomain for non-EC cookies (typically with leading dot)
origin_urlStringYesFull URL of publisher origin server
origin_host_header_overrideStringNoOutbound Host header to send while connecting to origin_url
proxy_secretStringYesSecret-store key name for the proxy URL secret
max_buffered_body_bytesIntegerNoBuffered-body cap / Fastly stream raw+decoded byte ceiling (default 16 MiB)

Note: EC cookies (ts-ec) derive their domain automatically as .{domain} and do not use cookie_domain. The cookie_domain field is used by other cookie helpers.

Example (replace the rejected secret placeholder before validation):

toml
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
# Optional: connect to origin_url but send this outbound Host header.
# origin_host_header_override = "www.publisher.com"
proxy_secret = "publisher_proxy_secret"

Environment Override:

bash
TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_HOST_HEADER_OVERRIDE=www.publisher.com
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216

Field Details

domain

Purpose: Primary domain for the publisher.

Usage:

  • Used for publisher routing and logging
  • Part of request context for proxy/origin handling

Format: Hostname without protocol or path

  • publisher.com
  • www.publisher.com
  • https://publisher.com
  • publisher.com/path

Purpose: Domain scope for non-EC cookies.

Usage:

  • Used by non-EC cookie helpers for domain scoping
  • EC cookies (ts-ec) use a separate computed domain derived from domain

Format: Domain with optional leading dot

  • .publisher.com - Shares across all subdomains
  • publisher.com - Exact domain only

Best Practice: Use leading dot (.publisher.com) for subdomain sharing.

origin_url

Purpose: Backend origin server URL for publisher content.

Usage:

  • Fallback proxy target for non-integration requests
  • HTML processing rewrites origin URLs to request host
  • Base for relative URL resolution

Format: Full URL with protocol

  • https://origin.publisher.com
  • https://origin.publisher.com:8080
  • http://192.168.1.1:9000
  • origin.publisher.com (missing protocol)

Port Handling: Includes port if non-standard (not 80/443).

origin_host_header_override

Purpose: Optional Host header to send to the publisher origin while still connecting to the host in origin_url.

Usage:

  • Connects, uses SNI, and checks certificates against origin_url
  • Sends the configured value as the outbound HTTP Host header
  • Useful when the origin endpoint expects a canonical publisher hostname

Format: Hostname with optional port, without protocol, path, query, or fragment

  • www.publisher.com
  • www.publisher.com:8443
  • https://www.publisher.com
  • www.publisher.com/path

Default: When omitted, Trusted Server sends the host from origin_url.

proxy_secret

Purpose: Secret-store key name for the HMAC-SHA256 value used to sign proxy URLs.

The referenced value is resolved from trusted_server_secrets at startup. Generate it with a cryptographically secure random source; at least 32 random bytes are recommended. Keep that value confidential, rotate it only intentionally, and never put it in the TOML file or pushed app-config blob.

Usage:

  • Signs /first-party/proxy URLs
  • Signs /first-party/click URLs
  • Validates incoming proxy requests
  • Prevents URL tampering

Security Warning

Changing proxy_secret invalidates all existing signed URLs. Plan rotations carefully and use graceful transition periods.

max_buffered_body_bytes

Purpose: Upper bound on how much of a publisher origin body the rewrite pipeline holds in memory — the post-rewrite output buffer on buffered adapters, and the per-stream raw/decoded byte ceiling on the Fastly streaming path.

Usage:

  • On buffered adapters (Axum, Cloudflare, Spin) it caps the decoded, post-rewrite output buffer for a publisher response processed in full. It also bounds how much decoded gzip output may sit in the heap at any one moment, so a decompression bomb is rejected mid-decode rather than after its full expansion. That second bound is per-step, not a total: a gzip-encoded response passes or fails on the same post-rewrite output size as the identity, deflate and brotli versions of the same body.
  • On the Fastly streaming path the origin body is preserved as a stream, so the same value caps the stream twice over: the cumulative raw (still compressed) bytes pulled from origin, and the cumulative decoded bytes emitted by the decompressor. The decoded cap is enforced during decompression, so a decompression bomb is rejected before its expansion is materialized rather than after.

Behavior when exceeded:

  • On buffered adapters the response fails before any bytes are committed.
  • On the streaming path the response headers are already committed when either cap trips, so the body is truncated mid-stream and the error is logged — the client receives a short (incomplete) body rather than a 5xx. Size the cap above your largest expected decoded page so legitimate responses are never truncated.

Default: 16777216 (16 MiB). On the Fastly streaming path this is now the sole ceiling: origin bodies are streamed rather than materialized in full, so the previous ~10 MiB raw-body limit no longer applies.

Minimum: Must be at least 1. A value of 0 is rejected at startup because a zero-byte cap fails every non-empty publisher response.

Environment Override:

bash
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216

Trusted Client IP Configuration

Use this optional section when a trusted CDN service forwards requests to the Fastly service running Trusted Server. It lets Trusted Server use the reader's address instead of the immediate fronting edge node's address. Only the Fastly adapter honours this section; the Cloudflare, Spin, and Axum adapters validate it but keep using their own runtime client address.

[trusted_client_ip]

FieldTypeRequiredDescription
ip_headerStringYesHeader containing exactly one reader IP address
auth_headerStringYesHeader containing exactly one shared-secret value
shared_secretStringYesKey in trusted_server_secrets for the front-door shared secret

All three fields are required when the section exists. When the section is absent, Trusted Server continues to use the immediate peer address, and ts config push omits the section from the published config blob so instances running an older binary keep accepting the blob.

Deploy the code before pushing the config

Once the section is configured, the pushed blob carries it, and Settings rejects unknown fields. A binary that predates trusted client-IP support fails to load a blob containing this section and returns its startup-error response. Upgrade every instance before pushing a config that enables the section, and restore a config without the section before rolling instances back. Getting this order wrong takes the service down rather than degrading it.

toml
[trusted_client_ip]
ip_header = "x-ts-client-ip"
auth_header = "x-ts-client-ip-auth"
shared_secret = "trusted_client_ip_shared_secret"

Prefer a dedicated x- name for ip_header, as shown. fastly-client-ip is also accepted and suits a fronting service dedicated to Trusted Server, but on a service carrying other traffic a dedicated name means the front door never modifies Fastly-Client-IP, so other consumers of that header keep working unchanged. See Fastly Setup for the front-door configuration this section depends on.

The front door must overwrite both headers on every request it forwards to Trusted Server, and must remove client-supplied copies on its other routes. Trusted Server resolves shared_secret from trusted_server_secrets at startup. It accepts the forwarded address only when the request has exactly one auth_header value that matches the resolved secret byte-for-byte and exactly one ip_header value that parses directly as IPv4 or IPv6. Values are not trimmed or normalized. Missing, empty, duplicate, non-UTF-8, mismatched, or malformed values do not reject the request; Trusted Server safely falls back to the immediate peer address. Both configured headers are removed before routing.

Header names are validated case-insensitively. ip_header must be fastly-client-ip or start with x-, while auth_header must start with x-. The names must differ. Neither field may use a header name reserved for Trusted Server's own internal signals (for example x-forwarded-for, x-geo-info-available, x-ts-ec, x-ts-tls-protocol, or x-ts-tls-cipher); the full reserved set is the internal-header list that Trusted Server strips before forwarding to third parties. These restrictions exclude standard sensitive headers such as Host, Content-Length, Cookie, and Authorization, as well as every Trusted Server internal header. Choose dedicated x- names that no other application or routing logic uses, because Trusted Server removes the configured headers before routing.

Generate the referenced secret value with a cryptographically secure random generator, encode it as hex or base64url, and store the same value only in the front door and the physical store mapped from trusted_server_secrets. Put only the key name in Trusted Server configuration. The resolved value must contain at least 32 ASCII graphic bytes (! through ~) with no whitespace, controls, DEL, or non-ASCII bytes.

Independently of this section, the Fastly adapter treats fastly-client-ip as client-spoofable and strips it at request entry, so Trusted Server no longer forwards an inbound Fastly-Client-IP to the publisher origin. This applies even when [trusted_client_ip] is absent. Check whether the origin reads that header before deploying.

Startup fails closed if the configured key is missing, empty, invalid UTF-8, or resolves to an invalid shared-secret value. Every adapter removes the configured IP and authentication headers before routing, although only Fastly uses them for client-IP resolution.

Environment Overrides:

bash
TRUSTED_SERVER__TRUSTED_CLIENT_IP__IP_HEADER=x-ts-client-ip
TRUSTED_SERVER__TRUSTED_CLIENT_IP__AUTH_HEADER=x-ts-client-ip-auth
TRUSTED_SERVER__TRUSTED_CLIENT_IP__SHARED_SECRET=trusted_client_ip_shared_secret

Because the typed environment overlay cannot create a missing section, add [trusted_client_ip] and all three fields to the TOML before using these overrides.

Settings for the optional tester-cookie endpoints. This feature is disabled by default and should only be enabled for intentional QA or troubleshooting flows.

FieldTypeRequiredDescription
enabledBooleanNoEnables routes to set and clear ts-tester cookie

When enabled, GET /_ts/set-tester returns 204 No Content and sets:

http
Set-Cookie: ts-tester=true; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax
Cache-Control: no-store, private

GET /_ts/clear-tester returns 204 No Content and clears the cookie:

http
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
Cache-Control: no-store, private

When disabled, both routes return 404 Not Found and do not set a cookie.

WARNING

The cookie is scoped with [publisher].cookie_domain, not the EC-specific computed domain. Keep cookie_domain aligned with the browser scope where your QA tooling expects to read ts-tester.

Example:

toml
[tester_cookie]
enabled = true

Environment Override:

bash
TRUSTED_SERVER__TESTER_COOKIE__ENABLED=true

EC Configuration

Settings for Edge Cookie identifier generation. The ec_store KV store is the only KV-backed EC lifecycle store. It holds identity graph state, minimal consent metadata, source-domain keyed partner UIDs, and withdrawal tombstones. Consent configuration controls request-local interpretation and forwarding, not separate KV persistence.

[ec]

passphrase is a key name in trusted_server_secrets; the resolved value must be at least 32 bytes. Keep it stable to preserve EC identifier continuity.

FieldTypeRequiredDescription
passphraseStringYesPublisher passphrase used as HMAC key
ec_storeString or nullNoFastly KV store name for EC identity graph and withdrawal state
pull_sync_concurrencyIntegerNoMaximum concurrent pull-sync requests per organic response
cluster_trust_thresholdIntegerNoCluster size threshold for identity trust decisions
cluster_recheck_secsIntegerNoLegacy compatibility setting; cluster rechecks no longer use timestamps
partnersArrayNoStatic partner registry entries

Partner keying

source_domain is the canonical partner key. It matches incoming OpenRTB EID source values and is also used as the EC KV ids map key.

api_token is optional. Set it to a key in trusted_server_secrets only when the partner calls the inbound identify or batch-sync APIs. A partner without api_token remains available for source-domain lookup, bidstream EIDs, and outbound pull sync, but cannot authenticate to those inbound APIs.

Example:

toml
[ec]
passphrase = "ec_passphrase"
ec_store = "ec_identity_store"

[[ec.partners]]
name = "Mocktioneer SSP"
source_domain = "mocktioneer.example"
bidstream_enabled = true
# api_token = "partner_api_token"  # only for inbound identify or batch sync
# ts_pull_token = "partner_ts_pull_token"  # required when pull sync is enabled

Environment Override:

bash
TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
TRUSTED_SERVER__EC__EC_STORE=ec_identity_store

Field Details

passphrase

Purpose: Secret-store key name whose resolved value is the HMAC key for EC ID generation.

Security:

  • The key name is stored in app config; the value is stored in trusted_server_secrets
  • Keep the value stable unless intentionally rotating EC identifiers
  • Do not place the value in environment overlays or the pushed blob

Validation: Application startup fails if:

  • Empty string

Response Headers

Custom headers added to all responses.

[response_headers]

Purpose: Add custom HTTP headers to every response.

Format: Key-value pairs

Example:

toml
[response_headers]
X-Custom-Header = "custom value"
X-Publisher-ID = "pub-12345"
X-Environment = "production"
Cache-Control = "public, max-age=3600"

Environment Override:

Override an existing header leaf by preserving its TOML key punctuation in the environment path. Shell assignment syntax cannot contain hyphens, so use env:

bash
env 'TRUSTED_SERVER__RESPONSE_HEADERS__X-CUSTOM-HEADER=updated value' \
  ts config validate

The overlay cannot add a header or replace the whole response_headers table. Edit TOML, validate, and push again for those changes.

Use Cases:

  • Custom measurement headers
  • Cache control overrides
  • Debugging identifiers
  • CORS headers (if needed)

Header Precedence

Custom headers may be overwritten by application logic. Standard headers (Content-Type, Content-Length) are controlled by the application.

Request Signing

Configuration for Ed25519 request signing and JWKS management.

[request_signing]

FieldTypeRequiredDescription
enabledBooleanNo (default: false)Enable request signing features
config_store_idStringIf enabledFastly Config Store ID for JWKS
secret_store_idStringIf enabledFastly Secret Store ID for private keys

Example:

toml
[request_signing]
enabled = true
config_store_id = "01GXXX"  # From Fastly dashboard
secret_store_id = "01GYYY"  # From Fastly dashboard

Environment Override:

bash
TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true
TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID=01GXXX
TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID=01GYYY

Store Setup

Config Store (for public keys):

bash
# Create store
fastly config-store create --name=jwks_store

# Get store ID
fastly config-store list

Secret Store (for private keys):

bash
# Create store
fastly secret-store create --name=signing_keys

# Get store ID
fastly secret-store list

Local Dev Setup (fastly.toml):

toml
[local_server.config_stores]
  [local_server.config_stores.jwks_store]
    file = "test-data/jwks_store.json"

[local_server.secret_stores]
  [local_server.secret_stores.signing_keys]
    file = "test-data/signing_keys.json"

See Request Signing and Key Rotation for usage.

Basic Authentication Handlers

Path-based HTTP Basic Authentication.

[[handlers]]

Purpose: Protect specific paths with username/password authentication.

Format: Array of handler objects

FieldTypeRequiredDescription
pathString (Regex)YesRegular expression matching paths
usernameStringYesHTTP Basic Auth username
passwordStringYesHTTP Basic Auth password

Example:

toml
# Single handler
[[handlers]]
path = "^/_ts/admin"
username = "admin"
password = "admin_password"

# Multiple handlers
[[handlers]]
path = "^/secure"
username = "user1"
password = "secure_handler_password"

[[handlers]]
path = "^/api/private"
username = "api-user"
password = "api_handler_password"

Environment Override:

bash
# Handler 0
TRUSTED_SERVER__HANDLERS__0__PATH="^/_ts/admin"
TRUSTED_SERVER__HANDLERS__0__USERNAME="admin"
TRUSTED_SERVER__HANDLERS__0__PASSWORD="admin_password"

# Handler 1
TRUSTED_SERVER__HANDLERS__1__PATH="^/api/private"
TRUSTED_SERVER__HANDLERS__1__USERNAME="api-user"
TRUSTED_SERVER__HANDLERS__1__PASSWORD="api_handler_password"

Path Patterns

Regex Syntax: Standard Rust regex patterns

Examples:

toml
# Exact path
path = "^/_ts/admin$"  # Only /_ts/admin

# Prefix match
path = "^/_ts/admin"   # /_ts/admin, /_ts/admin/users, /_ts/admin/settings

# Multiple paths
path = "^/(admin|secure|private)"

# File extension
path = "\\.pdf$"   # All PDF files

# Complex pattern
path = "^/api/v[0-9]+/private"  # /api/v1/private, /api/v2/private

Validation: Application startup fails if regex is invalid.

Admin coverage and passwords are validated at startup

Startup fails when no handler covers an admin route. The dynamic /_ts/admin/ec/{id} route accepts any segment after /_ts/admin/ec/, and Basic Auth runs on the raw path before routing, so coverage cannot be inferred from ID-shaped samples: a pattern such as ^/_ts/admin/ec/[a-f0-9]{64}[.][A-Za-z0-9]{6}$ is rejected. Use a prefix-level matcher (^/_ts/admin, or ^/_ts/admin/ec/ alongside the other admin patterns).

Handler expressions match the raw URI path, while a publisher origin may decode percent-encoded aliases before routing. For a whole-site staging gate, use path = "^/"; do not rely on a decoded-path prefix such as ^/secure to protect equivalent origin paths.

Startup also fails when any handler — admin or not — uses a placeholder or well-known weak password (changeme, password, admin, or a replace-with-… template value). Handler selection is first-match-wins, so a narrow handler ahead of the admin pattern governs the paths it matches.

Scope patterns to the paths you mean

Handler patterns are matched against the full request path, so a broad pattern covers everything beneath it. The /_ts/ namespace holds both admin routes and browser-facing endpoints that anonymous visitors must be able to reach:

PathCalled by
/_ts/page-bidsTrusted Server JS, on SPA navigation
/_ts/api/v1/identifyTrusted Server JS, in the browser
/_ts/api/v1/batch-syncTrusted Server JS, in the browser

A pattern such as path = "^/_ts" puts those behind Basic Auth. Browser fetches never carry Basic credentials, so every visitor gets 401 — on /_ts/page-bids that means no ads after any client-side navigation. Match the admin routes specifically (^/_ts/admin) instead.

Upgrading from a release before /_ts/page-bids existed: if any handler pattern covers it, narrow the pattern. The Trusted Server JS bundle falls back to the deprecated /__ts/page-bids alias in the meantime, but that alias is scheduled for removal (#970).

Security Considerations

Password Storage:

  • handlers[*].password is a key name in trusted_server_secrets
  • Store the resolved password only in the platform secret store
  • Rotate passwords through the store and restart/redeploy instances

Limitations:

  • HTTP Basic Auth (not OAuth/JWT)
  • Single username/password per path
  • No role-based access control
  • No rate limiting (add at edge)

Production Use

Do not put handler passwords in trusted-server.toml, environment overlays, or app-config blobs. Provision the referenced key in trusted_server_secrets before pushing the config.

URL Rewrite Configuration

Control which domains are excluded from first-party rewriting.

[rewrite]

FieldTypeRequiredDescription
exclude_domainsArray[String]No (default: [])Domains to skip rewriting

Example:

toml
[rewrite]
exclude_domains = [
    "*.cdn.trusted-partner.com",  # Wildcard
    "first-party.publisher.com",  # Exact match
    "localhost",                  # Development
]

Environment Override:

EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit exclude_domains in TOML, then validate and push the file again.

Pattern Matching

Wildcard Patterns (*):

toml
"*.cdn.example.com"

Matches:

  • assets.cdn.example.com
  • images.cdn.example.com
  • cdn.example.com (base domain)
  • cdn.example.com.evil.com (different domain)

Exact Patterns (no *):

toml
"api.example.com"

Matches:

  • api.example.com
  • www.api.example.com
  • api.example.com.evil.com

Use Cases

Trusted Partners:

toml
exclude_domains = ["*.approved-cdn.com"]

First-Party Resources:

toml
exclude_domains = ["assets.publisher.com", "static.publisher.com"]

Development:

toml
exclude_domains = ["localhost", "127.0.0.1", "*.local"]

Performance (already first-party):

toml
exclude_domains = ["*.publisher.com"]  # Skip unnecessary proxying

See Creative Processing for details.

Proxy Configuration

Controls first-party proxy security settings and path-based asset routes.

[proxy]

FieldTypeRequiredDescription
allowed_domainsArray[String]No (default: [])Hosts permitted for signing, initial fetches, and redirects
certificate_checkBooleanNo (default: true)Verify TLS certificates when proxying HTTPS origins
asset_routesArray[Table]No (default: [])Path prefixes proxied directly to configured origins

Example:

toml
[proxy]
allowed_domains = [
  "assets.example.com",  # Exact match
  "*.cdn.example.com",   # Wildcard: cdn.example.com and all subdomains
]

Environment Override:

EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit allowed_domains in TOML, then validate and push the file again.

Field Details

allowed_domains

Purpose: Allowlist of target hosts permitted for /first-party/sign and /first-party/proxy. When integrations.prebid.external_bundle_url is configured, this list must cover its host and any HTTPS redirect targets.

Behavior: Trusted Server checks the parsed host before signing a target, before fetching the initial proxy target, and before following each HTTP redirect (301/302/303/307/308). A host that does not match the list is blocked with a 403 error.

Default - open mode: When allowed_domains is absent or empty and no external Prebid bundle is configured, every valid host is allowed for signing, initial fetches, and redirects. Configuring an external Prebid bundle with an empty list fails deploy validation. Open mode supports zero-config development but should not be used in production.

Pattern Matching:

PatternMatchesDoes not match
assets.example.comassets.example.comsub.assets.example.com
*.cdn.example.comcdn.example.com, static.cdn.example.com, a.b.cdn.example.comevil-cdn.example.com
  • "example.com" — exact match only.
  • "*.example.com" — matches the base domain and any subdomain at any depth.
  • Matching is case-insensitive; entries are normalized to lowercase at startup.
  • Blank entries are ignored.
  • The * wildcard requires a dot boundary: *.example.com does not match evil-example.com.

Production Recommendation

Always configure allowed_domains in production. Without an explicit allowlist, clients can sign and fetch valid URLs for arbitrary hosts, including redirect targets.

toml
[proxy]
allowed_domains = [
  "assets.example.com",
  "*.cdn.example.com",
]

See First-Party Proxy for usage details.

certificate_check

Purpose: Control TLS certificate verification for HTTPS proxy and asset-route origins.

Default: true

Set this to false only for local development with self-signed certificates.

[[proxy.asset_routes]]

Asset routes proxy selected first-party paths to an alternate asset origin without requiring signed /first-party/proxy URLs.

FieldTypeRequiredDescription
prefixStringYesRequest path prefix to match
origin_urlStringYesAbsolute http or https origin URL
path_patternStringNoRegex matched against the incoming request path
target_pathStringNoReplacement path used with path_pattern
authTableNoOptional origin authentication
image_optimizerTableNoOptional route-level Image Optimizer settings

Example:

toml
[[proxy.asset_routes]]
prefix = "/assets/"
origin_url = "https://assets.example.com"

Path rewrite example:

toml
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://assets-cdn.example.com"
path_pattern = "^/\\.image/(.*)/[^/]+\\.([^/.]+)$"
target_path = "/image/upload/$1.$2"

Behavior:

  • Only GET and HEAD requests use asset routes.
  • Built-in and integration routes take precedence.
  • The longest matching asset-route prefix wins.
  • path_pattern and target_path must be configured together.
  • origin_url must not include userinfo, a path, a query string, or a fragment.
  • Unsafe origin response headers such as Set-Cookie are stripped before the response reaches the browser.

[proxy.asset_routes.auth]

The first supported origin auth type is s3_sigv4.

FieldTypeRequiredDefaultDescription
typeStringYesnoneMust be s3_sigv4
regionStringYesnoneAWS region used in the SigV4 credential scope
access_key_idStringNoaccess_key_idDefault-store secret reference for the AWS access key ID
secret_access_keyStringNosecret_access_keyDefault-store secret reference for the AWS secret access key
session_tokenStringNounsetOptional secret key containing a session token
origin_queryStringNoroute defaultpreserve or strip

Example:

toml
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://bucket.s3.us-east-1.amazonaws.com"

[proxy.asset_routes.auth]
type = "s3_sigv4"
region = "us-east-1"
origin_query = "strip"
access_key_id = "s3_access_key_id"
secret_access_key = "s3_secret_access_key"
# session_token = "s3_session_token"

S3 auth uses header-based AWS SigV4 with UNSIGNED-PAYLOAD. It is scoped to read-only asset requests and expects origin_url to use the S3 host that AWS validates. Credential references resolve from trusted_server_secrets at startup, and request signing performs no secret-store reads.

Effective origin_query precedence is auth-level origin_query, then enabled Image Optimizer origin_query, then the route default.

[proxy.asset_routes.image_optimizer]

Route-level Image Optimizer configuration selects a reusable profile set.

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable Image Optimizer for the route
regionStringYes when enablednoneFastly IO processing region, such as us_east
profile_setStringYes when enablednoneName under [image_optimizer.profile_sets.*]
origin_queryStringNostrip when enabledpreserve or strip; effective preserve is rejected while IO is enabled

Example:

toml
[proxy.asset_routes.image_optimizer]
enabled = true
region = "us_east"
profile_set = "default_images"

[image_optimizer.profile_sets.<name>]

Profile sets convert small request query controls into a closed set of Image Optimizer parameters.

FieldTypeRequiredDefaultDescription
base_paramsStringNo""Params applied before profile-specific params
default_profileStringNodefaultProfile used when no profile is requested
unknown_profileStringNouse_defaultuse_default or reject
profile_paramStringNoprofileQuery parameter containing the profile name
aspect_ratio_paramStringNoarQuery parameter containing aspect ratio
debug_paramStringNo_io_debugQuery parameter that disables IO when set to 1

Profile values live under [image_optimizer.profile_sets.<name>.profiles] and use query-string syntax.

toml
[image_optimizer.profile_sets.default_images]
base_params = "quality=70&resize-filter=bicubic"
default_profile = "default"
unknown_profile = "use_default"
profile_param = "profile"
aspect_ratio_param = "ar"
debug_param = "_io_debug"

[image_optimizer.profile_sets.default_images.profiles]
default = "width=1920"
medium = "format=auto&width=828"
thumbnail = "width=150&crop=1:1,smart"

Supported profile parameters are quality, resize-filter, format, width, height, and crop. Unknown profile parameters fail configuration validation.

[image_optimizer.profile_sets.<name>.aspect_ratios]

FieldTypeRequiredDescription
allowedArray[String]NoAllowed query values such as 1-1 or 16-9
profilesArray[String]NoDefined profiles that accept aspect-ratio overrides
toml
[image_optimizer.profile_sets.default_images.aspect_ratios]
allowed = ["1-1", "16-9", "4-3"]
profiles = ["medium", "thumbnail"]

[image_optimizer.profile_sets.<name>.crop_offsets]

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable offset bucketing
x_paramStringNoxQuery parameter for x-axis offset
y_paramStringNoyQuery parameter for y-axis offset
bucketsArray[Integer]No[10, 30, 50, 70, 90]Offset buckets in 0..=100
defaultIntegerNo50Offset used when input is missing or invalid
when_missingStringNosmartsmart or none when neither offset exists
toml
[image_optimizer.profile_sets.default_images.crop_offsets]
enabled = true
x_param = "x"
y_param = "y"
buckets = [10, 30, 50, 70, 90]
default = 50
when_missing = "smart"

See Asset Routes for request flow, S3 auth details, and Image Optimizer behavior.

Cache Configuration

Static and rehosted asset cache upgrades are operator-controlled. By default, Trusted Server leaves arbitrary publisher-origin assets under origin cache control. Add [[cache.asset_rules]] entries only for paths that are known to be content-addressed or otherwise safe for the configured TTL.

[[cache.asset_rules]]

Rules are evaluated in file order; the first enabled matching rule wins. Disabled rules never match, and their matcher and policy validation is deferred until they are enabled. Rule IDs are always normalized and must remain nonempty and unique, including for disabled placeholders.

FieldTypeRequiredDescription
idStringYesUnique operator-facing rule identifier
enabledBooleanNoWhether the rule participates in matching (default false)
presetStringMatcherBuilt-in preset such as nextjs-static
path_prefixStringMatcherRequest path prefix
path_globStringMatcherSingle glob matched against the request path
path_globsArray[String]MatcherMultiple globs matched against the request path
path_regexStringMatcherRegex matched against the request path
extensionsArray[String]MatcherCase-insensitive file extensions
fingerprint_styleStringNoRequired bundler fingerprint convention before matching
visibilityStringNopublic or private (default public)
browser_ttl_secondsIntegerPolicyBrowser max-age; required for private rules and positive with immutable = true
edge_ttl_secondsIntegerPolicyPublic rules only: TTL emitted through the runtime-specific shared-cache directive
stale_while_revalidate_secondsIntegerNoOptional stale-while-revalidate
stale_if_error_secondsIntegerNoOptional stale-if-error
immutableBooleanNoAdd immutable for a validated content-addressed rule

An enabled rule must configure exactly one matcher. Public rules must configure at least one of browser_ttl_seconds or edge_ttl_seconds; private rules must configure browser_ttl_seconds and must not configure edge_ttl_seconds. path_glob and path_globs are mutually exclusive. immutable = true additionally requires a positive browser TTL and either the content-addressed nextjs-static preset, hex, or esbuild-base32.

The filename fingerprint check examines the suffix immediately before the final extension and requires a nonempty filename prefix separated by ., -, _, or ~. The accepted immutable conventions are:

  • hex: hexadecimal suffixes of at least eight characters containing a letter, such as app.0123abcd.js;
  • esbuild-base32: eight-character uppercase Base32 suffixes, such as app-VRTVD5R5.js.

vite-base64-url remains available for non-immutable cache rules, but it cannot prove content addressing. Ordinary names such as hero-Portrait.jpg can match its eight-character Base64URL shape. A matching rule whose selected fingerprint style fails emits a debug log with the rule ID and rejected path.

Glob patterns are case-sensitive. * matches within a single path component, while ** matches recursively: /assets/*.js matches /assets/app.js but not /assets/vendor/app.js; /assets/**/*.js matches both.

Next.js preset example (disabled until the publisher confirms /_next/static/ is content-addressed):

toml
[[cache.asset_rules]]
id = "nextjs-static"
enabled = false
preset = "nextjs-static"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = true

Publisher allowlist example (enable only for an unambiguous immutable filename convention):

toml
[[cache.asset_rules]]
id = "publisher-fingerprinted-assets"
enabled = false
path_globs = [
  "/assets/**/*.js",
  "/assets/**/*.css",
  "/assets/**/*.png",
  "/assets/**/*.webp",
]
fingerprint_style = "hex"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = true

If [cache] is omitted or no enabled rule matches, Trusted Server preserves the origin cache policy for publisher-origin assets. On the publisher pass-through path, an origin private or no-store directive vetoes a matching rule. Other origin cache directives, including no-cache, are replaced by the configured policy. Vary is preserved, so do not assign a public immutable rule to paths that vary by cookies or other user-specific request state.

On a configured Fastly asset-rehost route, a matching rule is authoritative over the third-party origin's cache defaults, including no-store, because Trusted Server owns the rehosted copy. A later Trusted Server or operator-applied private or no-store directive still vetoes public policy reapplication and removes shared-cache headers.

TS-owned validated hash URLs such as /static/tsjs=...js?v=<hash> use their built-in cache policy and do not require an asset rule. Shared-cache keys for /static/tsjs= must preserve v; otherwise a matching immutable response can collide with the missing or mismatched version's short-TTL response.

edge_ttl_seconds only emits the selected runtime's shared-cache directive for public rules. The runtime or service must also enable and consume that directive. The checked-in Cloudflare manifests intentionally do not enable Workers Cache: the Worker serves the full publisher gateway, not an isolated static-only entrypoint. Emitting Cloudflare-CDN-Cache-Control alone must not be treated as permission to cache every response. Any future Workers Cache opt-in must isolate or explicitly allowlist cacheable traffic. Fastly synthetic and final egress responses still require explicit runtime cache integration, tracked in #908.

Integration Configurations

Settings for built-in integrations (Prebid, Next.js, Osano, Permutive, Testlight). For other integrations (APS, Didomi, Lockr, GAM, etc.), see the relevant integration guides.

Common Fields

All integrations support an enabled flag. Defaults vary by integration and only apply when the integration section exists in trusted-server.toml.

FieldTypeDescription
enabledBooleanEnable/disable the integration

Prebid Integration

[integrations.prebid] owns browser behavior only. Server endpoint, provider timeout, routing, profile debug/test controls, consent forwarding, bidder-param overrides, and notification suppression belong under [auction].

Browser fieldTypeDefaultDescription
enabledBooleantrueEnable browser bundle injection, interception, and the trustedServer adapter
account_idStringNoneOptional account value injected into browser Prebid configuration
timeout_msInteger1000Browser Prebid.js timeout; independent of every server provider timeout
debugBooleanfalseBrowser Prebid.js debug flag; independent of server profile debug
client_side_biddersArray[String][]Bidders kept on native browser adapters
excluded_gam_ad_unit_path_suffixesArray[String][]GAM suffixes excluded from Trusted Server refresh auctions
script_patternsArray[String]["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]Publisher Prebid script paths intercepted by Trusted Server
external_bundle_urlStringRequired when enabledHTTPS publisher-specific Prebid.js bundle URL
external_bundle_sha256 / *_sriStringNoneOptional bundle integrity and cache metadata
bundle.adapters / user_id_modulesArray[String]CLI selectionInputs used by ts prebid bundle

Server-side bidder codes are derived from validated [auction.bidders.*] routes and injected into the browser. There is no second server bidder list in [integrations.prebid]. A browser bidder stays client-side only when named in client_side_bidders and its adapter is present in the generated bundle.

Example:

toml
[integrations.prebid]
enabled = true
timeout_ms = 1000
debug = false
client_side_bidders = ["example-browser"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"
script_patterns = ["/prebid.js", "/prebid.min.js"]

[proxy]
allowed_domains = ["assets.example.com"]

[integrations.prebid.bundle]
adapters = ["example-browser"]

[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"

[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"
bid_param_overrides = { example-server = { placement = "example-placement" } }

[[auction.providers.pbs-main.profile_config.bid_param_override_rules]]
when.bidder = "example-server"
when.zone = "header"
set = { placement = "example-header-placement" }

[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]

[auction.bidders.example-server]
provider = "pbs-main"

Environment override:

bash
env 'TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true' \
  'TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000' \
  'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
  ts config validate

Environment overlays only replace existing scalar leaves. Keep client_side_bidders, provider profile tables, bidder-parameter overrides, and rules in TOML, then validate and push the edited file.

Script Pattern Matching:

The script_patterns configuration determines which Prebid scripts are intercepted and replaced with empty JavaScript responses. This prevents client-side Prebid.js from loading when using server-side bidding.

  • Suffix matching: /prebid.min.js matches any URL ending with that path
  • Wildcard patterns: /static/prebid/* matches paths under that prefix
  • Disable interception: Set script_patterns = [] to keep client-side Prebid

See Prebid Integration for full details.

Server Bid Param Override Surfaces:

These fields belong under [auction.providers.<id>.profile_config] for a prebid-server provider:

  • bid_param_overrides: static per-bidder shallow-merge overrides;
  • bid_param_zone_overrides: per-bidder, per-zone shallow-merge overrides; and
  • bid_param_override_rules: canonical ordered rules with when matchers and set objects.

Compatibility-shaped fields are normalized into the same profile-local runtime engine. Explicit rules run after compatibility-derived rules, so later rules win on conflicts.

Next.js Integration

Section: [integrations.nextjs]

FieldTypeDefaultDescription
enabledBooleanfalseEnable Next.js integration
rewrite_attributesArray[String]["href","link","url"]Attributes to rewrite
max_combined_payload_bytesInteger10485760Max combined RSC payload size

Example:

toml
[integrations.nextjs]
enabled = true
rewrite_attributes = ["href", "link", "url", "src"]
max_combined_payload_bytes = 10485760

Environment Override:

bash
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__MAX_COMBINED_PAYLOAD_BYTES=10485760

Edit rewrite_attributes in TOML because the overlay cannot replace arrays.

Osano Integration

Section: [integrations.osano]

FieldTypeDefaultDescription
enabledBooleanfalseEnable the Osano browser consent mirror

Example:

toml
[integrations.osano]
enabled = true

Environment Override:

bash
TRUSTED_SERVER__INTEGRATIONS__OSANO__ENABLED=true

The Osano mirror runs in the browser, so consent cookies it writes are available to Trusted Server on requests after the page where Osano consent APIs become ready. See Osano Integration for details.

Permutive Integration

Section: [integrations.permutive]

FieldTypeDefaultDescription
enabledBooleantrueEnable Permutive integration
organization_idStringRequiredPermutive organization ID
workspace_idStringRequiredPermutive workspace ID
project_idString""Permutive project ID
api_endpointStringhttps://api.permutive.comPermutive API URL
secure_signals_endpointStringhttps://secure-signals.permutive.appSecure signals URL
cache_ttl_secondsInteger3600Cache TTL in seconds
rewrite_sdkBooleantrueRewrite Permutive SDK references

Example:

toml
[integrations.permutive]
enabled = true
organization_id = "org-12345"
workspace_id = "ws-67890"
project_id = "proj-abcde"
api_endpoint = "https://api.permutive.com"
secure_signals_endpoint = "https://secure-signals.permutive.app"
cache_ttl_seconds = 7200
rewrite_sdk = true

Testlight Integration

Section: [integrations.testlight]

FieldTypeDefaultDescription
enabledBooleantrueEnable Testlight integration
endpointStringRequiredTestlight auction endpoint
timeout_msInteger1000Request timeout in milliseconds
shim_srcString/static/tsjs=tsjs-unified.min.js?v=<hash>Script source for testlight shim
rewrite_scriptsBooleanfalseRewrite Testlight script references

Example:

toml
[integrations.testlight]
enabled = true
endpoint = "https://testlight.example/openrtb2/auction"
timeout_ms = 1500
rewrite_scripts = true

Auction Configuration

[auction.providers.*] is the only server-side provider inventory, and [auction.bidders.*] is the only client-visible bidder route map. The optional [auction].mediator remains a separate integration selection; it is not a provider or bidder route.

[auction]

FieldTypeDefaultDescription
enabledBooleanfalseEnable the auction orchestrator
sanitize_creativesBooleanfalseStrip executable markup from winning-bid adm before delivery
rewrite_creativesBooleantrueRewrite winning-bid adm through first-party endpoints
timeout_msInteger2000Logical auction budget in milliseconds
mediatorStringNoneOptional separate adserver_mock mediator
creative_storeString"creative_store"Deprecated; creatives are delivered inline
allowed_context_keysArray[]Request context keys admitted into the auction

Creative markup delivered by POST /auction and the publisher SSAT/page-bids path is processed by two independent passes. With sanitize_creatives = true (opt-in, default false), executable markup (script/object/embed/form and event handlers) is stripped together with its inner content. This blanks script-based creatives, so enable it only when creatives render in a context that shares the publisher's origin. With rewrite_creatives = true (the default), eligible absolute or protocol-relative resource and click URLs not excluded by rewrite configuration are converted to signed first-party endpoints, and any bidder-supplied <base> element is removed. The POST /auction path emits root-relative endpoints and injects the creative TSJS runtime exactly once, whether or not the bidder supplied a <body>, since bare fragments are the common adm shape. The foreign-origin SSAT renderer emits absolute endpoints and does not inject that bundle. With both disabled, adm ships exactly as the bidder returned it, except that a creative larger than the 1 MiB per-creative cap is rejected in every mode and its adm is dropped. Accepted external URLs are not host allowlisted by the sanitizer. Neither setting affects HTML or CSS fetched through /first-party/proxy. See Creative Processing.

Existing configs, upgrade sequencing, and rollback

Default values are omitted from stored JSON; non-default values (sanitize_creatives = true, rewrite_creatives = false) are serialized, and older AuctionConfig schemas reject unknown fields.

Upgrading: binaries that predate sanitize_creatives reject a blob that carries it, so in a rolling deployment upgrade the binary first, then push a config with sanitize_creatives = true if you want sanitization. Between the binary upgrade and the config push, sanitization is off (the new default). During that interval the creative iframe sandbox is the only isolation for /auction markup. There is no mixed-version-safe value that keeps the old unconditional sanitization: omission means "sanitize" on old code and "don't" on new code, while an explicit true fails startup on old code.

Rolling back: before reverting to a binary that does not know a field, remove that field's non-default value (and any environment override), run ts config validate, push the resulting default-compatible blob, and only then roll back the binary.

Environment overlays: The pinned EdgeZero loader cannot create missing TOML leaves. Existing configs must add both leaves under [auction] (rewrite_creatives and sanitize_creatives) before TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES / TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES can take effect. An override for a missing leaf is silently ignored.

Provider map

Breaking migration from the provider list

The former [auction].providers = ["prebid", ...] list and server-owned fields under [integrations.prebid] and [integrations.aps] are no longer accepted, even when an integration is disabled. Replace them with provider instances and bidder routes before deployment.

For Prebid Server, move server_url to provider endpoint, server timeout to provider timeout_ms, request controls and bidder-parameter overrides to the prebid-server profile_config, notification suppression to notifications, and each server bidder to [auction.bidders.<id>]. Origin-only legacy server_url values compile to /openrtb2/auction; query parameters survive, and configured non-root custom endpoint paths remain exact. Browser timeout, debug, bundle, script interception, refresh exclusions, and client_side_bidders remain under [integrations.prebid]. Configure timeout or debug under both owners when both browser and server behavior should retain the old value.

For APS, move endpoint and timeout to the provider, then move account, inventory, debug, and creative controls to the aps profile_config.

Only bidder codes listed in [auction.bidders] are folded into Trusted Server requests. Unlisted publisher bids remain native browser demand. All provider endpoints must be absolute HTTPS URLs.

The old and new blobs are mutually incompatible. Activate the new binary and map-shaped config together. A binary-first or config-first rolling deployment will put one version on a schema it rejects. Roll back by restoring the old binary and old-schema blob together.

Each table name is the provider ID used for configuration, backend correlation, health, response metadata, and telemetry. Provider IDs must match ^[a-z][a-z0-9-]{0,62}$. Multiple instances may select the same profile and endpoint because the provider ID remains their distinct runtime identity.

Example:

toml
[auction]
enabled = true
sanitize_creatives = false
rewrite_creatives = true
timeout_ms = 2000
mediator = "adserver_mock"

[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"
timeout_ms = 1200

[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"

[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]

[auction.providers.aps-main]
protocol = "openrtb-2.6"
profile = "aps"
endpoint = "https://aps.example.com/e/pb/bid"
routing = "all_eligible"

[auction.providers.aps-main.profile_config]
account_id = "example-aps-account"
debug = false
allow_script_creatives = false

[auction.bidders.example-server]
provider = "pbs-main"

[integrations.adserver_mock]
enabled = true
endpoint = "https://mediator.example.com/mediate"
timeout_ms = 500
Provider fieldRequiredDefaultDescription
protocolYesNoneMust be openrtb-2.6
profileNostandardstandard, prebid-server, or aps
endpointYesNoneAbsolute HTTPS URL with host and no credentials or fragment
timeout_msNoProfile defaultProvider logical budget before the remaining-auction cap
routingNoexplicitexplicit, or all_eligible for non-PBS profiles
profile_configNo{}Typed object owned by the selected profile
notificationsNoNo suppressionCommon nurl/burl suppression after response normalization

Timeout defaults are 1000 ms for prebid-server, 800 ms for aps, and the auction timeout for standard. An explicit provider timeout overrides the profile default. Runtime uses min(provider timeout, auction time remaining) for launch decisions and OpenRTB tmax.

routing = "explicit" sends only slots carrying a bidder assigned to that provider, plus trusted stored-request routes. routing = "all_eligible" sends every banner-compatible slot to the provider, regardless of bidder routes. It does not disclose bidder parameters assigned to another provider. APS commonly uses all_eligible to preserve its whole-inventory participation. The prebid-server profile rejects all_eligible because every PBS impression must carry routed bidder or stored-request demand.

Bidder routes and bounds

Each [auction.bidders.<bidder-id>] maps one client-visible bidder ID to exactly one provider. Bidder IDs must be nonempty, no more than 128 UTF-8 bytes, contain no control characters or surrounding whitespace, and cannot be the reserved exact ID trustedServer. Browser trustedServer.bidderParams accepts at most 128 bidder entries; its optional zone is at most 256 UTF-8 bytes.

For the standard profile, profile_config.request_ext and imp_ext must be JSON objects. Each object is limited to 16 KiB serialized, eight container levels, and 256 keys at any one object level. Reserved driver, profile, and signing fields cannot be overwritten.

Common notification suppression uses exact returned OpenRTB seat values, not bidder route IDs:

toml
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]

suppress_seats permits at most 128 unique nonempty entries, each at most 128 UTF-8 bytes and without ASCII control characters.

Validation timing and target limits

ts config validate and ordinary deploy validation compile the complete target-independent plan: profiles and defaults, routes, endpoint ownership, extension bounds, notifications, signing structure, and mediator selection. Target-specific checks are deferred to adapter startup. Startup uses the same compiled plan and additionally validates backend-name prediction/collisions and provider fan-out capability.

Fastly and Axum support multiple configured providers. Cloudflare and Spin currently reject an enabled auction with more than one provider because those adapters do not support concurrent provider fan-out. Disabled auctions may keep dormant multi-provider maps without target rejection.

A target-aware pre-write ts config push --adapter <target> callback is not available in this tree because the required EdgeZero callback is not yet available. Until it lands, push performs target-independent validation and adapter startup is the mandatory target-aware gate. Do not treat a successful push as proof that a Cloudflare or Spin multi-provider plan can start.

Deadline behavior

Configured timeouts are logical budgets, not hard wall-clock guarantees. No current adapter claims an abortable provider-wide total-request deadline. Already-launched work may complete after the logical budget and a completed late response can remain eligible. Once the logical auction budget is exhausted, Trusted Server starts no additional provider or mediator network work, then finishes local decision and delivery. An auction can therefore exceed its configured wall-clock timeout.

Creative sanitization is opt-in. sanitize_creatives = true strips executable markup before delivery. rewrite_creatives = false skips first-party URL rewriting and creative TSJS injection. See Creative Processing.

Environment overrides replace map leaves that already exist in TOML:

bash
env 'TRUSTED_SERVER__AUCTION__ENABLED=true' \
  'TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES=false' \
  'TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES=true' \
  'TRUSTED_SERVER__AUCTION__TIMEOUT_MS=2000' \
  'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__ENDPOINT=https://prebid.example.com/openrtb2/auction' \
  'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__TIMEOUT_MS=900' \
  'TRUSTED_SERVER__AUCTION__MEDIATOR=adserver_mock' \
  ts config validate

Creative Opportunities Configuration

[creative_opportunities]

Defines the ad slots the trusted server offers on a page: which pages each slot appears on (page_patterns), its supported sizes (formats), and the GAM ad unit it maps to (gam_unit_path).

enabled is the dedicated server-side ad-template switch. It defaults to true for compatibility with existing configurations. Set it to false to stop publisher HTML and SPA page-bids template delivery while retaining the slot configuration and direct POST /auction endpoint.

Publisher document cache policy

For a successful GET publisher document, Trusted Server applies the browser-only Cache-Control: private, max-age=60 policy from #1007 when the server-side ad stack is structurally inactive. Trusted Server also applies this policy to a subsequent 304 Not Modified response so revalidation cannot restore the origin freshness policy. This includes an absent [creative_opportunities] section, enabled = false, no slot matching the path, or a disabled auction. The private directive prevents shared caches that use Cache-Control from storing the document. The policy replaces the origin browser cache policy except when the origin sends private or no-store, which are preserved. Bot, prefetch, and consent-denied requests also retain the origin policy because they can produce a request-specific representation for the same URL. Error responses and non-document requests retain the origin policy.

Trusted Server leaves origin validators and CDN-specific cache headers unchanged. Those headers continue to control supporting CDNs independently of the browser-only policy. If a response using the generated inactive-stack policy later carries Set-Cookie, cookie privacy finalization replaces it with Cache-Control: private, max-age=0 and removes the CDN-specific cache headers.

toml
[creative_opportunities]
enabled = true # set to false to disable server-side ad templates
gam_network_id = "123456789"
price_granularity = "dense"

# Shared placeholder value for the site root ("/") — see {section} below.
section_root = "home"
# Which path segment names the section, 0-based. Default 0 (first segment).
# Set to 1 for locale-prefixed URLs such as "/en/news/article".
# section_segment = 0

[[creative_opportunities.slot]]
id = "ad-header"
gam_unit_path = "/{network_id}/example/{section}"
# List each section landing page as well as its subtree: `/news/*` matches
# `/news/article` but NOT `/news` — the glob requires the trailing separator.
page_patterns = ["/", "/news", "/news/*", "/reviews", "/reviews/*"]
formats = [{ width = 728, height = 90 }]

The same switch can be overridden through the typed CLI environment overlay. Because EdgeZero only replaces TOML leaves that already exist, first add enabled = true to the [creative_opportunities] block in the base config before using this override. See Environment Variable Overrides (Typed CLI) for the general overlay rules.

bash
TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__ENABLED=false

WARNING

Setting enabled = false writes this field into the pushed configuration blob. Binaries released before this setting reject the unknown field and fail to load settings, which makes every request fail. Before rolling back to an older binary, restore enabled to its default, re-push and finalize the configuration, then roll back the binary.

Shared template assembly (assembly_mode = "esi")

This configuration is an experimental validation spike scoped to IABTechLab/trusted-server#1009, not a settled production cache interface.

assembly_mode controls how initial-page slot and bid state is delivered:

  • inline (default) transforms every origin response and injects the current reader's slots and bids directly.
  • esi opts into a reader-neutral transformed-template cache on Fastly. The cache stores identity bytes containing one inert, versioned comment. On an authorized cold miss, Fastly replaces that comment in a private working copy with one synthetic ESI include and resolves it from the already-built reader state using the pinned stackpop/esi parser. No HTTP fragment request occurs. Warm hits use an exact byte split instead, preserving the fast article-prefix stream while the auction finishes.

This is deliberately not general publisher-controlled ESI. A transformed origin document containing any <esi: directive bypasses the template cache and the parser, while the ordinary byte seam still produces the reader's complete response. The stored shared template object never contains executable ESI markup.

Only Fastly currently supplies the Core Cache backend used by the shared template cache. Other adapters accept the mode but safely fall back to the inline transform on every request. This is not a top-level HTTP cache hit: Compute still runs and the final assembled response is always Cache-Control: private, no-store.

All four keys below belong directly under [creative_opportunities]. They are one feature contract: assembly_mode selects how creative-opportunity state is delivered, while the other three constrain when and how long that mode may share its template. They are not a general top-level HTTP-cache configuration.

toml
[creative_opportunities]
assembly_mode = "esi"

# Every request header, except Accept-Encoding, that the publisher origin can
# name in Vary for these documents. Names are validated and de-duplicated.
template_cache_vary = [
  "rsc",
  "next-router-state-tree",
  "next-router-prefetch",
  "next-router-segment-prefetch",
]

# Safety ceiling for the shared template. Defaults to 60; valid range 1–86400.
# The origin's remaining edge freshness may make the actual lifetime shorter.
template_cache_max_age_seconds = 1200

# Default false. Enable only after proving publisher HTML ignores Cookie.
origin_is_cookie_independent = true

The cache fails closed. A template is stored only for a GET with a processable 200 text/html origin response, a supported content encoding, and explicit positive shared freshness. private, no-store, no-cache, exhausted or malformed freshness, Set-Cookie, Vary: *, Vary: Cookie, uncovered Vary names, response-bound CSP nonces, pass-through or ambiguous authorization, diagnostics sessions, range or conditional requests, positive or malformed request max-age, min-fresh, and unsupported CDN-specific cache policy fields all bypass the template cache. Fastly Surrogate-Control is the narrow exception: the template cache accepts exactly one positive max-age plus optional valid stale-while-revalidate and stale-if-error delta-seconds. Restrictive, duplicated, malformed, or unknown directives fail closed. Stale windows never extend template-cache freshness. Freshness follows Fastly edge precedence: Surrogate-Control: max-age, then Cache-Control: s-maxage, Cache-Control: max-age, then Expires. Restrictive directives in either policy still refuse sharing. Origin Age and apparent age from Date are deducted, time spent transforming the page continues consuming freshness, and the remaining lifetime is capped by template_cache_max_age_seconds.

A browser reload commonly sends Cache-Control: max-age=0. TS may reuse a fresh reader-neutral shared template for that reload, but it still builds a new private response and runs a new per-reader auction. Explicit no-cache, no-store, positive or malformed request max-age, range, and conditional requests still bypass the template cache. Check X-TS-Template-Cache: hit to verify template reuse.

Authorization has one narrow exception. A request carrying exactly the same single Basic credential that Trusted Server just validated at the edge may share a template; pass-through, repeated, appended, or replaced values still bypass. Trusted Server does not remove the validated header, so it remains forwarded to the publisher origin. If the origin uses that credential to select response content, it must declare Vary: Authorization; that response is deliberately not stored as a shared template.

template_cache_vary is necessary because lookup occurs before the origin can return Vary. Presence, empty values, repeated raw field values, host/scheme, origin identity, complete template-shaping settings, TSJS content, and schema version all participate in an opaque SHA-256 cache key. Accept-Encoding does not: the stored template is decoded identity and the assembled result is encoded for each reader with Vary: Accept-Encoding. This assumes the origin's Accept-Encoding variants differ only by HTTP content coding, as normal compression negotiation does. Do not enable ESI for an origin that changes the document's meaning based on Accept-Encoding. Never put Cookie or Authorization in template_cache_vary; startup rejects both because raw cookie or credential values are not reader-neutral template dimensions. With origin_is_cookie_independent = false (the safe default), all cookie-bearing requests bypass. With it set to true, an origin Vary: Cookie still overrides the assertion and refuses storage. Every other name the origin emits in Vary must appear in the configured list; an uncovered name safely refuses template storage.

For a canary, inspect X-TS-Template-Cache. Its bounded values are hit, miss-stored, miss-store-error, miss-reserved, bypass-request, bypass-response, unsupported, invalid, and backend-error. No URL, header value, or cache key is exposed. invalid and backend-error fail open to a fresh origin response; they do not fail the page. The corresponding template_cache logs provide server-side observability for this path.

X-TS-Assembly identifies how the private response was assembled:

  • esi-parser — authorized cold miss assembled by the repaired parser;
  • byte-seam — warm template-cache hit using the streaming byte seam;
  • byte-seam-fallback — cold response safely assembled by byte seam because the platform parser was unavailable or rejected the document.

The two headers together are the reliable verification signal. Timing alone can vary with the origin, auction, compression, browser connection reuse, and local proxy buffering.

Rollback must preserve configuration compatibility:

  1. Change assembly_mode to inline and deploy/push that configuration.
  2. Before rolling back to a binary that predates these fields, remove assembly_mode, template_cache_vary, template_cache_max_age_seconds, and origin_is_cookie_independent, then push the cleaned configuration. Older binaries use deny_unknown_fields and intentionally reject unknown keys.
  3. Purge the Fastly surrogate key ts-template using the service's normal purge tooling, or wait for the bounded origin-derived lifetime to expire.

Run scripts/template-cache-local-test.sh esi before a rollout and scripts/template-cache-local-test.sh inline as its control. The harness uses a temporary manifest, never edits the tracked fastly.toml, verifies cold/warm origin counts and response integrity, and executes the generated GPT module against the served seam to require a real defineSlot call.

gam_unit_path templating

gam_unit_path is a template. A publisher whose ad unit varies by site section expresses that in one slot rule instead of one rule per (slot × section).

Supported placeholders:

PlaceholderResolves to
{network_id}gam_network_id
{slot_id}the slot's id
{section}non-empty path segment at section_segment (default: first; see below)

A template with no placeholders is used verbatim. A slot with nogam_unit_path falls back to /<network_id>/<slot_id>. Both preserve the pre-templating behavior, so existing static configs are unchanged.

Trusted Server conservatively caps the whole rendered dynamic path at 100 UTF-8 bytes, informed by Google's 100-character per-ad-unit-code limit. If a request-specific substitution would exceed the dynamic limit, only that slot is omitted before auction dispatch; the response itself still succeeds. Trusted Server logs a warning containing the slot ID and request path. Explicit static paths and absent/default paths retain legacy behavior and are not subject to this dynamic-only limit.

{section} derivation

{section} is derived from the request path at request time:

  • It is the non-empty path segment at section_segment (0-based, default 0). With the default, /news/article-123news. A site that prefixes a locale sets section_segment = 1, so /en/news/articlenews rather than en.
  • It is sanitized: each run of characters outside [A-Za-z0-9_-] becomes a single _, and the request-derived result is capped at 100 ASCII bytes.
  • Casing is preserved. Google documents GAM ad-unit codes as case-insensitive, so do not lowercase the value.
  • The path is used raw — it is not percent-decoded. So /new%20snew_20s (only % is disallowed; 2 and 0 are kept), never the decoded new_s. This keeps {section} consistent with how page_patterns match the same raw path.
  • When the path has no segment at that index — the site root (/, or repeated slashes), or a path shorter than section_segment{section} is section_root. So with section_segment = 1, the path /en renders the root section rather than reusing the locale.

section_root is required whenever any slot's template uses {section}, and must match [A-Za-z0-9_-]+. There is no default: the home-section name is publisher-specific. Startup fails if {section} is used without a valid section_root. Startup rejects a blank gam_network_id only when an absent path/default or a {network_id} template consumes it; static paths and templates without {network_id} do not consume it. A [creative_opportunities] block with enabled = false or no slots is inactive, so no publisher templates are delivered and its gam_network_id is not checked when no slot uses it.

Both knobs are config-driven, so the URL→section convention stays with the publisher: section_segment selects which segment names the section, and section_root names the section when there is none.

During typed/startup finalization, after templates parse successfully, every placeholder-bearing dynamic template that omits section_segment has section_segment = 0 materialized, so an older binary rejects the pushed blob loudly. Static and absent paths remain compatible with the legacy config schema only when both section_root and section_segment are omitted. Before rolling back below this feature, replace or remove dynamic paths, remove both section_root and section_segment, re-push and finalize the config, then roll back the binary.

Example resolution for gam_unit_path = "/{network_id}/example/{section}" with gam_network_id = "123456789", section_root = "home", and the page_patterns shown above:

Request pathgam_unit_path
//123456789/example/home
/news/123456789/example/news
/news/article/123456789/example/news
/reviews/x/123456789/example/reviews

The same config with section_segment = 1 and locale-prefixed patterns (["/en", "/en/news", "/en/news/*"]):

Request pathgam_unit_path
/en/123456789/example/home
/en/news/123456789/example/news
/en/news/article/123456789/example/news

An unmatched route — a path matched by no slot's page_patterns — produces no slot at all, so no template is rendered for it.

Startup validation rejects a malformed template: an unknown placeholder (e.g. {oops}), an unmatched or nested {, a stray }, or an empty gam_unit_path.

Fastly Runtime Config Store

After the EdgeZero cutover, the Fastly adapter always dispatches through the EdgeZero entry point. The former edgezero_enabled and edgezero_rollout_pct canary keys are no longer read.

The Fastly service must still provide a trusted_server_config config store because the entry point opens it before dispatch and passes the handle to EdgeZero-backed platform services. The store may be empty unless another feature adds keys to it.

Local development (fastly.toml):

toml
[local_server.config_stores]
  [local_server.config_stores.trusted_server_config]
    format = "inline-toml"
    [local_server.config_stores.trusted_server_config.contents]

Production setup (Fastly CLI):

bash
# Create the store once and attach it to the service.
fastly config-store create --name trusted_server_config

Rollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required.

Validation

Automatic Validation

Configuration is validated at startup:

Publisher Validation:

  • All fields non-empty
  • origin_url is valid URL

EC Validation:

  • The passphrase key name is non-empty at push time
  • The resolved passphrase is at least 32 bytes at runtime
  • Known placeholder values are rejected after resolution

Handler Validation:

  • path is valid regex
  • username is ordinary configuration and non-empty
  • The resolved password is non-empty and is checked for placeholders at runtime

Integration Validation:

  • Each integration implements Validate trait
  • Custom rules per integration

Validation Errors

Startup Failure if:

  • Required fields missing
  • Invalid data types
  • Regex compilation fails
  • Secret key is default value
  • Integration config fails validation

Error Format:

Configuration error: provider `pbs-main` endpoint must be an absolute HTTPS URL

Best Practices

Configuration Management

Development:

toml
# trusted-server.dev.toml
[publisher]
domain = "localhost"
origin_url = "http://localhost:3000"
proxy_secret = "publisher_proxy_secret"

Staging and production:

  • Provision the same key names in the target trusted_server_secrets store.
  • Keep only the key names in trusted-server.toml and environment overlays.
  • Push the config after provisioning and restart/redeploy after rotation.

Secret Management

Do:

  • ✅ Store values in the platform secret store
  • ✅ Rotate values deliberately and restart/redeploy instances
  • ✅ Generate values locally without printing them to logs
  • ✅ Use different values per environment when appropriate
  • ✅ Keep stable key names for rotation

Don't:

  • ❌ Commit secret values to version control
  • ❌ Put secret values in environment overlays
  • ❌ Put secret values in config diff output or app-config blobs
  • ❌ Treat missing secret-store keys as inline values
  • ❌ Use default/placeholder values
  • ❌ Share secrets across environments
  • ❌ Log secret values
  • ❌ Expose in error messages

File Organization

Recommended Structure:

trusted-server.toml          # Base config
trusted-server.dev.toml      # Development overrides
.env.development             # Dev environment vars
.env.staging                 # Staging environment vars
.env.production              # Production environment vars (not in git)
.env.example                 # Example template (in git)

.gitignore:

.env.production
.env.staging
.env.local
*.secret

Troubleshooting

Common Issues

"Failed to build configuration":

  • Check TOML syntax (trailing commas, quotes)
  • Verify all required fields present
  • Check environment variable format

"Configuration field '...' is set to a known placeholder value":

  • Confirm the referenced key exists in trusted_server_secrets
  • Ensure the resolved value is non-empty and not a known placeholder
  • Do not replace the key name with a plaintext value in the app config
  • Rotate the value in the platform secret store, then restart/redeploy

"Invalid regex":

  • Handler path must be valid regex
  • Test pattern: echo "^/_ts/admin" | grep -E "^/_ts/admin"
  • Escape special characters: \., \$, etc.

"Integration configuration could not be parsed":

  • Check JSON syntax in env vars
  • Verify indexed arrays (0, 1, 2...)
  • Check field names match exactly

Environment Variables Not Applied:

  • Run the override through ts config validate, ts config diff, or ts config push
  • Verify the target leaf already exists in trusted-server.toml; the pinned EdgeZero loader does not create missing fields
  • Verify prefix: TRUSTED_SERVER__
  • Check separator: __ (double underscore)
  • Confirm the variable is exported: echo $VARIABLE_NAME
  • Rerun ts config push after changing a deploy-time override
  • Try explicit string: VARIABLE='value' not VARIABLE=value

Debug Configuration

Print Loaded Config (test only):

rust
use trusted_server_core::settings_data::get_settings;

let settings = get_settings()?;
println!("{:#?}", settings);

Check Environment:

bash
# List all TRUSTED_SERVER variables
env | grep TRUSTED_SERVER

Validate TOML:

bash
# Use any TOML validator
cat trusted-server.toml | npx toml-cli validate

Next Steps

Released under the Apache License 2.0.