Skip to content

Open the integration seam so a vendor module can live outside core - #1094

Open
jwrosewell wants to merge 208 commits into
IABTechLab:mainfrom
jwrosewell:split/7-integration-seam-impl
Open

jwrosewell wants to merge 208 commits into
IABTechLab:mainfrom
jwrosewell:split/7-integration-seam-impl

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

This description has been edited. The pull request now lands one
configuration convention for everything a deployment can switch on, and the
auction side is inside it. If you read an earlier version of this text, the
sections worth re-reading are "One convention for every provider", "The
auction side becomes provider types", "Why this rather than what main does
today" and the migration table. The rest is the same work described the same
way.

What changed in the pull request since the last version of this description:

  1. The ad server, plain OpenRTB, Prebid Server and APS became provider types
    that an integration registers, rather than names written into core. The
    earlier text said the auction was not opened. That is no longer true.
  2. [auction.providers] became [demand] and [auction] mediator became
    [adserver]. profile became implementation, and what sat in
    profile_config now sits flat in the provider's own table.
  3. The word mediator is gone. It is "ad server" in prose and adserver in
    configuration, and the auction response metadata that read
    parallel_mediation now reads parallel_adserver.
  4. An integration is switched on by being named in [integration] provider
    rather than by enabled = true inside its own block, and the table is
    [integration] rather than [integrations].
  5. [ec.providers.<name>] became [ec.<name>], and
    [permission_signal] sources became [permission_signal] provider.
  6. The demand seam's two request hooks hand an implementation only the
    parts of a request it owns, the extension objects and the outbound
    headers, rather than the whole request, and the driver refuses an
    implementation that writes ext.trusted_server.
  7. Every page integration refuses a setting it does not know, which the
    configuration rules page promised and nine integrations did not do.
  8. Every name a person types is snake_case, so host-signals,
    client-fixed, gpp-sale-opt-out and us-privacy became host_signals,
    client_fixed, gpp_sale_opt_out and us_privacy.
  9. A new reference page,
    Configuration Rules,
    states the convention, what is checked before a request is served, and how
    to move a file from the previous layout.

There is no deprecation period for the auction keys. A configuration still
carrying [auction.providers] or [auction] mediator is refused at startup
with a message naming where the setting moved to.

Stacks on the provider documentation set (#1047), the last of the five pull
requests that decompose #838, so this pull request sits on the whole provider
series. It also depends directly on the EdgeCookieProvider and
DeviceProvider traits from the Edge Cookie provider seam (#1043) and the
device and location seam (#1044), whose providers a registration can now carry,
and on the permission model (#1045), where an unset [geo] provider makes no
host geo call. The stack has six pull requests
(#1043, #1044, #1045, #1046, #1047, #1094), each targeting main, with this one
sixth.
Compare split/5-response-hook-docs with split/7-integration-seam-impl
to see only this pull request's change.

The design specs for the series are carried by the spec pull request (#1084),
which has no code. This pull request implements
2026-08-27-integration-provider-seam-design.md,
where sections 3.1 to 3.6 define the seam and section 8 records what
implementing it found, so read #1084 first and this pull request as the answer
to it.

What this does and why

trusted-server-core
carries nine vendor integrations as core code because every place an
integration plugs in is closed. The builder table is private, browser
JavaScript is fixed at compile time, deploy validation names each vendor's
configuration type, the auction names its vendors in core, and two vendors
reach into core through named types. Every new vendor is therefore another core
change, most recently the LiveRamp module (#1054).

This pull request opens those places. An integration can now ship in its own
crate with its Rust, its browser script, its configuration type, its deploy
rules and its tests, and an adapter composes that crate into a deployment at
startup without core naming the vendor. No existing integration changes
behavior, the served script keeps its exact bytes and its ?v= hash, and the
built-in set still registers through the same path a vendor crate would use.

What is now composable:

Seam How a crate outside core reaches it
Integration registration IntegrationBuilder::new(id, source, build, validate), passed to an adapter's routes_with_registrations, which builds the registry with IntegrationRegistry::with_plan_and_registrations
Browser script .with_js_module(CarriedJsModule { source, sha256 }) on the registration
Deploy validation the builder's validate function, run by validate_settings_for_deploy_with
Request preparation .with_request_preparer(...), run by IntegrationRegistry::prepare_request
Identity, location and device .with_ec_provider(...), .with_geo_provider(...) and .with_device_provider(...), selected by [ec] provider, [geo] provider and [device] provider
Auction demand .with_demand(...), selected by [demand] provider
The ad server .with_adserver(...), selected by [adserver] provider

One convention for every provider

Everything a deployment can switch on is a provider, and every provider type is
configured the same way.

[<type>]
provider = "<name>"          # a string where one runs, a list where several run

[<type>.<name>]              # only when that provider has settings
setting = "value"

The types are ec, geo, device, permission_signal, demand, adserver
and integration. A type that runs one provider takes a string, a type that
runs several takes a list, and a provider with nothing to set needs no table at
all. A table's name is the implementation it configures, unless the table
carries an implementation line, which is how two Prebid Servers run side by
side under names a deployment chooses for itself. Every name a person types is
snake_case.

Four things stop a deployment rather than being tolerated:

  • A [<type>.<name>] table that its type's provider does not name.
  • A provider entry, or an implementation line, naming an implementation
    this build does not have. The message lists the ones it does have.
  • A setting the chosen provider does not know, because every provider rejects
    keys it does not recognize.
  • A demand or adserver endpoint that is not HTTPS. Plain HTTP is allowed
    only to 127.0.0.1, ::1 or localhost, so a local test stack runs without
    certificates and nothing leaves the machine unencrypted.

The rules that hold for every type live in one place,
crates/trusted-server-core/src/provider_table.rs,
so a new provider type inherits them instead of writing them again. The whole
convention, the checks and the move from the previous layout are written up for
operators in
docs/guide/configuration-rules.md.

The auction side becomes provider types

The ad server, plain OpenRTB, Prebid Server and APS are now implementations
that an integration registers through .with_demand(...) and
.with_adserver(...) in
crates/trusted-server-core/src/integrations/mod.rs,
selected by [demand] provider and [adserver] provider. The four that ship
here, in
openrtb.rs,
prebid_server.rs,
aps.rs
and
adserver_mock.rs,
go through the same registration a vendor crate uses.

What an implementation may decide is stated once, in
crates/trusted-server-core/src/auction/demand.rs.
A shared OpenRTB driver builds every standard request field, sends the request
and enforces the privacy rules, and an implementation chooses only its field
policy, its own extensions, its outbound headers and how its responses are
read. The compiled plan in
crates/trusted-server-core/src/auction/plan.rs
resolves each selected name to a registered implementation, so
crates/trusted-server-core/src/auction/profile.rs and its fixed list of three
profiles are deleted.

[auction] keeps the settings that belong to the auction itself, being
enabled, the whole-auction timeout and creative handling, with
[auction.bidders.<code>] provider naming the demand source a browser bidder
code is sent to.

The provider interfaces are asynchronous, and providers get the services

Before this change every provider trait method was synchronous while every
platform service was asynchronous, and a provider was handed no services at
all, so a provider that needed to call a backend, read a key-value store or
fetch a secret could not be written. The methods that do the work on all three
provider interfaces (EdgeCookieProvider::generate, DeviceProvider::detect
and PlatformGeo::lookup) are now asynchronous and take &RuntimeServices,
and so is resolve_from_client, which has to verify what the browser posted.
Every implementation was converted, the built-in ones included. Finalizing the
Edge Cookie response is asynchronous too and takes the services, because
main's orphaned identifier recovery asks the selected provider for a
replacement identifier.

The traits keep their Send + Sync bound and use #[async_trait(?Send)], as
PlatformHttpClient in this repository already does, so a provider stays safe
to share while its future stays on one thread.

Two tests in
crates/trusted-server-core/src/ec/mod.rs,
a_provider_reads_a_platform_service_through_the_services_it_is_given and
a_geo_provider_reads_a_platform_service_through_the_services_it_is_given,
drive a provider that reads a value from the configuration store it is handed,
through the production path, and assert the value reaches the output. They
exist because once everything compiled and passed, nothing anywhere read the
services parameter, and a parameter no code exercises is not a working seam.

Two supporting changes

The bid renderer is no longer an enum with a single APS variant. It is a type
tag plus the payload the demand provider supplies, serialized flat so the
response a page receives is byte for byte what it was, and ApsRendererV1
moves into the APS integration. DataDome's cache and origin-path marker becomes
a neutral PersonalizedResponse request extension that any integration may
set, so core acts on the marker without knowing which integration asked for it.

One design decision made while stacking this on #1045

Two designs for [geo] met when this branch moved onto the stack, and they
disagreed about what an unset selector means. The permission model (#1045)
treats unset as resolving nothing and making no host geo call, and this branch
had treated unset as leaving the host's own lookup in place. The permission
model's meaning is kept, because a deployment that has not asked for a host geo
lookup should not be making one. Unset and "none" therefore both resolve to
DisabledGeo, "platform" is the explicit opt-in to the adapter's own lookup,
and any other value names a module that declared a location provider.

Where to start

  1. crates/trusted-server-adapter-axum/tests/seam_probe.rs.
    Every seam is driven end to end from a crate core does not know about,
    through the Axum adapter's real router. The tests turn on observable
    outcomes, being the bytes served, the JSON a route returns and the error a
    startup or deploy check produces, rather than on a function having been
    called. Start here, because a seam is only proven by an implementation that
    is not the built-in one.
    ec_provider_generates_an_identifier_with_the_modules_prefix shows the
    module's own Edge Cookie provider ran, because its identifier starts
    seam-probe-, which the built-in HMAC provider cannot produce, and
    ec_selector_naming_a_module_starts_the_adapter starts the Axum router with
    that provider selected.
  2. crates/integrations/seam-probe/src/lib.rs,
    the fixture those tests drive. It is a workspace member and a dev-dependency
    of the Axum adapter only, so no production build reaches it. One
    registration carries a browser module, a proxy route, a request preparer,
    location, Edge Cookie and device providers and its own configuration block.
  3. crates/trusted-server-core/src/provider_table.rs,
    the table every provider type shares, and
    auction/plan.rs,
    which resolves [demand] and [adserver] names to registered
    implementations and reports an unknown one by listing the ones this build
    has.
  4. crates/trusted-server-core/src/integrations/registry.rs,
    with_plan_and_registrations. Building the registry refuses a duplicate id,
    naming both sources. It checks the SHA-256 of a carried browser module, and
    resolves [ec] provider, [geo] provider and [device] provider against
    the modules that declared each capability.
  5. crates/trusted-server-core/src/tsjs_bundle.rs,
    new. Composition of the served script moves out of trusted-server-js and
    into core, keyed on content rather than on ids, because a module a vendor
    crate carries is not in the compile-time map. The byte rule is unchanged,
    being core first then each part joined by ;\n. The tests in that file
    compare the composed bytes and hash against
    trusted_server_js::concatenate_modules and
    trusted_server_js::concatenated_hash, which is the evidence that no
    current ?v= value moves.
  6. crates/trusted-server-core/src/config.rs.
    The hand-written list of vendor configuration types is replaced by a loop
    over the builders, and the auction plan is compiled and checked on the same
    path, so a bidder route naming a demand source [demand] provider does not
    select is caught before a push.
  7. crates/trusted-server-core/src/auction/types.rs,
    BidRenderer. This is the riskiest single change, because the wire shape
    {"type":"aps", ...} has to survive byte for byte.
    aps_renderer_serializes_to_versioned_camel_case_contract pins the
    serialized form and renderer_bid_id_key_matches_the_serialized_form pins
    the one field the publisher reads by key.

crates/trusted-server-core/build.rs
is also new. The migration guard used to list core source files by hand with
include_str!, so a vendor moving out of core would break the build rather
than a test. The list is now generated from the source tree, so a file that
leaves core leaves the guard with nobody editing anything.

Why this rather than what main does today

Core names vendors it has no business knowing.
crates/trusted-server-core/src/auction/plan.rs on main
holds MOCK_MEDIATOR_ID = "adserver_mock" and refuses any other ad server. It
special-cases the profile ids prebid-server and aps by name when it
compiles a plan, and
crates/trusted-server-core/src/auction/profile.rs on main
holds the three profiles a deployment may choose, being standard,
prebid-server and aps, as a fixed list with a closed enum behind it. A
vendor cannot add a demand source or an ad server without a pull request
against core, which is the cost this series exists to remove.

The same idea is spelled four different ways. An integration is switched on
by enabled = true inside its own block. A demand source is switched on by a
key appearing in [auction.providers]. A permission signal is switched on by
its name appearing in a sources list. An Edge Cookie provider is switched on
by [ec] provider naming a nested [ec.providers.<name>] block. Two of those
four are on main today. The other two arrived inside this stack, in the Edge
Cookie provider seam (#1043) and the permission model (#1045), which shows how
quickly a fifth spelling appears when no rule says what a spelling looks like.
One convention now covers all of them, and the next provider type gets the same
one without anyone deciding again.

For the people who run a deployment, one convention plus refusal at startup
changes what a mistake costs:

  • A table left behind after a provider is switched off is caught rather than
    quietly ignored. On main an [integrations.datadome] block that outlives
    its integration keeps every setting it had, and nothing tells the next reader
    whether those settings are live. A leftover [auction.providers.pbs-old]
    entry is worse, because on main the entry is what switches the demand
    source on, so a stale one is a live bidder nobody meant to keep. Now a table
    has to be named by a provider line or the deployment does not start.
  • A misspelled setting fails instead of being dropped. Every provider rejects
    keys it does not know, so timeout_millis where the setting is timeout_ms
    stops the deployment rather than silently leaving the default in place, which
    is the kind of fault that otherwise turns up much later in a latency graph.
  • Switching a provider is a one-line change that reads plainly in review.
    provider = ["pbs_main"] becoming provider = ["pbs_main", "aps_main"] says
    what happened. On main the same intent is a new nested block, a profile
    string, a profile_config object and a protocol key, and a reviewer has to
    reconstruct the outcome from four places.
  • An endpoint cannot quietly be plain HTTP. Demand and ad server endpoints must
    be HTTPS, or HTTP to a loopback host for a local test stack.
  • An old file is not half accepted. A configuration still carrying
    [auction.providers] or [auction] mediator is refused, and the error names
    the table the setting moved to instead of leaving the operator to search for
    it.

For the people who write providers, a provider plugs in through one
registration and inherits every check above without writing any of them. Core
code names no vendor, so shipping a demand source, an ad server, an identity
provider, a location provider or a device provider means publishing a crate,
not editing core and waiting for a core release. The seam probe proves this
from the outside, because it is a crate core does not know about and it reaches
every one of those seams through the real router.

What this costs. These are breaking changes, and the auction keys get no
deprecation period. That is a deliberate choice for a major release, taken
because the alternative is carrying two spellings of every selector
indefinitely, and because a configuration refused with a message naming the fix
is cheaper to migrate than one that half works.

Moving from the previous layout

Previous Now
[integrations.<id>] with enabled = true <id> in [integration] provider, and [integration.<id>] only for settings
[ec.providers.<name>] [ec.<name>]
[permission_signal] sources [permission_signal] provider
host-signals, client-fixed, gpp-sale-opt-out, us-privacy host_signals, client_fixed, gpp_sale_opt_out, us_privacy
[auction.providers.<id>] with protocol, profile and profile_config [demand] provider and [demand.<name>], with implementation and the settings flat in the table
profile = "standard" implementation = "openrtb"
[auction] mediator = "adserver_mock" and [integrations.adserver_mock] [adserver] provider = "adserver_mock" and [adserver.adserver_mock]
[integrations.aps] rendering_mode rendering_mode in the [demand.<name>] table of the aps provider
[debug.auction_html_comment_options] include_mediator_response include_adserver_response

The word mediator goes with those keys. It is "ad server" in prose and
adserver in configuration, and the auction response metadata that read
parallel_mediation now reads parallel_adserver.

A file in the old shape is not read on a best-effort basis. [auction] providers and [auction] mediator are both refused with a message naming
where the setting moved to.

How it was verified

The head this pull request shows now is ede61f685. The gates below were run
locally on 6a7cbb91d, and the four commits after it change only test code
and one comment, each verified by the gate that reaches it.

On Windows, cargo fmt --all --check, Clippy with warnings denied on the
Fastly, Axum, Cloudflare and Spin adapters on their native and wasm targets
and on the codegen and parity crates, the core suite (2,982 tests and 18
doctests), the Axum, Cloudflare and Spin adapter suites, the seam-probe
fixture's tests, the cross-adapter parity suite, the Fastly release wasm
build, and the docs Prettier check, ESLint and VitePress build with the
pinned Prettier all pass. The CLI crate does not build on a Windows host,
because a dependency uses an unstable Windows feature, so its tests and its
Clippy job ran under WSL, where they pass at 80 unit tests and 7 environment
overlay tests. The Fastly adapter's own tests run only under Viceroy on
wasm32-wasip1, where they pass at 192, and the device, geo, OpenRTB and
JavaScript crates pass under Viceroy as well.

Three of those runs found fixtures the native gates could not reach and that
this pull request had left on the old layout: two ts audit tests counted a
commented example the template now carries, the CLI environment overlay test
still built its project on [auction.providers], and three Fastly fixtures
did the same. Each is fixed in a commit of its own on this head.

On CI, every check passes on ede61f685 except CodeQL:
cargo test,
the Axum tests on Ubuntu,
the CLI tests,
format-docs,
the integration tests
and
vitest.
CodeQL
reports one high alert on a log line in the orchestrator that names the ad
server and its timeouts. The query reaches that line by tracing a test
helper's secret store into the log, and the line logs neither request data
nor a secret, so the alert stands as reported rather than being suppressed.

The CI workflow runs the seam-probe fixture crate's own tests, but no clippy
alias or workflow step names that crate, so its source is not linted in CI.

What is not in this change

The design in #1084 is wider than this pull request. Every part of it this pull
request does not deliver is listed here.

No implementation has physically moved out of core yet. The auction, Edge
Cookie, location and device seams are open and the built-in implementations go
through them, but they still ship inside
crates/trusted-server-core.
Each vendor moves in its own pull request after this one, which is the point of
doing the seam once.

Nothing is refused for a missing host signal (section 3.6). A registration
declares no host-signal requirements, so a provider that needs a signal the
running adapter does not expose is not turned back at startup.

The acceptance round trips run on Axum, not Fastly (section 6, items 1 and
2).
Both items ask for the round trip on the Fastly adapter, and the tests
here run through the Axum adapter, because the Fastly adapter cannot yet take a
vendor crate (below). Item 1 also asks that a module's hooks run in the right
order, and the tests show the preparer runs before routing and exactly once,
but no test checks the order of several hooks on one registration.

There is no finalize hook (section 3.5). The prepare half is done, and
IntegrationRegistry::prepare_request replaced the direct
gpt_diagnostics::prepare_request calls in all four adapters. The finalize
half is not. The registration builder has no finalize method, and the GPT
diagnostics decision is still a named type in
html_processor.rs
and
publisher.rs,
so a GPT move needs that hook first.

A module's own deploy rules still run nowhere an operator can reach (section
8, items 1 and 6).
ts config validate and ts config push go through
TrustedServerAppConfig, which calls validate_settings_for_deploy with no
extra builders, so only core's rules run on the path an operator uses. The
registry calls a builder's build function and not its validate function, so a
vendor whose checks live in validate has them enforced on no path unless the
deployment's own code calls validate_settings_for_deploy_with with that
builder. The seam probe works around this by repeating its check inside its
build function, which every vendor would have to copy.
docs/guide/integration-guide.md
records the gap, and it needs a decision, either the CLI is built per
deployment with its vendor crates, or the registry runs validate when it
builds.

A Fastly deployment still cannot take a vendor crate (section 8, item 7).
The Axum, Cloudflare and Spin adapters expose build_state_with_registrations
and routes_with_registrations publicly. The Fastly adapter's
build_state_with_registrations is pub(crate) and the crate has no library
target, so composing a module into a Fastly deployment means editing that
adapter. Fastly is the primary deployment target, so this gap decides whether
the seam is usable in production or only in the dev server, and closing it
should come before the first vendor is asked to use the seam.

A provider can still be resolved more than once per request (section 8, item
3).
On POST /auction location is looked up to build the Edge Cookie context
and again in handle_auction, and the seam probe's proxy route calls its
location provider as well. This pull request adds no per-request provider
context, so a module sharing one backend across identity, location and device
has nowhere to hang a single call per request.

Request preparers still cover different routes on each host (section 8, item
5).
Replacing the vendor-named call with the registry call did not make the
covered routes the same. Axum runs preparers once before routing, Cloudflare in
its shared handler wrapper and its fallback dispatch, Fastly after its early
admin dispatch and in its fallback, and Spin only in the auction handler, the
page-bids handler and the fallback. A module that strips its own reserved query
or cookie is therefore protected on a different set of routes depending on the
host.

One core reader still reaches into an APS payload (section 8, item 4). The
hb_adid fallback in
publisher.rs
reads the APS renderer's bid id. It goes through the neutral accessor
BidRenderer::payload_field, which reads one field instead of cloning the
whole payload, but publisher.rs still imports APS_RENDERER_TYPE and
APS_RENDERER_BID_ID_KEY, so the APS move needs a neutral answer for that
fallback and not only the renderer contract.

Core TypeScript still imports APS directly (section 8, item 8).
auction.ts
and
types.ts
import and name the APS renderer, so generalizing the Rust renderer does not by
itself move APS out.

DataDome is still named in core outside its own integration.
publisher.rs
reads DataDomeClientTagSuppressed and
html_processor.rs
imports it, so that DataDome's head injection can leave its client-side tag
out. That marker decides nothing about caching or the origin path any more,
which the neutral PersonalizedResponse marker now does, and it moves out with
the DataDome integration, as the code comment at the read site in
publisher.rs says.

The ts audit vendor table is unchanged. The
ts audit
command keeps its own vendor detection patterns outside the registry, and how
the CLI learns a vendor's detection pattern from a crate is still open.

One deliberate deviation from the spec. Section 4 says the source-file
guard should drop the nine vendors' files. This pull request instead generates
the whole guard list from the source tree through the new
crates/trusted-server-core/build.rs,
so a file joins or leaves the guard on its own and no vendor move has to touch
it. The goal is the same and the mechanism is different.

What the round trip does not exercise. The seam probe declares a proxy, a
carried browser module, a request preparer, and location, Edge Cookie and
device providers. It declares no head injector, attribute rewriter, script
rewriter, HTML post processor or request filter, and it uses neither the
deferred nor the standalone script delivery flag, so the round trip does not
exercise those from outside core.

@aram356

aram356 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

The sequencing discussion for this series is on #1084. This PR is superseded rather than rejected. The design in §3.6 is accepted and most of the provider work carries over onto the reordered base. See #1084.

@jwrosewell

Copy link
Copy Markdown
Contributor Author

Sequencing note on the rc/202608 release branch and this stack.

While checking today's activity I found that rc/202608 (cut from main at d516a9e on 28 August, currently at 07dfc1c) carries 67 merged PRs, including #940, #823, #1016, #1036, #1039 and #1079, in 643 commits. main is an ancestor of it, so it can land on main as a fast-forward at any time.

A merge simulation of rc/202608 against this PR's head (b557d87) conflicts in 33 files, listed below. The same simulation against #1084's head (1800604) is clean, because that branch is specs only.

  • crates/trusted-server-adapter-axum/src/app.rs
  • crates/trusted-server-adapter-axum/tests/routes.rs
  • crates/trusted-server-adapter-cloudflare/src/app.rs
  • crates/trusted-server-adapter-fastly/src/app.rs
  • crates/trusted-server-adapter-fastly/src/main.rs
  • crates/trusted-server-adapter-fastly/src/platform.rs
  • crates/trusted-server-adapter-spin/src/app.rs
  • crates/trusted-server-adapter-spin/src/platform.rs
  • crates/trusted-server-core/src/auction/README.md
  • crates/trusted-server-core/src/auction/formats.rs
  • crates/trusted-server-core/src/auction/mod.rs
  • crates/trusted-server-core/src/auction/orchestrator.rs
  • crates/trusted-server-core/src/auction/test_support.rs
  • crates/trusted-server-core/src/config.rs
  • crates/trusted-server-core/src/config_payload.rs
  • crates/trusted-server-core/src/ec/device.rs
  • crates/trusted-server-core/src/integrations/aps.rs
  • crates/trusted-server-core/src/integrations/datadome/protection.rs
  • crates/trusted-server-core/src/integrations/mod.rs
  • crates/trusted-server-core/src/integrations/registry.rs
  • crates/trusted-server-core/src/platform/traits.rs
  • crates/trusted-server-core/src/proxy.rs
  • crates/trusted-server-core/src/publisher.rs
  • crates/trusted-server-core/src/response_privacy.rs
  • crates/trusted-server-integration-tests/fixtures/configs/trusted-server.integration.toml
  • docs/guide/api-reference.md
  • docs/guide/auction-orchestration.md
  • docs/guide/configuration.md
  • docs/guide/ec-setup-guide.md
  • docs/guide/error-reference.md
  • docs/guide/fastly.md
  • scripts/template-cache-local-test.sh
  • trusted-server.example.toml

Reproduce:

git fetch upstream main rc/202608
git merge-tree --write-tree --name-only upstream/rc/202608 b557d87e4

The request is the one we made on #940. Please land this stack (#1043 to #1047, #1084 and this PR, all based on main) before rc/202608 merges to main, or if the release branch has to go first, say so here so we rebase once against a known base rather than chase one that moves as it takes more PRs. Every one of the 33 files is one we would have to re-resolve by hand, and the count rises with each PR merged into the release branch.

@jwrosewell
jwrosewell force-pushed the split/7-integration-seam-impl branch 7 times, most recently from 35584db to b64b45c Compare September 1, 2026 23:24
…ider

First of five PRs decomposing the provider and permission epic. The
EdgeCookieProvider trait routes Edge Cookie minting, cookie read-back,
and KV keying through the selected provider, so a vendor identifier
round-trips verbatim instead of being dropped by the built-in shape
check.

- [ec] provider selector with per-provider [ec.providers.<key>] blocks.
  The deprecated [ec] passphrase form still starts for one release
  cycle: it maps to provider = "hmac" with a deprecation warning, and a
  configuration carrying both forms is rejected. provider = "none"
  spells explicit statelessness. A configured block that is not the
  selected provider is rejected at startup, as is a block with no
  selector.
- Global identifier bounds enforced by core at mint, read-back, and
  cookie write: the cookie-safe alphabet [A-Za-z0-9._~-] and a 256-byte
  cap. An identifier outside the bounds is rejected loudly, never
  rewritten, so the cookie value and the identity-graph key can never
  silently diverge.
- The identity graph is keyed by the provider's canonical form of the
  identifier (normalize_id_for_kv), so equivalent representations of
  one identity share one row.
- Request evidence abstraction (crate::evidence) giving providers read
  access to the client IP, headers (including cookies), URL path, and
  query parameters.
- Adapter injection seam: RuntimeServices carries an optional vendor
  provider, so a vendor provider lives in its own crate and core never
  names it. A selected provider the adapter does not inject fails the
  request loudly rather than silently running stateless.
- Provider generate failures log at error level with the request
  proceeding stateless.

Edge Cookie creation and use stay gated by the existing consent context
exactly as on main, including with no provider selected; the permission
model replaces that input in the third PR of this series.

Config migration: move [ec] passphrase to [ec.providers.hmac] and set
[ec] provider = "hmac". The old form keeps working for one release with
a warning. Passphrases shorter than 32 characters are now rejected at
startup; previously they were accepted.

The design spec for this slice and the next lives at
docs/superpowers/specs/2026-07-30-pluggable-providers-design.md, the
2026-07-31 draft revised to match the implementation with a
revision-record table of every divergence.

Every provider carries a mandatory registered four-character code
(provider-code-registry.md): core mints {code}~value, checks the code
at read-back, and keys the identity graph with it, so identifiers from
different providers can never collide and a switch of provider cannot
silently adopt another provider's identities. The built-in hmac
provider mints hmac~<hash>.<suffix> and dual-reads its pre-envelope
bare form for one release cycle.
Since the provider-code envelope, the mint path issues identifiers as
hmac~{64hex}.{6alnum}, and that is the value identify hands to partners.
Pull sync, batch sync and the admin lookup still validated the bare
shape through is_valid_ec_id, so pull sync skipped every freshly minted
identifier, batch sync answered invalid_ec_id for the value partners were
given, and the admin lookup answered 400. CI stayed green because the
lifecycle scenario seeds a bare cookie.

is_valid_ec_id now accepts the hmac envelope as well as the legacy bare
form and rejects any other provider's code, and normalize_ec_id_for_kv
keeps the envelope so the key matches the one written at mint. Tests
cover the validator, the normalizer and each of the three call sites
with a coded identifier.
CodeQL's cleartext-logging query treats a call whose name contains
"passphrase" as a sensitive source, and because the method mutates the
Settings it belongs to, every later log line that prints anything from
Settings (store names, timeouts, header names) is reported as writing a
secret to a log. The passphrase itself is a Redacted<String> and none of
the flagged lines prints it. The method now describes what it does,
migrate_legacy_ec_layout, and its behavior is unchanged.
A reviewer raised a P1 against the pluggable Edge Cookie provider work:
three of the four adapters broke the provider contract that an
unavailable required service or an uninjected provider stops the
request.

The Axum, Cloudflare and Spin adapters each read the Edge Cookie context
with `EcContext::read_from_request_with_geo(...).unwrap_or_else(...)`,
logged a warning and continued with `EcContext::default()`. A deployment
whose selected provider could not be built therefore came up and served
every request with no identity, silently. The Fastly adapter already
kept the report and answered with an error response.

`build_ec_context` on the three adapters now returns
`Result<EcContext, Report<TrustedServerError>>` and every call site
propagates it to that adapter's own `http_error`, the same helper Fastly
uses, so all four answer with the same status and shape.

The design this implements has the composition root check a selected
provider's needs once at startup rather than per request, so
`ensure_provider_available` was added to `ec/provider.rs` and is called
from `build_state_with_settings` on all four adapters (Fastly included,
so the rule is uniform). Building a provider reads no request data, so
a selection an adapter can never supply now fails when application state
is built, and the three adapters answer every route from their existing
`startup_error_router` instead of coming up. Statelessness, meaning no
`[ec] provider` selector or the explicit `"none"`, still passes and
still serves.

The widening question was checked rather than assumed.
`read_from_request_with_geo` can only fail from two places: the provider
build, and a `Cookie` header that is not valid UTF-8. A malformed cookie
value is dropped with a warning by `request_ec_id_if_allowed`, consent
parsing returns a value rather than a `Result`, and the geo lookup is
already swallowed by the adapter before the call, so no ordinary parse
problem reaches the error path and none is turned into a failed request.

Tests: each of the three adapters gains a route test proving an
uninjected provider fails at startup, and an in-crate test proving
`build_ec_context` returns the error rather than a default context.
Core gains a test that the startup check rejects an uninjected provider
and still allows statelessness both ways.

Addresses: Christian Pavilonis review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:317 (P1)
`Settings::finalize_deserialized` runs derive validation before
`Ec::migrate_legacy_ec_layout`, and the deprecated `[ec] passphrase`
field carries no `#[validate]` attribute of its own, so the advertised
32-byte minimum was only enforced on the new `[ec.providers.hmac]`
location. A configuration still on the old form could start with
`passphrase = "short"`, or with an empty value, and mint identifiers
from keying material the new location rejects.

The migration now calls `Ec::validate_passphrase` on the value it is
about to move, before it logs the deprecation warning and writes the
`[ec.providers.hmac]` block, and reports a configuration error naming
the minimum length and the new location.

Tests: `a_legacy_passphrase_is_held_to_the_passphrase_rules` drives
`Settings::from_toml` with the `[ec]` section rewritten to the
deprecated form and proves a short value and an empty value are both
rejected, and that a passphrase of adequate length still migrates to
`provider = "hmac"` with the passphrase in the hmac block. Removing the
new check makes that test fail, so it tests the fix rather than the
surrounding code.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:658 (wrench)
The provider spec (section 6) says `deny_unknown_fields` is set on both
built-in provider config structs, but `HmacProviderConfig` carried no
such attribute, so `[ec.providers.hmac] typo_key = "x"` was accepted
silently. An operator who mistypes a key gets a deployment that starts
and quietly uses the default for the setting they meant to change.

`HmacProviderConfig` now sets `#[serde(deny_unknown_fields)]`, matching
`Ec` itself and the rest of the settings tree. The struct is a plain
field of `EcProviders` rather than a flattened one, so the attribute
does not collide with the `#[serde(flatten)]` vendor map alongside it.

Tests: `an_unknown_key_in_the_hmac_provider_block_is_rejected` adds an
unknown key to the block in the crate test configuration and proves
`Settings::from_toml` fails and names the key. Removing the attribute
makes that test fail.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:726 (wrench)
`build_provider`'s `"hmac"` arm mapped over `ec.providers.hmac`, so a
deployment that selected `provider = "hmac"` with no
`[ec.providers.hmac]` block got `Ok(None)` and ran stateless under a
selector that says it has an identity provider. Every other unbuildable
selection in the same match already errors.

The arm now returns `TrustedServerError::EdgeCookie` naming the missing
block, which the startup check `ensure_provider_available` turns into a
failed application state on every adapter.

`Ec::validate_provider_selection` rejects that pair before settings
reach the composition root, so nothing routes through the new arm today.
It is the drift guard for the case where the two checks stop agreeing,
which is exactly the shape of the defect being fixed, so it is worth
keeping rather than leaving the silent branch in place.

Tests: `selecting_hmac_without_its_block_fails_loudly` builds the `Ec`
programmatically, bypassing settings validation to reach the seam, and
proves the error names the missing block. The doc comment's `# Errors`
section is corrected in the same commit, since it still claimed no
built-in construction can fail.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:304 (refactor)
The error raised when a provider mints an identifier outside the
identifier bounds was written across two source lines without the
trailing backslash that joins them, so the 22 spaces of source
indentation became part of the literal and the logged message read
"...bytes, or                      outside the cookie-safe alphabet".

The continuation is restored, so the message reads as one sentence.

The whole of ec/mod.rs was scanned for the same fault, matching every
string literal and stripping real continuations before looking for runs
of more than one space or a newline inside a literal. This message was
the only one.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/mod.rs:444 (nitpick)
The paragraph written for the `EdgeCookieProvider` trait sat at the top
of `ProviderCode`'s doc block, so rustdoc rendered it as part of that
struct's documentation and the trait itself had no doc comment at all.
A vendor implementer opening the trait saw nothing, and a reader of
`ProviderCode` saw two subjects run together.

The paragraph moves onto the trait and `ProviderCode` keeps only the
registry text that belongs to it.

The moved sentence was also stale: it said a provider returns `Ok(None)`
from `generate`, but `generate` returns a `GeneratedEdgeCookie` and
signals "no identifier this request" through its `id` field. The
sentence now describes the actual return, with an intra-doc link to the
field.

`cargo doc --no-deps` reports no warning against either item.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:177 (nitpick)
`ec::get_ec_id` had no callers anywhere in the workspace, and this
branch loosened its filter to accept any well-formed `{code}~` value
with no ownership check against the selected provider. A future caller
picking it up would adopt another provider's identifiers, which
`EcContext` deliberately treats as absent.

The no-callers claim was checked across every crate in the workspace
(the four adapters, the CLI, core, the integration tests, openrtb) plus
benches, tests and docs. The only matches are for a different,
crate-private `edge_cookie::get_ec_id`, which reads the `x-ts-ec` header
as well as the cookie and is what `proxy.rs` and the testlight
integration call.

Deleted rather than realigned, for two reasons. The workspace sets
`publish = false`, so `trusted-server-core` is not distributed and
nothing outside this repository depends on the symbol. And aligning the
filter would mean calling `provider_owns_id`, which needs a
`&dyn EdgeCookieProvider` that a function taking only `&Request` cannot
obtain, so it would have meant changing the signature of a function with
no callers. `EcContext::read_from_request` already performs the
provider-aware read that production uses.

`parse_ec_from_request`, `is_valid_ec_id` and `log_id` all keep other
callers in the module, so nothing else becomes dead. The core README
line that advertised the helper is removed in the same commit.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/mod.rs:137 (nitpick)
The `ec/provider.rs` module doc said a provider's constructor takes the
services it needs, naming `RequestInfo` as the example, and its opening
sentence was garbled where two half-sentences had been spliced
together. `RequestInfo` is not a constructor argument. It is borrowed
per call as the `request_info` parameter of
`EdgeCookieProvider::generate`, so the first thing a vendor implementer
read contradicted the trait they were about to implement.

`evidence.rs` carried the same claim in its own words, that a
constructor takes services as `Arc<dyn Trait>` supplied per request.
Nothing in the workspace passes `RequestInfo` that way. Every use site
is a `&dyn RequestInfo` argument.

Both module docs now describe the real shape, which is construction
once at startup from configuration or adapter injection, then borrowed
request evidence on every call with nothing retained. The `evidence.rs`
title changes to match, and its pointer to the borrowed view
`BorrowedRequestInfo` is named alongside `OwnedRequestInfo`.

Documentation only, no behavior change. `cargo doc --no-deps` reports no
warning against either module.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:4 (nitpick)
The keys `"hmac"` and `"none"` were spelled as bare string literals at
four places: `Ec::validate_provider_selection`, `build_provider`,
`provider_owns_id`'s `provider.id() == "hmac"` check, and a private
`HMAC_PROVIDER_CODE` in `ec/generation.rs`. Nothing tied them together,
so a fifth built-in provider would add a fifth spelling and a typo in
any one of them would compile.

`EcProviderSelection { None, Hmac, Vendor(String) }` now holds the
vocabulary in `ec/provider.rs`, with `NONE_KEY` and `HMAC_KEY` as the
only places those two words are written. Vendor keys are open-ended, so
the catch-all `Vendor` variant takes any other key and
`#[serde(from = "String", into = "String")]` gives the enum an
infallible conversion in each direction rather than a hand-written
visitor. `HMAC_PROVIDER_CODE` moves next to it as a `ProviderCode`
const, built from `HMAC_KEY`, and `generation.rs` uses that instead of
its own copy. `HmacProvider::id` and `HmacProvider::code` return the
same two constants.

`Ec::provider` becomes `Option<EcProviderSelection>`, so the two
validation paths and `build_provider` match on variants rather than
comparing strings, and `Option` still distinguishes an absent selector
from an explicit `"none"` exactly as before.

The configuration surface is unchanged. The selector reads and writes
the same string, so an existing `trusted-server.toml` parses to the same
choice and a config push writes the same key back.

Tests: `the_selector_round_trips_through_serialization` parses `none`,
`hmac` and an arbitrary vendor key from TOML, checks each maps to its
variant, and checks each serializes back to the same string.
`each_selection_builds_what_its_string_key_built_before` proves the
three selections still build what they built before, which is nothing
for `none`, the built-in provider with the built-in code for `hmac`, and
the adapter-injected provider of that id for a vendor key.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs (refactor)
A provider's response headers were inserted into the outbound response
without any check on what they set. A provider could return
`Set-Cookie: ts-ec=...`, including on a request where it minted no
identifier at all, and so write the managed identity cookie without
going through core's identifier validation or its requirement that a
minted identifier have an identity-graph row. It could also overwrite an
`x-ts-*` header or a framing header.

Core now defends by reserving its own namespace rather than banning
`Set-Cookie`, because providers legitimately need cookies of their own.
`reserved_response_effect` in `ec/provider.rs` classifies one header and
rejects three things: a `Set-Cookie` naming a cookie in the `ts-` prefix
core manages (`ts-ec`, `ts-eids`, `ts-tester`), a header in the `x-ts-`
namespace core emits and strips, and a message framing or hop-by-hop
header (RFC 7230 6.1 plus `content-length`, the same set each adapter's
`is_hop_by_hop_response_header` uses). Everything else, a provider's own
cookie included, passes through unchanged. The cookie name is read from
the raw header bytes so a value that is not valid UTF-8 cannot smuggle a
managed name past the check.

A rejected effect fails the request rather than being dropped with a
log. The check sits in `EcContext::generate_with_provider`, the only
place provider headers are captured, next to the identifier-bounds check
that already fails the request when a provider mints outside the
cookie-safe alphabet. Both are the same kind of fault, a provider
breaking its contract, and this branch has already decided that
identity problems stop the request rather than serving without identity.
Finalization cannot fail a request in any case, since it returns no
result.

Tests cover the classifier directly (managed cookie, reserved header,
framing header, a non-UTF-8 `Set-Cookie`, and the allowed cases), and
cover both halves through the organic generate path: a provider setting
`ts-ec` with no identifier fails the request, and a provider setting its
own `acme-evidence` cookie mints normally and has that cookie reach the
response alongside core's own `ts-ec`.

Addresses: Christian Pavilonis review of PR 1043, crates/trusted-server-core/src/ec/finalize.rs:57 (P2)
Brings upd/split/3-permissions at a2a1dfc into split/4-client-resolve. That
renames `config/permissions/vanilla.yaml` to `config/permissions/sample.yaml`
and says in the file, in the guide and on the constant that the sample is for
testing and evaluation only, is not a production policy and is not legal
advice.

The merge had no conflicts. `trusted-server.example.toml` auto-merged, because
this branch changes other parts of the same file.

Tests. `cargo fmt --all --check` is clean and the core suite passes 2,885
tests.
configuration guide

Brings upd/split/4-client-resolve at e17090b into
split/5-response-hook-docs, which carries split/3's a2a1dfc renaming
`config/permissions/vanilla.yaml` to `config/permissions/sample.yaml`.

This branch adds the permissions paragraph in `docs/guide/configuration.md`,
which split/3 does not have, so the merge could not correct it. That sentence
now names the new path and says the sample is for testing and evaluation only
and is neither a production policy nor legal advice, matching the file header
and the permission model guide.

The merge had no conflicts. `trusted-server.example.toml` auto-merged.

Tests. `cargo fmt --all --check` is clean and the core suite passes 2,885
tests. The docs Prettier configuration sets `proseWrap: preserve`, so the
edited sentence cannot change the formatting of the file.
…split/5

Brings upd/split/5-response-hook-docs at 672cc60, which carries main at
066ea3c and the permission signal work through split/1 to split/5, into
split/7-integration-seam-impl. CLAUDE.md and 22 files conflicted.

The auction follows main. Main's IABTechLab#1016 replaced the auction provider
table this branch had opened with a compiled auction plan, which the
orchestrator and the integration registry share. This branch no longer
changes the auction, so AuctionProviderBuilder, its two function types,
build_orchestrator_with_providers, the public
AuctionOrchestrator::register_provider, the seam probe's auction provider
and the test that composed it are gone. compile_auction_plan,
build_orchestrator_with_plan, AuctionOrchestrator::from_plan and the
test-only register_provider stand as main has them, and the auction
module, its README and the auction guide take main's text. The
generalized BidRenderer stays, with the APS renderer types in
integrations/aps.rs and an unchanged serialized form.

- The registry. with_plan gains with_plan_and_registrations(settings,
  plan, extra). Prebid and APS register from the plan first, as on main,
  and then the builders run with the duplicate id and preparer handling.
  The Prebid and APS ids are recorded with the builders, so an outside
  builder claiming either is still refused naming both sources, as it
  was when they were builders. new and with_registrations are test-only,
  as new is on main.
- BUILT_IN_BUILDERS holds main's twelve integrations in main's order,
  with js_asset_proxy first, through IntegrationBuilder::new. aps and
  prebid leave it, because main makes them plan providers.
- Validation follows main's validate_settings_for_deploy and
  validate_settings_for_runtime, with the builder loop in place of main's
  list of named integrations. Deploy validation leaves placeholder secret
  values to runtime validation, as main does since IABTechLab#1036, because secret
  fields hold key names at deploy time. Prebid, APS and adserver_mock are
  validated by name, as on main, and the new
  validation_reaches_every_plan_backed_integration plants a bad block for
  each, at deploy and at runtime. It failed on adserver_mock before that
  check was restored. js_asset_proxy validates through its builder and
  still checks a disabled block. DataDome's deploy validator had the same
  body as its startup validator and no caller left, so it goes, and so
  does adserver_mock::validate.
- Identity. This branch's asynchronous EdgeCookieProvider::generate,
  which takes RuntimeServices, meets main's orphaned identifier recovery
  in finalize. ec_finalize_response and the two recovery functions are
  asynchronous and take the services, and the Fastly entry point blocks
  on the finalize as it does on its other asynchronous steps. The geo
  lookup main added in Didomi is asynchronous and takes the services.
- The adapters. Axum, Cloudflare, Fastly and Spin compile the plan, check
  it for their target, build the orchestrator from it, and build the
  registry with their registrations over the same plan, so the Edge
  Cookie provider a module supplies still comes from the registry.
- CLAUDE.md is a symlink to AGENTS.md again, where this branch had
  committed the plain file a Windows checkout writes.
- The integration guide no longer describes an auction provider builder.

Tests. The merged tree passes cargo fmt, Clippy with warnings denied on
the Fastly, Axum, Cloudflare and Spin adapters on their native and wasm
targets and on the CLI, codegen and parity crates, and the release wasm
builds for Fastly and Spin. Core passes 2,937 tests natively and 2,930
under Viceroy with 7 ignored, and 18 doctests. The Fastly adapter passes
192 tests under Viceroy, Axum 72 including 9 seam probe round trips,
Cloudflare 47, Spin 89, the seam probe crate 8 and a doctest, the parity
suite 13, the CLI 87 on Linux and the codegen crate 7, and the benchmark
smoke run passes.
The Axum adapter checked the [ec] provider selection before it built the
integration registry and gave the check no module provider, so a
deployment selecting a module's Edge Cookie provider was refused at
startup with "Edge Cookie provider `seam_probe` is selected but this
deployment's adapter does not provide it", although the request path
takes that provider from the registry. The Cloudflare, Fastly and Spin
adapters already build the registry first.

The check now runs after the registry is built and is given the
registry's provider. ec_selector_naming_a_module_starts_the_adapter
starts the Axum router with the seam probe's provider selected, and it
failed with that error before the change. The selector tests beside it
build the registry directly, so none of them reached the adapter's check.
Every name an operator types into configuration is snake_case, so the
demonstration provider is now selected with `[ec] provider = "client_fixed"`.
The key constant, the two startup messages that spell the key, the page
script's log lines, the cargo feature comment, the provider table in AGENTS.md
and the resolve endpoint in the API reference all follow. This commit changes
only that one name.

The cargo feature that compiles the provider in keeps the name
`client-fixed-demo`, because a cargo feature name is not a configuration value.
The tsjs module id `ec_client_fixed` is unchanged as well, being already
snake_case and never compared against the key.

The old name is refused when settings load, which every adapter does through
`settings_from_config_blob` before it serves a request. The message names
`client_fixed` and says what to write, so a deployment still configured with
`client-fixed` stops at startup rather than being told to add a provider block
that this provider does not need. A block written under the old name does not
make the old name acceptable either, because the name is refused before any
block is looked for. That is a breaking change, accepted for a major release.

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,886 tests and 8 doc-tests, with and without the
`client-fixed-demo` feature, the new test among them. The Axum adapter passes
62 tests, `cargo clippy-axum` is clean, and `cargo check -p
trusted-server-adapter-fastly --target wasm32-wasip1` builds. The page
script's own vitest file passes 6 tests and ESLint is clean on it.
Every provider type selects the same way, with provider in its own section,
and names its providers in snake_case. Permission signals now follow that,
so [permission_signal] sources becomes [permission_signal] provider. The key
stays an ordered list, and a configuration that leaves it out still runs
every provider the adapter links, in the order the adapter offers them.

The two hyphenated provider names become gpp_sale_opt_out and us_privacy,
while gpc and tcf are unchanged. The crates report the new identifiers, and
core's selection messages, the startup log, the module README, the guides,
the example configuration and the permissions sample follow.

sources is removed rather than accepted alongside provider, which breaks a
configuration written against the branch that introduced it. A section
carrying sources is refused when the settings are read, with a message
naming provider, on each path a deployment reads settings through, being a
TOML file, the TOML value ts config push reads, and the JSON config blob
read at startup. Reading the section by hand rather than with
deny_unknown_fields is what makes that message possible, because a derived
refusal can only name sources by declaring it as a field, and would then
offer it as a key it expects whenever it refused any other.

No provider takes settings, so a [permission_signal.<name>] block stays an
unknown field. Its refusal now says that a provider which gains settings
will take them there.

Tests cover the removed key on each of those paths, a settings block
refused as an unknown field, a list surviving a config blob round trip,
and, where the real crates are linked, the documented names selecting all
four providers in order and an old hyphenated name being refused with the
names available.
An integration used to carry its own `enabled` flag, with a default that
differed between integrations, so whether one ran depended on the flag, on
that default and on whether its block was present at all. It now runs when,
and only when, `[integration] provider` names its id, which is the convention
the other provider types already follow.

The `[integrations]` table becomes `[integration]`, and a configuration still
carrying the old name is refused with the move spelled out rather than with a
bare unknown-field error. Every integration's `enabled` field, its default
function, `IntegrationConfig::is_enabled` and the disabled-schema machinery go
with it. A block is written only for an integration the list names: one
written for an integration it does not name is refused, as is an id named
twice and an `enabled` key left behind in a block. A named integration with no
block of its own is read from an empty one, so an integration that takes no
settings runs on its id alone and one that requires a setting reports the
setting it is missing.

The registry builds only the builders the list names, so an integration runs
exactly when an operator names it, whatever its builder would otherwise make
of the settings, and it refuses an id no builder in the deployment supplies,
listing the ids it has. Deploy validation deliberately does not make that
check, because a vendor crate the CLI never links may supply the id. The
DataDome secret paths move with the table, from `integrations.datadome.*` to
`integration.datadome.*`, keeping their optional segments so an integration
that does not run is never asked for a secret.
`ts audit` used to flip an `enabled` key in the four active integration stubs
the starter template carried. It now writes `[integration] provider` with the
integrations it found and can configure on its own, appends the Google Tag
Manager block with the container it read from the page, and appends the
JavaScript asset proxy block with every asset it found set to
`proxy = "disabled"` for review. It names that asset proxy only when it found
at least one asset, because a block with no assets is refused.

`ts prebid bundle` reads and writes `[integration.prebid]`, the
integration-test fixture names the one integration that environment runs and
drops the blocks for the ones it does not, and the template-cache harness
names Prebid in the list rather than flipping a key. The environment override
examples move to `TRUSTED_SERVER__INTEGRATION__<ID>__<SETTING>` and lose the
`ENABLED` ones, because the provider list is an array and the overlay replaces
only scalar leaves that already exist. The browser Prebid module's operator
message points at `[integration.prebid.bundle]`.
Every name an operator types into configuration is snake_case, so the built-in
host-signal provider is now selected with `[ec] provider = "host_signals"` and
configured with `[ec.providers.host_signals]`. The key constant, the serde name
of the typed block, the validation error key, the registered secret path
`ec.providers.host_signals.passphrase`, the placeholder-secret report, the two
startup messages that spell the key and the provider's own log line all follow.
This commit changes only that one name.

The `HostSignals` trait, the `host_signals` fields and arguments that carry it,
and the prose that calls this the host-signal provider are unchanged, because a
host capability is not a configuration value. The test secret-store key name
`host-signals-passphrase-key` is unchanged for the same reason, being a name an
operator picks for a secret rather than a provider name.

The Rust field name and the configuration name are now the same word, so the
`#[serde(rename)]` on the typed block goes and the field is declared the same
way as `hmac` beside it. That also retires the reason the `EcProviders`
validation keys were spelled out by hand, so the note above those keys is
rewritten to say what still holds, which is that each key has to match the
secret path `TrustedServerAppConfig::secret_fields` registers.

The old name is refused when settings load, which every adapter does before it
serves a request. The message names `host_signals` and says what to write. The
refusal comes before any block is looked for, because a block left behind under
the old name is captured as a vendor block, so the old selector would otherwise
find that block, pass the settings check, and fail later in provider resolution
with a message about an adapter that supplies no such provider. That is a
breaking change, accepted for a major release.

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,764 tests and 5 doc-tests with 4 ignored, the new test among
them. The Axum adapter passes 43 tests across its three binaries, `cargo
clippy-axum` is clean, and `cargo check -p trusted-server-adapter-fastly
--target wasm32-wasip1` builds.
The Edge Cookie provider blocks move from [ec.providers.<name>] to
[ec.<name>], so identity follows the one convention every provider type
uses, where [<type>] provider = "<name>" selects and [<type>.<name>]
holds that provider's settings. The [ec.providers] table is gone, and a
configuration still carrying it is rejected with the new location in the
message.

A block exists only when the provider has settings. The built-in hmac
provider has a required passphrase, so selecting it still needs
[ec.hmac], while a provider with no settings needs no block at all. Only
the adapter that injects a provider knows whether that provider has
settings, so core no longer demands a block for a name it does not
supply itself.

A block may name the implementation it configures with
implementation = "<id>", which makes the block's own name a label of the
operator's choosing. [ec] provider = "primary" with [ec.primary] holding
implementation = "hmac" and a passphrase configures the built-in
provider under a name that means something to the deployment. Everything
that resolves the selection now reads the implementation rather than the
label, covering the built-in lookup, the matching of a provider the
adapter injects, the check that a selected implementation has the
settings it needs, and the errors. An implementation this deployment
cannot build fails startup naming the implementations it does have.

The fixed [ec] keys stay reserved and cannot name a provider, every
other key in the section has to be a table, and a key that is not one is
reported as the unknown field it almost certainly is, so a typo such as
ec_stor is still caught with a sensible message. Provider names and
implementation ids are snake_case. A block the selector does not name
still fails startup, as it did before.

Secrets follow the blocks. TrustedServerAppConfig::secret_fields now
lists ec.hmac.passphrase, and EdgeZero's path segments cannot say
"whatever name the operator chose", so core reads the labeled blocks out
of the configuration itself through the new ConfiguredSecretFields trait
and resolves their passphrases from trusted_server_secrets in the same
pass. Push-time validation, where those fields hold key names rather
than secrets, no longer runs the passphrase value check against a
labeled block's key name. The check itself is unchanged wherever
settings are loaded with their secrets resolved.

The legacy [ec] passphrase shim still works and now points at [ec.hmac].
Core named three vendors. The auction plan compiled a fixed list of
profiles, `standard`, `prebid-server` and `aps`, and would accept only
one ad server, the demonstration mock. A vendor could not ship a demand
source without editing core, which is the coupling the integration seam
exists to remove.

Demand and the ad server are now provider types, configured the way the
Edge Cookie, location, device and permission signal providers are:

    [demand]
    provider = ["pbs_main"]

    [demand.pbs_main]
    implementation = "prebid_server"
    endpoint = "https://pbs.example/openrtb2/auction"
    timeout_ms = 1500

    [adserver]
    provider = "adserver_mock"

    [adserver.adserver_mock]
    endpoint = "http://127.0.0.1:6767/adserver/mediate"

An integration builder registers an implementation, and a builder that
supplies only implementations is not a page integration a deployment can
name. `openrtb`, `prebid_server`, `aps` and `adserver_mock` register that
way, so core names no vendor and a vendor's own crate registers on the
same terms.

The shared `provider_table` module carries the rules every provider type
follows, so each type reports the same faults the same way. A table its
type's `provider` does not select refuses startup rather than sitting
unread. An implementation this build does not have refuses startup and
the message lists the ones it does have. Every implementation rejects a
setting it does not know, so a misspelt key fails rather than being
dropped. A demand or ad server endpoint must be HTTPS, or plain HTTP to
a loopback address, so a local test stack runs without certificates and
nothing leaves the machine unencrypted.

`auction::demand` is the seam itself. An implementation states the
standard `OpenRTB` field choices it makes, adds its own extensions,
adjusts its outbound headers and reads its own responses. The shared
driver builds every other field, enforces privacy, and applies the
notification policy and routing diagnostics the same way for all of
them, so an implementation can leave data out of what enforcement
approved and can never add data enforcement removed.

APS stops being an integration. It is selected in `[demand]`, its
`rendering_mode` sits in its own demand table, and the renderer
registers from the plan. Two APS sources that disagree on the rendering
mode refuse startup, because one page can be rendered only one way.

The word "mediator" is replaced by "ad server" throughout. The debug
option `include_mediator_response` becomes `include_adserver_response`
and the response metadata strategy `parallel_mediation` becomes
`parallel_adserver`.

Breaking changes an operator sees, each refused at startup with a
message naming the new home:

  `[auction.providers.<id>]`  ->  `[demand] provider` and `[demand.<name>]`
  `protocol` and `profile`    ->  `implementation`
  `profile_config` fields     ->  the settings flat in the table
  `[auction] mediator`        ->  `[adserver] provider`
  `[integrations.aps]`        ->  the `aps` demand table
  `[integrations.adserver_mock]` -> `[adserver.adserver_mock]`
Every guide that showed `[integrations.<id>] enabled = true` now shows the
`[integration] provider` list beside the block, the per-integration `enabled`
rows are gone from the settings tables, and the pages that told an operator to
enable an integration say to name it. The integration guide's seam tables say
that a build function runs only for a module the list names and that the CLI
accepts an id it has never heard of, which the registry then refuses at
startup, so a vendor reads both halves of that gap in one place.

The creative processing page and the repository notes said the server
concatenates the modules of the enabled integrations, and they now say the
modules of the integrations that run.

The CHANGELOG carries the migration the startup error points at, and the two
entries in the same release that named the old table are brought with it.
The per-integration switch is gone, so the word goes with it. The registry
query `integration_enabled` is now `integration_runs` over a
`running_integration_ids` list, `gpt_diagnostics::is_enabled` is `runs`, and
`validate_enabled_integrations` is `validate_integration_blocks`, which is what
its own doc comment already said it does. Nothing outside these files used
either name.

The comments that called a module registered but not enabled now say that
`[integration] provider` does not name it, which is what the errors beside them
already told an operator, and the publisher cache tests name the new table in
two comments and in one assertion message.

The DataDome module doc showed `enabled = true` in its example block, which
startup now refuses, so the example names the integration in
`[integration] provider` instead.

A few comment sentences in the template, the integration test fixture and two
doc comments are joined with linking words rather than with a colon, to match
the house style.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants