Repository navigation
Conversation
The configuration file spelled the same idea four different ways. An integration was switched on by `enabled = true` inside its own block, a demand source by a key in `[auction.providers]`, a permission signal by a `sources` list, and an Edge Cookie provider by a nested `[ec.providers.<name>]` block. Someone writing one of these files had four patterns to learn and no way to tell which fault a mistake would produce. The new page `docs/guide/configuration-rules.md` sets out the single rule, what Trusted Server checks and when, and why one syntax plus refusal at startup helps the people who run deployments and the people who write providers. It carries a complete worked example and a table from the previous layout to the new one. The rest of the documentation, the example configuration, the integration fixtures and the template-cache script follow the same convention, so nothing shows a reader a shape the code no longer accepts. Two claims are corrected rather than repeated. `ts config validate` does not run the Edge Cookie, location, device or permission signal selection checks, which live in the path only a running instance takes, so the page says plainly which checks happen at push time and which at startup, and that a passing validate is not proof that a change to those four will start. The auction orchestration guide no longer describes an ad server as a mediator.
Both halves of the provider convention land together, so the configuration file has one rule rather than two new ones. The integration half brings `[integration] provider`, which is the only switch an integration has. The demand half brings `[demand]` and `[adserver]`, and it takes APS and the ad server mock out of the integration table, because neither is a page integration. Where the two disagreed the resolution follows that split: the integration branch owns `[integration]`, its registry and its documentation pages, and the demand branch owns the ad server, APS and Prebid Server as implementations. Core reserves only the Prebid id now, since APS and the ad server mock reach the registry through the compiled plan rather than through `[integration] provider`, and the APS renderer registers from the demand sources the plan selected.
The adapter test still built an `[auction.providers]` entry, which the provider convention removed. It now selects an APS demand source the way a deployment does, so the test proves the renderer registers from the compiled plan rather than from a table that no longer exists.
Every adapter's startup test still built `[auction.providers]` entries, which the provider convention removed. Each now selects its sources the way a deployment does, so the tests still prove what they were written to prove: that the APS renderer route is registered from the compiled plan, and that Cloudflare and Spin refuse a plan with two demand sources because their HTTP clients run one request at a time. The Spin adapter gains serde_json as a development dependency, which is what writing a settings table in a test needs.
Brings the Edge Cookie provider block layout, where each provider has its own [ec.<name>] table and an optional implementation = "<id>" line, under the host-signal provider rename this branch already carried. Both changes are the same decision applied to different parts of one section, so the host-signal provider is now selected with [ec] provider = "host_signals" and configured in [ec.host_signals], not [ec.providers.host_signals]. Four files conflicted. crates/trusted-server-core/src/settings.rs. split/1 replaced the typed EcProviders struct with EcProviderBlocks, a map of the blocks written under [ec], in which only the hmac implementation keeps typed settings and everything else is held as the raw values an adapter reads. This branch had added host_signals as a second typed built-in beside hmac, so the resolution gives it the same standing in the new model rather than demoting it to raw values. EcProviderSettings gains a HostSignals variant, EcProviderBlock gains host_signals_settings, EcProviderBlocks gains host_signals_blocks beside hmac_blocks, read_provider_block reads a host_signals block into the typed config, and From<HostSignalsProviderConfig> builds the block. The block-required check in validate_provider_selection now covers both implementations built into core, because both take a passphrase. The placeholder-secret report names the block the operator wrote, as it already did for hmac. The refusal of the old host-signals spelling is kept and its message now sends the operator to [ec.host_signals]. That refusal moved to the top of validate_provider_selection and now covers a block left under the old name as well as the selector, because split/1's snake_case rule runs over the block names first and would otherwise refuse host-signals with the general rule instead of the message naming the spelling to write. The tests of both branches are kept, with this branch's host-signal test rewritten onto the new block layout. crates/trusted-server-core/src/ec/provider.rs. Both branches edited the resolution arms and the module documentation. build_provider keeps the host_signals parameter this branch added, because the host-signal provider needs a service only some hosts supply, and the host-signal arm of resolve_named_provider now reads its settings out of the block the selector names rather than a fixed field. BUILTIN_PROVIDER_KEYS keeps both entries under split/1's wording, which calls them implementation ids. The documentation keeps split/1's [ec.<name>] spelling together with this branch's statement that the host-signal provider is built per request. crates/trusted-server-core/src/config.rs. The registered secret path for the host-signal passphrase drops the removed providers segment and becomes ec.host_signals.passphrase. Push-time validation reads the host-signal blocks the same way it reads the hmac ones, and the reader that finds a built-in provider configured under a label of the operator's choosing now looks for either implementation, so a labeled host-signal block resolves its passphrase too. crates/trusted-server-core/src/config_payload.rs. Both branches added a key to the test secret store. Both are kept, being separate keys serving separate tests. Two follow-on fixes outside the conflicts. The tests this branch had written against the removed EcProviders type now go through a new select_host_signals_provider test helper beside select_hmac_provider, and the build_provider calls split/1 had reduced to two arguments take the host_signals argument again. Tests. cargo fmt --all -- --check is clean and cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings passes. The core suite passes 2,782 tests and 5 doc-tests with 4 ignored. The Axum adapter passes 43 tests across its four binaries, cargo clippy-axum is clean, and cargo check -p trusted-server-adapter-fastly --target wasm32-wasip1 builds.
…it/2 Brings up the Edge Cookie provider block layout from split/1, where each provider has its own [ec.<name>] table, together with split/2's rename of the host-signal provider to host_signals. Both are the same decision this branch applies to permission signals, which is that every provider name an operator types is snake_case and a provider type is a top-level table whose provider key selects what runs. Nothing conflicted. The permission signal work this branch carries sits in its own section and its own module, so the Edge Cookie section changes merged alongside it. One fix the merge needed. A test of the jurisdiction acknowledgment built its stateless case by deleting the hmac provider block from the test configuration by text, and it still named that block [ec.providers.hmac]. Under the new layout the block is [ec.hmac], so the deletion would have matched nothing and left a provider block configured with no selector, which the new layout rejects. The test now names the block it means. Tests. cargo fmt --all -- --check is clean and cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings passes. The core suite passes 2,874 tests and 8 doc-tests with 4 ignored. The Axum adapter passes 64 tests across its five binaries, cargo clippy-axum is clean, and cargo check -p trusted-server-adapter-fastly --target wasm32-wasip1 builds.
…rom split/3 Brings up the Edge Cookie provider block layout from split/1, the host_signals rename from split/2 and the permission signal provider key from split/3, under the client_fixed rename this branch already carried. All four are the same decision, so this branch's demonstration provider now sits in the same layout as the rest. Two files conflicted. crates/trusted-server-core/src/ec/provider.rs. Both branches added constants and resolution arms. The client_fixed constants and its resolution arm are kept, listed in BUILTIN_PROVIDER_KEYS beside the two providers that derive an identifier at the edge, under split/3's wording, which calls these implementation ids rather than names. The arm now matches on the implementation the selected block names rather than the selector itself, which is how the other arms read since the block layout landed, so the demonstration provider can be configured under a label as any other provider can. check_named_provider_configuration needed a real decision. This branch wrote it to answer two questions, which are whether the build compiles a name in and whether the name needs a settings block, the second by looking every name up in [ec.providers]. The block layout removed that lookup and moved the block question back into the settings, where it is now asked only of the implementations core knows take settings, because a provider an adapter injects has its block read by that adapter and core cannot say whether it needs one. Reinstating a block requirement for every name would undo that, so the function keeps only the question this branch added it for, which is whether the demonstration provider is compiled into this build, and the settings ask the block question. The function's own reasoning still holds for what it keeps, which is that only the resolution knows what is compiled out. The refusal of the old client-fixed spelling moved with it into the settings, beside the refusal of the old host-signals spelling, and both now run before anything else. They have to, because the block layout holds every provider name to snake_case and both old spellings break that rule, so leaving either refusal later meant an operator got the general rule instead of the message naming the spelling to write. Both refusals cover a block left behind under the old name as well as the selector, through one names_retired_provider helper. crates/trusted-server-core/src/settings.rs. The Ec struct keeps this branch's resolve_allowed_origins field and loses the providers field the block layout replaced. resolve_allowed_origins is a fixed key of the [ec] section, so it joins EC_SECTION_KEYS, without which the section would read it as a provider block. Four of this branch's tests followed the new layout. The one that proves a provider with no block validates now says why the settings no longer demand one, the old-spelling test builds its configuration through the [ec] section helper rather than by deleting the hmac block by text, the missing-block error names [ec.hmac], and the stale-block test builds its block through the shared test helper. Tests. cargo fmt --all -- --check is clean and cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings passes. The core suite passes 2,909 tests and 8 doc-tests with 4 ignored. The Axum adapter passes 64 tests across its five binaries, cargo clippy-axum is clean, and cargo check -p trusted-server-adapter-fastly --target wasm32-wasip1 builds.
…guides Brings up the Edge Cookie provider block layout from split/1, the host_signals rename from split/2, the permission signal provider key from split/3 and the client_fixed rename from split/4. All four are the same decision, which is that every provider name an operator types is snake_case, a provider type is a top-level table, and a provider key inside it selects what runs. Nothing conflicted. The response hook and the documentation set this branch carries sit beside the sections those renames touched. The documentation needed the work the clean merge did not do. These guides were written against the old layout, so they still told operators to write a table that is now refused and to select providers by spellings that now stop startup. The configuration guide, the Edge Cookie setup guide, the error reference, the Fastly guide and the key rotation guide all move from [ec.providers.hmac] to [ec.hmac], and the environment override for that passphrase becomes TRUSTED_SERVER__EC__HMAC__PASSPHRASE, which is the path the settings now hold it at. The configuration guide's [ec] reference needed more than a rename. Its rule that every selection must have a matching block is no longer what the code does, because a provider has a block only when it has settings, so it now says which providers need one and which do not, and it explains the implementation key that lets a block carry a label of the operator's choosing. Its provider list and its validation notes follow the snake_case names, and the resolve_allowed_origins key this stack added to the [ec] section is documented alongside the rest. The Edge Cookie guide named the host-signal and demonstration providers by their old spellings throughout, including in a diagram label, and now names them host_signals and client_fixed. The cargo feature that compiles the demonstration provider in keeps the name client-fixed-demo, because a cargo feature name is not a configuration value. The example configuration's commented-out host-signal block carried both the old table and the old name, and is now [ec.host_signals] selected by provider = "host_signals". Tests. cargo fmt --all -- --check is clean and cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings passes. The core suite passes 2,909 tests and 8 doc-tests with 4 ignored. The Axum adapter passes 64 tests across its five binaries, cargo clippy-axum is clean, and cargo check -p trusted-server-adapter-fastly --target wasm32-wasip1 builds.
This brings the rest of the stack onto the seam branch, so every provider type in the file now follows one rule rather than the demand and ad server pair following it alone. Where the two sides disagreed, each kept what it owns. The Edge Cookie side owns `[ec.<name>]`, the optional `implementation` line and the label-aware passphrase handling, so the secret paths become `ec.hmac.passphrase` and `ec.host_signals.passphrase` and a block written under a label of the operator's choosing has its key name read out of the configuration rather than from a fixed path. The seam side owns `[demand]`, `[adserver]` and `[integration]`, the ad server vocabulary and the corrected note that `ts config validate` does not run the provider selection checks. The adapters keep this branch's field name for the Edge Cookie provider they resolve per request, and take split/5's corrected selector name for the permission signals, which is `provider` rather than `sources`. Three test fixtures still wrote `[ec.providers.<name>]`, which startup now refuses, so they are restated in the new shape.
Four sentences in the guides joined their clauses with a semicolon, and one table row grew past its column. They now read as one sentence each, and the table is realigned, so the pages follow the house style the rest of the guide follows.
The seam promised that an implementation decides only its field policy, its own extensions, its outbound headers and how its responses are read, while the driver builds every standard field and enforces privacy. The two request hooks did not hold that promise structurally. augment_request received the whole OpenRTB request, so an implementation could rewrite any standard field the driver had built, and prepare_outbound received the whole outbound request, so it could replace the body. augment_request now receives the extension objects and nothing else, one for the request and one for each impression, each impression beside the routed slot it was built from. That removes the zip by position that Prebid Server used to pair impressions with slots, which a slot with no valid format could have misaligned. prepare_outbound now receives the outbound headers alone, and the driver captures the headers as they will be sent, after the implementation has added its own. The driver also refuses a request whose extension claims ext.trusted_server, because that object is the driver's signature and identity block and no implementation may write it. The plain OpenRTB implementation already refused it in its static extensions at startup, and now the driver holds the line for every implementation.
… the code does The configuration rules page promised that every provider rejects a setting it does not know, and nine of the thirteen page integrations did not. A misspelled key in a Didomi, Google Tag Manager, GPT, asset proxy, Lockr, Permutive, Sourcepoint, Testlight or Next.js block was ignored. Each of their settings types now refuses unknown keys, and a test walks every built-in page integration to prove a planted key fails deploy validation naming the integration and the key. The rules page and the CHANGELOG now say only what the code does. The push-time list names the refusal of an unselected block, of an enabled key left behind and of the removed [integrations] table, rather than a typo in a switched-off block being caught, which under the new selector cannot exist. The startup list says that a module declaring an identity or device provider no type selects is logged, which is what the registry does, rather than reported. The two overlapping CHANGELOG entries for the convention and for the integration selector become one, carrying what the second said that the first did not. The rest is the sweep before these changes go public: the mediator rename had left "a ad server", "ad server=none" and a mangled function name in comments, an environment example carried a stray separator, the Prebid table header in the example configuration had lost its line break, and the getting-started guide still sent readers to the [auction.providers] table this stack removed. Em dashes and clause-joining marks in lines this stack added are gone, the loopback endpoint test uses a documentation address rather than a private one, and the documentation tables are realigned.
# Conflicts: # docs/guide/configuration.md
The fixture selecting a vendor provider this adapter does not inject writes its settings in [ec.acme], as every provider block is written since the tables moved under [ec], and the comment above it still named the removed [ec.providers.<key>] table. The comment now says what the fixture does, in the words the other branches of this series already use.
…te's example Two audit tests counted the string [[integration.js_asset_proxy.assets]] in the draft configuration to prove how many asset entries the audit wrote. The draft is built on the example configuration, which now documents that array as a commented example, so the count included the example and the two tests failed on Linux, which is where CI runs them. The first test now reads the draft as TOML and counts the entries in the array the audit wrote, which is what it meant to check, and the second asserts that no uncommented line opens that array. Both stay silent about the example, because a commented line configures nothing.
… writes The environment overlay tests built their project by adding an [auction.providers.pbs-main] table and an example-bidder route naming it to the integration fixture, which is the layout this series removed. Every test that pushed or validated that project failed on Linux, where CI runs the CLI job, because [auction.providers] is refused with the move spelled out and the hyphenated name is not snake_case. The fixture already carries [demand.pbs_main] with its endpoint and the [auction.bidders.example-bidder] route that names pbs_main, so the project no longer adds a second copy under the old names. The endpoint overlay now addresses TRUSTED_SERVER__DEMAND__PBS_MAIN__ENDPOINT and the pushed envelope is read back from the demand table, and the legacy provider list test asserts the refusal names [demand] provider, which is where the message sends an operator, rather than the CHANGELOG, which it does not mention.
Three fixtures in the Fastly adapter's own tests still described the auction the old way, two with an [auction.providers.prebid] table and one with an empty providers map, which this series refuses with the move spelled out. Those tests run only under Viceroy on wasm32-wasip1, so no native gate reached them and the first of them crashed the CI job. The two Prebid fixtures now select a demand source named prebid whose implementation is prebid_server, with the same endpoint and timeout, and the empty map is gone because an empty demand list is the default. All 192 tests in the adapter's binary pass under Viceroy 0.17.0.
Brings in the eight commits main gained since this branch was cut, of which three touch the same code as the provider seam. The parser-aware body hold gives every adapter a second entry point, `build_state_with_services`, so a caller can supply the `RuntimeServices` each request uses. The composition-root check the seam added now runs in that function rather than in `build_state_with_settings`, and it is given whatever provider those supplied services already carry instead of always `None`, so a caller that resolved one is not made to resolve it twice. On Cloudflare and Spin the state keeps both the provider this branch resolves once at start-up and the services main lets a caller supply. The free function this branch added is gone and its one job, handing the resolved provider to every request, moved into `services_for_request`, which is the method main introduced for the same purpose. The core README this branch corrected one line of has been rewritten wholesale by the documentation refresh, and the section that line was in no longer exists, so main's version is taken as it stands.
Carries up the resolutions from split/1, where main's second entry point, `build_state_with_services`, meets the composition-root provider check. This branch had already given that check and `build_reusable_provider` a `host_signals` argument, so each call now passes `None` for host signals and whatever provider a caller's supplied services already carry. The free function that handed the resolved provider to every request is gone on both adapters and its job sits in `services_for_request`, the method main added, which passes the settings this branch made `build_runtime_services` take.
Carries up the resolutions from split/2 and settles what main's own changes mean for the permission model. The adapters keep both the permission signal providers this branch resolves at start-up and the services main lets a caller supply, and `services_for_request` passes the providers to `build_runtime_services`, so a request served from supplied services and one served from the request context see the same schemes. Main added tests for `allows_ec_creation`, `has_explicit_ec_withdrawal` and `gate_eids_by_consent`, all three of which this branch replaced with the permission model, so those tests name functions that no longer exist and have not been taken. The behaviour the new one covers, stripping every EID when personalised advertising is not permitted, is already asserted here by `gate_eids_strips_eids_when_a_required_permission_is_unset`, which reaches it through the resolved permissions rather than through the consent context. The no-op HTML post processor in the registry tests goes with `IntegrationHtmlPostProcessor`, the trait main removed in favour of per-document stream processors.
Carries up the resolutions from split/3. Nothing on this branch touches the code main changed, so the merge needed no decision of its own.
Carries up the resolutions from split/4. Main's documentation refresh rewrote the section table in the configuration guide, so its version is taken with this branch's sections added in alphabetical order. That is device and geo, which the old table listed, and permission_signal, which it never did.
The per-request Edge Cookie test builds AppState by hand, and the merge left it without the field the supplied-services path added. It drives the per-request path, so it supplies none.
Main added a Next.js auction fixture that every adapter now shares. This branch refuses a document that configures an Edge Cookie provider without saying where its readers are, because otherwise every request resolves at the top of the rules tree, so the fixture acknowledges single-jurisdiction operation the way the other fixtures on this branch do.
The host selector refuses a label that starts or ends with a hyphen, with a test. The guide says each key is pushed in lower case and that an IPv6 literal host is refused. The thing a host selects is called an app-config blob throughout, including the refusal log line the harness greps. Comments that restated a test, the guide or another comment are cut.
An operator's response_headers replaced headers of the same name as the response was finalized, so a site-wide Content-Security-Policy removed the sandbox from /_ts/data and a page that opened it could read the record. The data page now marks its headers as required, and core sets them again after the operator's headers on every adapter.
# Conflicts: # crates/trusted-server-core/src/auction/endpoints.rs # crates/trusted-server-core/src/ec/finalize.rs # crates/trusted-server-core/src/test_support.rs
# Conflicts: # crates/trusted-server-adapter-fastly/src/main.rs # crates/trusted-server-core/src/test_support.rs
main's new EID persistence gate is the permission model's sharing check. EIDs are written to the identity graph only when the storage and personalised-ad permissions are both set, the pair that already gates the bid request, so allows_eid_persistence and its parity test are removed and the tests that described Purpose 4 denial state the permissions instead.
main's rule that partner EIDs are written to the identity graph only with consent for personalised ads is stated in the guides as the storage and personalised-ad permissions, the pair the permission model already uses for sharing.
…d run the Cloudflare auction test with no demand modules The build script's TRUSTED_SERVER__GIT_VERSION reaches test processes and the strict loader refused it as an unknown field. Cloudflare runs one demand source at a time, so the fixture's two demand modules stopped worker startup.
jwrosewell
marked this pull request as ready for review
October 14, 2026 12:35
This was referenced Oct 14, 2026
Collaborator
|
Moved into this repository as #1352, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on page changes as middleware (#1295). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/10-page-middlewarewithsplit/11-config-by-hostto see only this pull request's change.
One Fastly service can serve several publishers, each from an app-config blob of its own. A
__KEYselector that contains{host}names a blob for each host, and each request reads the blob stored under the host it names.What this pull request does
The entry point puts the request's host in the placeholder's place before anything reads settings, so the debug probes, the application build and the finalizers all read the blob of the publisher the request is for. With
{host}set, every request opens the config store and reads one entry before anything else, the counters and JA4 probes included. Only/healthis answered first. Each key is pushed in lower case, because the host is lowercased and--keystores the key as typed.www.publisher-a.examplewww.publisher-a.exampleWWW.Publisher-A.example:443orwww.publisher-a.example.www.publisher-a.examplewww.publisher-b.examplewww.publisher-b.example421 Misdirected Request, not to be storedHostthat is not a host name, an IPv6 literal, or noHost421 Misdirected Request/health, for any host200, before any config is read500A selector without the placeholder is the key as it stands and nothing is looked up. A service that reads one blob is unchanged.
What keeps one publisher's settings from answering for another
421Hostheader is written to name another key in the store, such as the logical store ID or a chunk of a blob500and not servedThe reusable sandbox
Sandboxwaslifecycle::Sandbox<RetainedApp>, one application for the life of the sandbox. It islifecycle::Sandbox<RetainedApps>, which keeps each application under its blob's key.Sandbox<T>holds one value, builds it only when empty and hands it out by shared reference, with no way to replace it. So the holder is made once and its list sits behind aRefCell. One callback runs to completion before the next begins and no borrow is held across a build.421once the store no longer holds it, even while a sandbox still keeps its application, because the store is asked on every request.What the publishers on one service share
Why
A Fastly service reads one app-config blob, at the key its
__KEYselector names or at the logical store ID. An operator who runs Trusted Server for several publishers therefore runs a service for each, with its own domains, stores, deploys and rollbacks, although the code is the same and only the settings differ.Two things stood between that and one service reading a blob for each host.
The placeholder goes in the selector that already exists, and not in a switch of its own beside the sandbox bounds, because of EdgeZero's staged deploy. A staged deploy copies every runtime entry of the service into a store of its own and replaces only the config key selectors, pointing each at
<logical-store-id>_staging(staging_entries_from_production). A separate switch would be copied with the rest, and a staged version would then choose by host and read the production blobs. The selector is replaced, so a staged version reads the one staging blob for every host and never a production one.The selector is also read by
runtime_env_config, whose lookup panics on an error. A selector that cannot be read therefore fails the request, where a switch read on the side would have had to choose between failing and falling back to the logical store ID.How it was verified
CI passed on the fork on the head,
ba4008fe7. CI has not run here, because the branch conflicts withmain.Closes #1270.