Repository navigation
Open the integration seam so a vendor module can live outside core - #1094
jwrosewell wants to merge 369 commits into
Conversation
|
Sequencing note on the While checking today's activity I found that A merge simulation of
Reproduce: The request is the one we made on #940. Please land this stack (#1043 to #1047, #1084 and this PR, all based on |
35584db to
b64b45c
Compare
c6dd589 to
54337f0
Compare
1bfb2e6 to
95be9fb
Compare
The `hb_adid` fallback in `build_bid_map_with_auction_id` wants one field, the bid identifier, and reached it through `BidRenderer::payload_as::<ApsRendererV1>`, which clones the entire payload map and deserializes it into an owned struct. One of those fields is `aaxResponse`, a base64 creative envelope capped at 256 KB, so every bid on every page view that runs the ad stack copied that envelope twice, once into the cloned `Value` and once into the owned `String`, only to drop both. `BidRenderer::payload_field` borrows one value out of the payload with the same type-tag check `payload_as` applies, and the call site uses it with a new `APS_RENDERER_BID_ID_KEY` naming the wire key. Nothing is serialized differently: `payload_field` only reads, so the wire form is untouched. `payload_as` stays for the callers that genuinely want the whole descriptor, with a doc note pointing one-field readers at the new accessor. The one behavior difference is that `payload_field` does not check that the rest of the payload deserializes, so a malformed payload that still carries `bidId` now yields it. Every APS payload is built by `from_typed` from an `ApsRendererV1`, so no production path reaches that case, and the accessor documents it. Evidence the output is unchanged: - `renderer_bid_id_key_matches_the_serialized_form` (aps.rs) asserts the new accessor returns exactly what `payload_as` deserializes, and pins `APS_RENDERER_BID_ID_KEY` to the camelCase name `ApsRendererV1` actually serializes. - `bid_map_prefers_the_renderer_bid_id_over_ad_id_and_the_openrtb_bid_id` (publisher.rs) gives every `hb_adid` source a different value and a 200 KB envelope, so it passes only if the renderer field is the one read. - `bid_map_ignores_a_renderer_bid_id_carried_under_another_type_tag` covers the tag check at the call site. - The pre-existing `bid_map_exposes_aps_renderer_and_selected_bid_id` still passes untouched.
The comment on the carried-module hash check said the SHA-256 costs one per request on Fastly and "once per process on the other adapters". That is wrong on two hosts of three. Reading the pinned EdgeZero adapters (v0.0.4, git checkout 9e661ae): `edgezero_adapter_cloudflare::run_app` calls `A::build_app()` inside the per-request entry point, and its own comment says "every Worker request re-enters this function"; `edgezero_adapter_spin::run_app` does the same, with the matching comment about `#[http_service]`. Fastly's `run_app` also calls `A::build_app()` per request, and this repo's Fastly `main` calls `TrustedServerApp::build_app_with_state()` per request in any case. Only `edgezero_adapter_axum::run_app` calls `A::build_app()` once before serving, and this repo's Axum `main` likewise calls `TrustedServerApp::routes()` before handing the router to the dev server. So the application is built once on Axum and per request everywhere else. Corrected here: - `integrations/registry.rs`, the carried-module hash comment, which is the comment the finding names. - `docs/guide/configuration.md`, which said a provider-selector failure "shows there on every request rather than once" only because "the Fastly adapter builds the application per request". It shows on every request on Cloudflare Workers and Spin too. `tsjs_bundle.rs`'s `compose_hash` doc carried the same claim and was corrected in the previous commit. Also corrected, though pre-existing rather than new on this branch: the `AppState` doc comments in the Cloudflare and Spin adapters both said "built once at startup and shared across all requests", which is false for the same reason, and the Axum one now says why it is the exception. Fastly's already said "once per Wasm instance" and is accurate.
`the_open_renderer_serializes_to_the_same_bytes_as_the_aps_variant_did` compared `serde_json::to_string` against two literal strings whose keys happen to be in alphabetical order. That order holds only while `serde_json::Map` is a `BTreeMap`, which is only while the crate's `preserve_order` feature is off. The feature is reachable in this workspace. `trusted-server-cli` depends on `edgezero-cli`, which depends on `handlebars`, which declares `serde_json` with `features = ["preserve_order"]` unconditionally. Cargo unifies features across everything built for one target, so any command that builds the CLI and core together for the host target turns the map into an `IndexMap` inside core. `cargo tree -p trusted-server-core --target x86_64-pc-windows-msvc -e features -i serde_json` shows no `preserve_order`; adding `-p trusted-server-cli` to the same command shows three. A maintainer running `cargo test --workspace --target <host>` gets the second build, and the test would fail there for a reason that has nothing to do with the wire form. Both sides now go through a `with_sorted_keys` helper before comparison, so the key order is fixed by the test rather than by which map `serde_json` was compiled with. The literals are unchanged, byte for byte, and nothing is weakened: two JSON objects serialize to the same sorted bytes only when they carry exactly the same keys with exactly the same values, which is the promise the literals were there to make.
Fallout from rebasing this branch onto the five-PR stack rather than onto main, where both sides had changed the same functions. The Axum adapter's test helper still called `build_orchestrator`, which this branch's import list replaced with `build_orchestrator_with_providers`. It now calls the latter with no external providers, which is the same thing. The Cloudflare adapter had a test calling `build_per_request_services` with the old `(ctx, settings)` argument pair. That function now takes `(state, ctx)` so it can apply a module's geo provider, and the test has no application state, so it calls `build_runtime_services` directly, which is what it actually wanted. Four doc comments still said an unset `[geo] provider` leaves the host's own lookup in place. Reconciling the two geo designs settled that unset resolves nothing and makes no host geo call, matching the permission model's default, and that `"platform"` is the explicit opt-in to the host lookup. The comments now say that on all four adapters. Addresses: rebase of this branch onto split/5, conflicts resolved in the composition roots and the geo selector
Two checks were written when the host lookup was the only geo provider there could be, and both treat any other value as no geo at all. validate_jurisdiction_acknowledgment asked whether the selector was `"platform"`, so a deployment selecting a module's geo provider was told it had none and made to set assume_single_jurisdiction to acknowledge something untrue. It now asks the question the other way round: only an unset selector and the explicit `none` resolve nothing, and everything else resolves a location. build_geo_provider's doc said an unknown provider is rejected by GeoConfig::validate_provider_selection, which is no longer where that happens. The function returns DisabledGeo for a module id because it cannot see the registry, and the adapter then substitutes the module's provider. The doc now says that the value is the base the adapter starts from rather than what serves the request, and that the registry is the layer that rejects a selector naming a module with no geo provider. The seam probe's test settings also predate the permission model, so they now carry the required [geo] default_country. The two tests that select a geo provider write their own [geo] table, so the fixture adds one only when the caller has not. Addresses: reconciling the geo selector across the permission model and the seam
Answers the architectural finding on IABTechLab#1043 rather than deferring it. Vendor identity reached core through RuntimeServices injection while everything else a module supplies was declared on its integration registration, so there were two extension mechanisms and identity was on the second one. A registration can now declare an Edge Cookie provider and a device provider the same way it declares a geo provider. The registry resolves each against its selector, warns when a module declares a capability the selector does not choose, and the adapters apply all three to RuntimeServices in one place. RuntimeServices gains a device slot, which it had no way to carry before, and with_ec_provider and with_device_provider to match with_geo. Identity and device differ from geo in one way that matters. Both have providers built into core, so a selector naming a built-in is not an error at the registry, and resolution returns None for those and lets core resolve them as before. Two closed allowlists had to open, being the same fault in two more places. The device selector accepted only `builtin` and `fastly`, so any module id was rejected before the registry saw it. The jurisdiction check asked whether the geo selector was `platform`, so a deployment selecting a module's geo provider was told it had none. Neither could stand once a module can supply these. Proven by the probe, which now declares all three capabilities from one registration. Two tests select the module for identity and for device and assert the resolved provider is the module's own. The probe's identity provider mints a value the built-in HMAC provider cannot produce, so a passing assertion means the module's provider ran rather than core's. Addresses: Aram, `ec/provider.rs`, vendor identity should lean on the integration system rather than a second extension mechanism
The test-only constructors of IntegrationRegistryInner still set only the geo fields, so the Fastly target failed to compile while the Axum one did not, because the two targets gate those constructors differently. Addresses: build failure from the registration capability change
A setting that holds a secret holds the name of its key in a pushed configuration and the secret once the settings load. Core named DataDome's two such settings in three places: the list of secret paths, the step that clears a reference nothing uses before the lookup, and the deploy check that a reference in use names a key. A module with a secret could not leave core while core listed its settings by name. A builder now declares them with `with_secret_settings`, each as a path inside the module's own table and a rule for when the table as written puts it to use. Core does the three things from the builders it is given, for any module. DataDome declares its server-side key as in use when protection is on, and its test bypass credential when the bypass is on as well, which are the rules core applied for it. The settings load takes the declarations from the builders it already validates against, so a module a deployment added has its secrets looked up as a stock one does. A leaf listed twice is looked up once, because the fixed list and the load can both name a module core itself offers, and a second lookup would read the secret as the name of a key. A module's table sits under the name its section selects it by, which may be the short name or the full one. The paths are listed and cleared at both, where the code this replaces looked only at `bot-protection.datadome`.
DataDome is the module `bot-protection.datadome`, and it now lives in `crates/bot-protection/datadome` with its Protection API filter and its protection scope. The stock list offers it in the place its hooks ran, and the settings that select and configure it do not change. It leaves its tag suppression for one request, and declares its two secret settings, through the hooks any module has. Its 59 tests went with it and eight were added from what core tested for it: that a suppressed document keeps and rewrites the publisher's own tag, that an ordinary one gets the client configuration, that the settings load looks up both secrets when both are in use and clears each one that is not, the deploy check of an invalid test bypass, and the refusal of a removed setting. The stock list crate checks that a registered list puts DataDome's two settings among the leaves a push treats as naming a key. Core's tests that used DataDome use core's stand-in, or the neutral marker they were about. The stand-in gains a request filter that leaves its mark for a header, so core still tests state a filter leaves reaching the document, which is the route DataDome takes. The scheduling test's origin document gains a head. It asserted that DataDome's tag was absent from a document that had no head for any tag to be written into, so it could not fail, and it now asserts that what a module left on the request is written. The tests of a personalized response say personalized where they said suppressed, and no longer set DataDome's marker beside the neutral one. Core's configuration tests use a module that exists only in the test. Core's settings no longer remove DataDome's two deprecated secret store selectors from its table as they are read. DataDome accepts and discards both, as it did, so a configuration that carries one still loads, and a pushed configuration now keeps them.
`ts audit` turns the integrations it finds on a page into the section that selects each one's module and the name written there. It took each module's name from `trusted_server_core::integrations`, so it stopped compiling when the first module left core, and no local check showed that because the `ts` tool cannot be built on the machine this branch was made on. The stock list crate now answers the question by integration id from the builders it already lists, with a test of every id the generator knows, and the generator asks it. The generator names no module of a vendor's. Prebid is the one exception, because it is registered through the auction plan and no builder carries its name, and it stays in core for now.
Prebid and APS each register a page integration that depends on the compiled auction plan, so the registry called both by name before it ran any builder, recorded Prebid's id by hand, and listed Prebid's module name among the ones a section may select. Deploy validation called Prebid's two checks by name for the same reason. Neither module can leave core while core does that. A builder now registers from the plan with `with_plan_registration` and checks its settings against the plan with `with_plan_validator`. A registration from the plan is made whether or not a section selects the module, because the function decides from the plan and the settings whether the module runs, and its hooks go ahead of every module a section selects, which is where Prebid's and APS's ran. Prebid is a builder like any other, selected in `[auction]`, and APS's implementation builder carries its registration. The registry and the deploy check name neither. The page path read the identifier a renderer picks its bid by with APS's type tag and APS's payload key. The code that builds a renderer descriptor now states which payload key holds that identifier, with `picking_bid_by`, and the page path asks the descriptor. A descriptor built without the statement has no such identifier whatever its payload holds, so a key of the same name under another renderer's tag is still not read, which a test pins. The statement is no part of what is sent to a page.
The demonstration ad server is the implementation `ad-server.mock`, and it now lives in `crates/ad-server/mock`. The stock list offers it, so `[ad-server] module = "mock"` selects it as before and its settings do not change. Its 22 tests went with it. Core's tests of the ad server seam selected the mock because it was the one ad server there was. They select a stand-in compiled for core's tests only, `ad-server.fixture`, which reads an endpoint and a timeout, refuses any other setting and answers every request with no bid. That covers the plan compiler's tests of how a name resolves within `[ad-server]` and how a table's settings are checked, the registry's refusal of an auction implementation named in a section, and three tests that built a mock provider only because they needed a provider. One orchestrator test ran APS through the mock ad server and checked that the winner kept its seat, its bidder name and its renderer. It is a test of the two together, so it is in the stock list crate, where both are on offer, and it drives the orchestrator through its public interface. The bid dimension helpers the mock ad server admits a bid with are public, so an auction implementation in a crate of its own applies the rules core's response reader applies.
The Amazon Publisher Services demand implementation is `auction.aps`, and it now lives in `crates/auction/aps`. The stock list offers it, so a `[demand.<name>]` table with `implementation = "auction.aps"` selects it as before and its settings do not change. Its builder registers the page support the APS renderer needs when the auction plan selects an APS source. Its 38 tests went with it, and 24 more came from core, where APS had been the example the engine was tested with: - three tests of the renderer descriptor APS writes, - eight that run an APS source through the orchestrator and the response reader, - four of the plan compiler and the page registration, - six of the request APS builds, with both golden requests carried byte for byte, - three of the deploy check of APS's own settings. Those tests run against core's engine from APS's crate, so core offers what they need under its `test-utils` feature. That is the helpers that build an auction context, a demand table and a plan, two drivers that build a request for a plan's first source and read a response as that source, and the backend double in the platform test doubles. Core's own tests of routing, the plan compiler, the request builder, the response format and the page's bid map use core's plain OpenRTB source, or a renderer descriptor of the test's own tagged `example`, in place of APS. `ERROR_TYPE_HTTP_STATUS` and `ignored_bidder_params_count` are public, so an implementation in a crate of its own reports a failed call and counts ignored bidder parameters the way core does.
`integrations/prebid.rs` held three things: the page integration, the request, response and bidder parameter override code the Prebid Server demand runs, and the provider and combined configuration kept for the test build. The shipped code falls into two parts with nothing shared between them, so the file is divided along that line and nothing it does changes. The page integration stays in `prebid.rs`. The override rules and their engine, the transport headers, the response reader and the upstream error text join the demand in `prebid_server.rs`, with the test-only provider that the tests compare the demand with. Neither file refers to the other. The combined test configuration is divided the same way, so each file keeps the fields its own code reads. The page's is `LegacyPrebidConfig`, the browser settings and the bidder list a plan would supply. The demand's keeps the name `LegacyPrebidServerConfig`. Of the file's 178 tests, 57 stay with the page integration and 120 go with the demand. One is not carried: `register_rejects_invalid_bid_param_override_rule` checked that the page registration built from the combined configuration refused a bad override rule, and that registration no longer compiles override rules. The provider's own test of the same refusal stays. `validate_config_for_startup` was test-only and nothing called it, so it is gone.
The Prebid page integration is the module `auction.prebid`, and it now lives in `crates/auction/prebid`. `[auction] modules = ["prebid"]` selects it as before and its settings do not change. The stock list offers it first, so what it registers from the auction plan still runs ahead of APS's page support and of every module a section selects. Its 57 tests went with it, and seven came from core, where they checked rules that are Prebid's own: the removed `server_url` setting is refused, the two deploy rules for the external bundle, deploy and runtime validation both reaching Prebid's table, a bidder owned by the browser and by the server being refused only while the auction is on, and the shim loading deferred with the bundle route registered. The two bidder tests ran through the configuration blob in core and call runtime validation with Prebid's builder here, which is the step of the blob load they test. Core's shared test settings selected Prebid, so every test of core and of every other module ran with Prebid in it. They now select no auction module, and Prebid's tests select Prebid themselves. Core's tests that used Prebid as the one deferred browser module there was, or as the integration whose settings reach a page template, run on a stand-in compiled for core's tests only, `testing.deferred-fixture`, which carries a deferred module and writes its two settings into `<head>`. The settings tests of a leftover `enabled` key and of a module named twice use the example module they already had. `AuctionPlan::browser_bidder_codes` and `AuctionPlan::with_enabled` are public, so a page module in a crate of its own reads the plan's bidder routes and a test switches a compiled plan off. The `ts` audit generator asks the stock list where Prebid is selected, as it does for every other module, and the `ts prebid server inspect` tests read Prebid's configuration type from Prebid's crate.
The Prebid Server demand implementation is `auction.prebid-server`, and it now lives in `crates/auction/prebid-server` with its request extensions, its bidder parameter overrides, its transport headers, its response reader and the provider kept for its tests. The stock list offers it, so a `[demand.<name>]` table with `implementation = "auction.prebid-server"` selects it as before and its settings do not change. Its 124 tests went with it. Sixteen more are in its crate, run through core's driver from outside core: - eight that core held for it, which are the body consent by source and forwarding mode, the debug query fragment, the ordered overrides with the stored request as fallback, each impression paired with its slot's parameters, the stored intent applied after overrides, empty parameters falling back to a stored request, the exact request golden, and two sources run through the orchestrator, - eight for the Prebid Server half of a test core keeps, which are the consent matrix, the language tag, a disabled empty candidate, the signed request golden, the unsigned request identity, the endpoint completed through the plan, the refusal of all-eligible routing and the default timeout. Both request goldens are carried byte for byte. Core's auction tests used Prebid Server as the implementation that departs from the plain one. They run on a stand-in compiled for core's tests only, `auction.fixture`, which serves stored requests, forbids all-eligible routing, takes bidder parameters, keeps the unsigned request identity and completes an endpoint that names a host alone. Twenty-nine tests of routing, the plan, the formats and the driver pass on it with the name alone changed, and six read the stand-in's extension or its endpoint where they read Prebid Server's. Their names, labels and messages say what they test. `ResponseAdmissionDiagnostics`, `build_bid_dimension_index`, `apply_notification_policy` and `ProviderSlotInput::allows_stored_fallback` are public, so an implementation in a crate of its own admits bids, applies the notification policy and reads a slot's stored request intent the way core does. Two test drivers are added under `test-utils`, `route_to_first_source` and `routed_transport`. Prebid's test of the browser injection compiles its plan with Prebid Server's builder, the `ts` tool reads the implementation's name from its crate, and the stock list states the two rules its order carries.
…list The adapter's test settings select Prebid and a Prebid Server demand source, as a deployment does. Two test helpers compiled the auction plan, and one of them built the registry, from core's built-in modules alone, which held while both lived in core. With Prebid and the Prebid Server demand in crates of their own the helpers failed, and the adapter's test binary stopped at the first of them. Both helpers now take the stock list's builders, which is what the adapter's own entry point does. The two commits before this one left the Fastly tests failing, and they pass again here (3,572 in the Fastly build).
The integration guide, the integrations overview, the configuration guides and the onboarding page still described an integration as code inside `trusted-server-core`, or as "still in core" with the name its crate would have. Every page integration now has a crate of its own, so they say that: an integration is a crate under `crates/<type>/<vendor>/` here or in a repository of the vendor's, and a module's name is that crate's path. The agent notes list the module crate folders in the workspace layout, and the onboarding table gains the rows for a module crate and for the stock list.
Core's error type had a `Prebid` variant that only Prebid raised. Prebid's page integration and the Prebid Server demand raise `Integration` under their own names now, as every other module does, and core's error type names no vendor's variant. A reader sees no difference. Both variants answer 502 with the generic message, and the detail is logged as "Integration error (prebid)" or "Integration error (auction.prebid-server)" where it was "Prebid error".
`models.rs` held `AdResponse` and `Callback`, the response of one ad server, kept with dead code allowed "for external ad-server compatibility". Nothing in the workspace reads either type, so the file goes, with the ten tests of how the two types deserialize. The `Gam` error variant named Google Ad Manager and nothing raised it, so it goes from the error type and from the tables that list every variant.
Core's OpenRTB model held `RequestExt`, `PrebidExt`, `ImpExt`, `PrebidImpExt` and `ImpStoredRequest`, the typed `ext.prebid` objects of a request and an impression. Core's driver writes an implementation's extensions as the objects the implementation hands it and uses none of these types. Only the provider that Prebid Server's crate keeps for its tests builds a request through them, so they sit beside it there, compiled for its tests alone.
The limit on the zone a browser names for a slot was `MAX_PREBID_ZONE_BYTES`, and the admitted request headers were described as approved for "Prebid transport forwarding". Routing admits both for every demand source, so the limit is `MAX_ZONE_BYTES` and the headers are approved for forwarding to a demand source.
Each module crate says who maintains it in `[package.metadata.maintainers]` of its manifest, the way Prebid.js requires a named maintainer of every adapter. Three crates did not: the seam probe, the stock list and, in part, Testlight, which named an owner and no status. They do now, and the demonstration ad server's entry, which read "No vendor has not yet adopted this crate", says that the crate is the project's own. A status is one of three. `vendor owned` is a crate its vendor has adopted, `seeking vendor owner` is one the Trusted Server maintainers hold for a vendor, and `project owned` is one that is the project's own. A test in the stock list crate reads the manifest of every crate under `crates/<type>/<vendor>/`, and its own, and fails for one with no declaration, no owner or a status that is not one of the three. The integration guide states the convention and its checklist asks for it.
Deploy validation reads the integration builders the process registered, because the app config validates through a trait that takes no arguments. The `ts` tool registers the modules a stock build ships before it validates. Three other places validate a configuration in a process of their own and registered nothing, so once the Prebid Server demand left core they refused the fixture's `[demand]` source as an implementation the build does not have: - the integration test crate's Viceroy configuration generator, which is the "Generate integration Viceroy configs" step of the integration workflow, - the integration tests' own configuration envelope, - two of the command line tool's integration tests in `audit_config_diagnostics.rs`, one of which asserted a refusal and would have passed for the wrong reason. Each registers the stock list first, as the tool does. The tool's own unit test of a refused candidate validates once before the code under test registers, so it registers too. The integration test crate gains a dependency on `trusted-server-modules`.
`generated_blob_verifies_and_applies_origin_override` builds the envelope and then loads settings from it the way an adapter does at startup. It called the loader that knows core's own modules alone, so it refused the fixture's Prebid Server source once that implementation left core. It passes the stock list, as every adapter does. The integration workflow runs this binary's tests before the container tests, so the failure also hid whether those run.
The plain OpenRTB demand implementation is `auction-protocol.openrtb`, and it now lives in `crates/auction-protocol/openrtb`. The stock list offers it, so a `[demand.<name>]` table with `implementation = "auction-protocol.openrtb"` selects it as before and its `request_ext` and `imp_ext` settings do not change. It is the project's own, and its manifest says so. It was the last module in core's `integrations` folder, which now holds the registry, the builder a module registers with and the JavaScript asset proxy. Core's built-in list offers no auction implementation. What reads an ordinary OpenRTB response is core's and not this implementation's, so `parse_openrtb_response` moves beside the response extraction it wraps in `auction::openrtb`, and stays public for an implementation in a crate of its own. Core's tests of the plan compiler, the routing, the request driver and the page path ran on the plain implementation. They run on a stand-in compiled for core's tests only, `auction.plain-fixture`, which takes every eligible slot, inherits the auction's timeout, leaves its endpoint as written and writes the `request_ext` and `imp_ext` its table sets, so both golden requests stay in core unchanged. `demand_named` takes the implementation its sources run. Two of core's tests were the implementation's own and went with it, under names that say what they cover: - `openrtb_extensions_are_bounded_and_cannot_claim_reserved_fields` is `static_extensions_are_bounded_and_cannot_claim_reserved_fields`, - `standard_static_extensions_have_no_invented_bidder_param_location` is `static_extensions_have_no_invented_bidder_param_location`. The crate also holds both golden requests, signed and unsigned, byte for byte as core holds them, a test that its endpoint is left as written and the test that its name is its folder. The Prebid crate's two tests of a bidder that runs on both sides name the plain implementation and hand its builder to the validation they call.
…tegration-seam-impl
Add the integration seam and one selection convention for every type
Stacks on the module documentation set (#1047), the last of the five pull
requests that decompose #838, so this pull request sits on the whole module
series. It also depends directly on the
EdgeCookieModuleandDeviceModuletraits from the Edge Cookie module seam (#1043) and the device and location
seam (#1044), whose modules a registration can now carry, and on the
permission model (#1045), where an unset
[geo] modulemakes no host geocall. The stack has six pull requests (#1043, #1044, #1045, #1046, #1047,
#1094), each targeting
main, with this one sixth.Compare
split/5-response-hook-docswithsplit/7-integration-seam-implto 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
On
main,trusted-server-corecarries 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 and then moves every integration
module through them. An integration ships in its own crate with its Rust, its
configuration type, its deploy rules and its tests, it can carry its browser
script too, and an adapter composes that crate into a deployment at startup
without core naming the vendor. No existing integration changes behavior, the
same modules compose to the same served bytes and
?v=hash, and the modulesa stock build ships register through the same path a vendor crate uses. One browser module's own bytes change, because the Prebid
module's console error for a bidder with no adapter names
[auction.prebid.bundle.modules].What is now composable:
IntegrationBuilder::new(id, source, build, validate), passed to an adapter'sroutes_with_registrations, which builds the registry withIntegrationRegistry::with_plan_and_registrations.with_js_module(CarriedJsModule { source, sha256 })on the registrationvalidate_settings_for_deploy_withget_settings_from_config_store_withandsettings_from_config_blob_with, which validate against the builders a deployment composes and run each builder's validate function.with_request_preparer(...), run byIntegrationRegistry::prepare_request.with_response_finalizer(...), run byIntegrationRegistry::finalize_responsewith what the module's request hooks left for the request.with_secret_settings(...), looked up as the settings load when a section selects the module.with_plan_registration(...)and.with_plan_validator(...), for a module whose page support follows what the plan selects.with_ec_module(name, ...),.with_geo_module(name, ...)and.with_device_module(name, ...), each selected by that name in[ec] module,[geo] moduleand[device] module.with_module_name("<type>.<name>")on the builder, selected in[<type>]withmoduleormodules.with_demand(...), selected by[demand] modules.with_adserver(...), selected by[ad-server] moduleOne convention for every type
Everything a deployment can switch on is a module, selected the same way: the
Edge Cookie identity, the location and device lookups, the permission signals,
the demand sources in an auction, the ad server that picks the winner, and the
page integrations.
The types are
ec,geo,device,permission-signal,demandandad-server, and the section of each page integration's type,cmp,tag,ad-tag,bot-protection,identity,audienceandframework, withauctionandproxyselecting modules beside their own settings. A typethat runs one takes
module, a string, a type that runs several takesmodules, a list, and a name with nothing to set needs no table at all. A table's name is the implementation it configures,unless the table carries an
implementationline naming the implementationby its module path. A demand table always carries one, because
demandisnot the type its implementations are named under, which is how two Prebid
Servers run side by side under names a deployment chooses for itself. A name
is parts joined by
., each of lower case letters, digits,_or-. Amodule from a crate is named by its folder below
crates/, so it may bewritten in full, as
permission-signal.gpc, or with its section's type leftoff. Core's own modules such as
hmactake bare names, anda
[demand]or[ad-server]name is snake_case because it may be a labelof the operator's own.
Five things stop a deployment rather than being tolerated:
[<type>.<name>]table that its type's selector does not name.misspelt table of Trusted Server's own meets.
implementationline, naming an implementation thisbuild does not have. The message lists the ones it does have.
it does not recognize.
demandorad-serverendpoint that is not HTTPS. Plain HTTP is allowedonly to
127.0.0.1,::1orlocalhost, so a local test stack runs withoutcertificates and nothing leaves the machine unencrypted.
A configuration written for the previous spelling is told the new one rather
than half read: a
providerkey in[demand]or[ad-server]is refused withthe key to write, an
[adserver]table is refused with its new name, and an[integration]table is refused with where modules are selected now.The rules the auction tables and the module sections share live in one
place,
crates/trusted-server-core/src/provider_table.rs,whose
SectionModulesreads a section'smoduleormodulesand thetables of the modules it selects, and
TypeSectionsinsettings.rsreads every top-level table core does not read itself as one. 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 selects modules the same way
The ad server, plain OpenRTB, Prebid Server and APS are implementations that
a crate registers through
.with_demand(...)and.with_adserver(...)on itsbuilder, selected by
[demand] modulesand[ad-server] module. The fourthat ship here are
auction-protocol.openrtb,auction.prebid-server,auction.apsandad-server.mock, incrates/auction-protocol/openrtb,crates/auction/prebid-server,crates/auction/apsand
crates/ad-server/mock,and core's own list offers none.
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.rsresolves each selected name to a registered implementation, so
crates/trusted-server-core/src/auction/profile.rsand its fixed list of threeprofiles are deleted.
main's Prebid Server changes since the fork sit inside that split. A storedrequest is sent only when the slot's intent allows the fallback, an
implementation that has no demand for an impression marks it omitted, the
driver leaves those impressions out and makes no request when none is left,
and the parse state keeps only the slots that were sent, so a response cannot
be read against an impression that never went.
[auction]keeps the settings that belong to the auction itself, beingenabled, the whole-auction timeout and creative handling, with[auction.bidders.<code>] modulenaming the demand source a browser biddercode is sent to.
The module interfaces are asynchronous, and modules get the services
Before this change every module trait method was synchronous while every
platform service was asynchronous, and a module was handed no services at
all, so a module 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
module interfaces (
EdgeCookieModule::generate,DeviceModule::detectandPlatformGeo::lookup) are now asynchronous and take&RuntimeServices, andso 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 module for areplacement identifier.
The traits keep their
Send + Syncbound and use#[async_trait(?Send)], asPlatformHttpClientin this repository already does, so a module stays safeto share while its future stays on one thread.
Two tests in
crates/trusted-server-core/src/ec/mod.rs,a_module_reads_a_platform_service_through_the_services_it_is_givenanda_geo_module_reads_a_platform_service_through_the_services_it_is_given,drive a module 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 implementation supplies, serialized flat so the
response a page receives is byte for byte what it was, and
ApsRendererV1moves into APS's crate. DataDome's cache and origin-path marker becomes
a neutral
PersonalizedResponserequest extension that any integration mayset, so core acts on the marker without knowing which integration asked for it.
main's split between the reader's origin requirement, decided before theresponse is stripped, and the request's, decided after, is carried into that
neutral marker rather than keyed on one vendor's tag.
One design decision made while stacking this on #1045
Two designs for
[geo]met when this branch moved onto the stack, and theydisagreed 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 toDisabledGeo,"platform"is the explicit opt-in to the adapter's own lookup,and any other value names a module that declared a geo module.
Where to start
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_module_generates_an_identifier_with_the_modules_prefixshows themodule's own Edge Cookie module ran, because its identifier starts
seam-probe-, which the built-in HMAC module cannot produce, andec_selector_naming_a_module_starts_the_adapterstarts the Axum router withthat module selected.
crates/testing/seam-probe/src/lib.rs,the fixture those tests drive. It is a workspace member and a dev-dependency
of the Axum and Fastly adapters only, so no production build reaches it. It names its
module from its folder,
testing.seam-probe, and one registration carriesa browser module, a proxy route, a request preparer, geo, Edge Cookie and
device modules and its own settings table. Its builder also registers a
demand source and an ad server, which
[demand]and[ad-server]select.crates/trusted-server-core/src/provider_table.rs,the table
[demand]and[ad-server]share, which takesmodulesormodulefrom its selection type, andauction/plan.rs,which resolves
[demand]and[ad-server]names to registeredimplementations and reports an unknown one by listing the ones this build
has.
crates/trusted-server-core/src/integrations/registry.rs,with_plan_and_registrations. Building the registry refuses a duplicate id,naming both sources, runs a builder when a section selects its module and
refuses a selection no module answers to, listing what the section could
select. It checks the SHA-256 of a carried browser module, and resolves
[ec] module,[geo] moduleand[device] moduleagainst the namesthe running registrations declared their modules under.
crates/trusted-server-core/src/tsjs_bundle.rs,new. Composition of the served script moves out of
trusted-server-jsandinto 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 filecompare the composed bytes and hash against
trusted_server_js::concatenate_modulesandtrusted_server_js::concatenated_hash, which is the evidence thatcomposing in core moves no
?v=value.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] modulesdoes notselect is caught before a push.
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_contractpins theserialized form and
renderer_bid_id_key_matches_the_serialized_formpinsthe one field the publisher reads by key, both in APS's crate,
crates/auction/aps/src/lib.rs.crates/trusted-server-core/build.rscarries two jobs. The migration guard used to list core source files by hand
with
include_str!, so a vendor moving out of core would break the buildrather 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. The second job
is
main's, writing the manifest's default config store id into the build.The integration modules move out of
trusted-server-coreThe sixteen integration modules that sat in
crates/trusted-server-core/src/integrations/now live in fifteen crates of their own undercrates/<type>/<vendor>/, which is the layout #784 asks for. Each is selected and configured exactly as before. Each carries its own Rust, settings type, deploy rules and tests, and names its maintainers in its manifest (#1100). Core names none of them.The move is 31 commits, one per module, per extension point, per removal or per fault found, so each can be read on its own.
What an operator sees
Nothing changes in a configuration.
[cmp] module = "didomi",[auction] modules = ["prebid"], a[demand.<name>]table withimplementation = "auction.prebid-server"and every module's own table read as they did, and the routes, the browser module names and the wire names (aps,adserver_mock,gpt) are the same.The modules
cmp.osanocrates/cmp/osanoidentity.lockrcrates/identity/lockraudience.permutivecrates/audience/permutivecmp.sourcepointcrates/cmp/sourcepointcmp.didomicrates/cmp/didomitag.google-tag-managercrates/tag/google-tag-managertesting.testlightcrates/testing/testlightframework.nextjscrates/framework/nextjsad-tag.googleandad-tag.google.diagnosticscrates/ad-tag/googlebot-protection.datadomecrates/bot-protection/datadomead-server.mockcrates/ad-server/mockauction.apscrates/auction/apsauction.prebidcrates/auction/prebidauction.prebid-servercrates/auction/prebid-serverauction-protocol.openrtbcrates/auction-protocol/openrtbA module is named by its crate folder, and a test in each crate holds its
MODULEconstant to that folder.crates/trusted-server-moduleslists the modules a stock build ships, in the order their hooks run. Every adapter builds its state and loads its settings with that list followed by the builders a deployment added, and thetstool registers it for deploy validation. Offering a module does not run it, because the registry builds a module only when a section of the settings selects it.What stays in core's
integrationsfolder is the registry, the builder a module registers with and the JavaScript asset proxy, which is core's own. Core's built-in list offers no auction implementation.Maintainers
Every crate outside core names who maintains it in its manifest, the way Prebid.js requires a named maintainer of every adapter.
vendor ownedpermission-signal/mtm(51Degrees)seeking vendor ownerproject ownedA test in
crates/trusted-server-modulesreads all 24 manifests and fails for one with no declaration, no owner or an unknown status. The integration guide states the convention.What core gained to let them go
Three extension points, each vendor neutral, each its own commit with its own tests.
Core also offers its auction test helpers under its
test-utilsfeature, so an implementation's crate tests itself against core's request builder, response reader and orchestrator. About fifteen small items of core's auction code are public for an implementation outside core (bid admission, the notification policy, a slot's stored request intent, the plan's bidder routes), and the reader of an ordinary OpenRTB response sits in core's own OpenRTB driver for any implementation to use.Two places where core read a vendor's own data are closed by the moves. The
page's bid map read the APS renderer's bid id by APS's names, and a renderer
descriptor now carries the name of the field that holds its bid id, stated by
the implementation that builds it, which the bid map asks for. DataDome's
marker for leaving its browser tag out of a page was a named type in the page
path, and it is DataDome's own request state now.
Tests
A module's tests went with it. Where core held a test of a vendor's own behavior, the test moved to the vendor's crate and runs through core's public interface. Where core used a module as its example of a kind of module, the test stayed and runs on a stand-in compiled for core's tests only. There are seven, being four page modules (one that streams, one that tags a page, one that acts on a single request, one with a deferred browser module), two demand implementations (a plain one, and one that serves stored requests) and an ad server.
Core's shared test settings selected Prebid, so every test of core and of every other module ran with Prebid in it. They select no auction module now.
integrations/prebid.rswas 9,994 lines holding the page integration, the Prebid Server request and response code and a provider kept for the test build. Its shipped code falls into two parts with nothing shared, so 27648c0 divides the file inside core without changing behavior, and the two halves then move in 39f1240 and dd7f232. The test-only provider and its combined configuration are kept and divided the same way. The Prebid Server, APS and plain OpenRTB request goldens, two each, are carried byte for byte.One test is not carried.
register_rejects_invalid_bid_param_override_rulechecked that the test-only page registration refused a bad override rule, and that registration no longer holds override rules (the provider's own test of the refusal stays). The ten tests of the unused ad server model inmodels.rswent with it. Compared by name, every other test core had before the move is in core or in a module's crate after it, 60 of them under a name that says what the test covers now, such asa_bot_protection_block_is_refused_by_the_status_checkfora_datadome_block_is_refused_by_the_status_check.Size
Every
.rsfile undercrates/trusted-server-core/src, tests included, because a module's tests leave core with the module:mainAgainst
main, core is 9.5 percent smaller with comments and 12.8 percent smaller without, and that is with the module seams and the permission model thatmaindoes not have. A comment line is a line that holds only a comment, and blank lines are counted in both columns. The sixteen modules are 41,581 lines in their crates, 38,735 without comments.What the moves found
tsaudit generator named moved modules through core. It asks the stock list where a module is selected now (e08ef73).Cookieheader of every request. A stock build still does that, and a build without the Google crate forwards cookies as they arrived.<head>for a tag to be written into, so it could not fail. It asserts something that can.module = "bot-protection.datadome") did not have its secrets looked up, because the lookup read the short name alone. The second extension point reads both.tstool's integration tests validate settings in a process of their own and registered none, so the first CI run of the move refused the fixture's Prebid Server source in the integration workflow and in the tool's browser tests. Each registers the stock list first, as the tool does (9ce9e3e). One test of the generator's own loads settings from the blob it built, as an adapter does at startup, and it passes the stock list to the loader (190ea05).Why this rather than what
maindoes todayCore names vendors it has no business knowing.
crates/trusted-server-core/src/auction/plan.rsonmainholds
MOCK_MEDIATOR_ID = "adserver_mock"and refuses any other ad server. Itspecial-cases the profile ids
prebid-serverandapsby name when itcompiles a plan, and
crates/trusted-server-core/src/auction/profile.rsonmainholds the three profiles a deployment may choose, being
standard,prebid-serverandaps, as a fixed list with a closed enum behind it. Avendor 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 = trueinside its own block. A demand source is switched on by akey appearing in
[auction.providers]. A permission signal is switched on byits name appearing in a
sourceslist. An Edge Cookie module is switched onby a selector naming a nested
[ec.providers.<name>]block. Two of those fourare on
maintoday. The other two arrived inside this stack, in the EdgeCookie module 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 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:
quietly ignored. On
mainan[integrations.datadome]block that outlivesits 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
mainthe entry is what switches the demandsource on, so a stale one is a live bidder nobody meant to keep. Now a table
has to be named by its type's selector line or the deployment does not
start.
keys it does not know, so
timeout_milliswhere the setting istimeout_msstops 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.
modules = ["pbs_main"]becomingmodules = ["pbs_main", "aps_main"]sayswhat happened. On
mainthe same intent is a new nested block, aprofilestring, a
profile_configobject and aprotocolkey, and a reviewer has toreconstruct the outcome from four places.
be HTTPS, or HTTP to a loopback host for a local test stack.
[auction.providers],[auction] mediator,[adserver],[integration]or a
providerkey is refused, and the error names the table or key thesetting moved to
instead of leaving the operator to search for it.
For the people who write modules, a module plugs in through one
registration and inherits every check above without writing any of them. Core
names nothing a crate outside it supplies, so shipping a demand source, an ad
server, an identity module, a geo module or a device module means publishing a
crate, not editing core and waiting for a core release. The seam probe shows
this from the outside, because it is a crate core does not know about. Through
the real router or the real startup path it reaches a proxy route, a carried
browser module, a request preparer, a geo module, an Edge Cookie module, a
demand source and an ad server. Its device module is resolved by
[device] module, and the Fastly adapter's own tests show a registered devicemodule classifying a request.
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
[integrations.<id>]withenabled = true, and[integration] module[cmp] module = "didomi",[ad-tag] modules = ["google"]and so on, with[<type>.<name>]only for settings[ec.providers.<name>][ec.<name>][permission_signal] sources[permission-signal] moduleshost-signals,client-fixed,gpp-sale-opt-out,gpp_sale_opt_out,us_privacyhost_signals,client_fixed,gpp,us-privacy[auction.providers.<id>]withprotocol,profileandprofile_config[demand] modulesand[demand.<name>], withimplementationand the settings flat in the tableprofile = "standard"implementation = "auction-protocol.openrtb"profile = "prebid-server"andprofile = "aps"implementation = "auction.prebid-server"andimplementation = "auction.aps"[auction] mediator = "adserver_mock"and[integrations.adserver_mock][ad-server] module = "mock"and[ad-server.mock][integrations.aps] rendering_moderendering_modein the[demand.<name>]table of theauction.apsimplementation[debug.auction_html_comment_options] include_mediator_responseinclude_adserver_responseThe word mediator goes with those keys. It is "ad server" in prose and
ad-serverin configuration, and the auction response metadata that readparallel_mediationnow readsparallel_adserver.A file in the old shape is not read on a best-effort basis.
[auction] providersand[auction] mediatorare both refused with a message namingwhere the setting moved to, and so are
[adserver],[integration]and aproviderkey.ts prebid server inspectfollows the file. It reads the[demand.<name>]tables whose
implementationline isauction.prebid-server, and the[auction.prebid]table. Each demand sourceit reports carries
selected, which is whether[demand] modulesnames it,and the report's own
selectedis whether[auction] modulesselectsprebid, in place of the removedenabled_explicit.How it was verified
The head this pull request shows now is
49bdf21b8, which merges the move ofthe integration modules, 31 commits ending at
f5a2858fa, intoa90a141bc,the head that carries
mainat 182fdf4. The tree of49bdf21b8is the tree off5a2858fa.On
f5a2858fa, on Windows,cargo fmt --all --check, the core suite (2,557tests and doctests), the Fastly alias under Viceroy (3,565 across its test
binaries, the Fastly adapter's and every module crate's included), the Axum
alias (897 tests, which runs every module crate's tests natively), the
Cloudflare (56 tests) and Spin (90 tests) adapter suites, the cross-adapter
parity suite (17 tests), the seam-probe fixture's own tests (11), the Viceroy
configuration generator's own tests (8) and the 29 tests of the integration
target that need no container, Clippy with warnings denied on all four
adapters, the permissions inspector's 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 the audit generator, the bundle command,
ts prebid server inspect, the environment overlay test and the tests of deploy validation werechanged by reading and are verified by CI alone, where the host-target Clippy
of the CLI and the CLI's own tests on Ubuntu and macOS pass. CI caught two
faults that way, a type error in the audit generator on the section change,
fixed in ad734fd, and the settings validated without the stock modules that
the section on the move records, fixed in 9ce9e3e and 190ea05. Four
browser script files changed with the move, being two comments and a test
fixture path that named a Rust file or the Google Publisher Tags bootstrap
script by its place in core. CI runs the browser script's lint, format check
and tests, and they were not run again locally.
On CI, every workflow passes on
49bdf21b8: Run Tests, Run Format, Integration Tests, Permissions Inspector and CodeQL Advanced. Run Tests passed on its second attempt. The first failed in one test of thetstool,collects_gpt_slot_from_local_fixture, on a browser launch timeout, which #1256 describes. GitHub's codescanning check is the one check that shows as failing, because CodeQL carries one open
alert on this branch and on no other in the stack,
alert 192
(
rust/cleartext-logging, high, raised on 15 September), on the orchestrator'slog line that names the ad server, its logical budget and its transport
timeout. The move of the modules added no alert. The query reaches that line
by tracing a test helper's secret store into the runtime services the
transport timeout is canonicalized through, and the line logs an identifier
and three millisecond values, so the alert stands as reported, with the
reason in a comment at the line, rather than being worked around in the code.
The same log line is on
main, with the word mediator where this branch saysad server. Dismissing the alert needs security write access on this
repository.
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.
Core's auction engine still builds and reads OpenRTB itself. Only the
plain demand implementation moved, so OpenRTB as a translation at the edge of
the auction is a later change.
Vendor logic in core's own files is mostly untouched. What sat in an
integration module has moved. What sits in core's own files has not, for
example Prebid's identifier cookie handling, the Prebid slot parameters in the
creative opportunities settings and Google Ad Manager slot paths. Four small
pieces of it are removed, being the
Prebiderror variant (Prebid's cratesraise
Integration, which answers the same), the unusedmodels.rsandGamerror variant, Prebid's typed request extensions, and two Prebid names in the
routing code.
Each vendor's browser script is still in
crates/trusted-server-js, andcore TypeScript still imports APS directly (section 8, item 8).
auction.tsand
types.tsimport and name the APS renderer, so moving APS's Rust did not move its
browser code.
The consent decoders are still in core. The TCF, GPP and US Privacy
decoders sit in
crates/trusted-server-core/src/consent, and the permissionsignal crates read the consent context core decoded.
The stock list is written by hand (section 3.1). The design discovers the
built-in integrations at build time from their directories. Core's own list
holds the JavaScript asset proxy alone, and the modules a stock build ships
are listed in
crates/trusted-server-modules,written in hook order, because the order of that list is the order the page
hooks run in, and a generated list needs that order stated another way
first.
Page changes are still the four hook traits (section 3.7). The middleware
contract and the
[[fetch]]and[[serve]]entries are designed in #1084and are not in this pull request. They follow in a pull request of their
own.
A crate that supplies only an Edge Cookie, geo or device module does not
run from its selector alone (section 3.6).
[ec] module,[geo] moduleand
[device] moduleselect a module by the name its registration declaredit under, and the module that supplies it has to be selected in a section as
well, because the registry builds only what a section selects.
[ec],[geo]and[device]are read by core and are not sections that run abuilder, so a crate that supplies only one of these modules cannot yet be run
from its selector alone.
[geo]and[device]also hold no settings tablefor a module. The first vendor identity, geo or device crate needs the
first, and a geo or device crate with settings of its own needs the second.
Nothing is refused for a missing host signal (section 3.6). A registration
declares no host-signal requirements, so a module that needs a signal the
running adapter does not expose is not turned back at startup.
No test checks the order of several hooks on one registration (section 6,
item 1). The acceptance round trips run through the Axum adapter and, under
Viceroy, through the Fastly adapter, which is where items 1 and 2 ask for
them. 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.
A deployment's own module is not known to the stock CLI (section 8, items 1,
6 and 15).
ts config validateandts config pushvalidate against themodules a stock build ships, so each of those modules' own rules runs on the
path an operator uses. A module from a crate a deployment added is not on that
list. Its validate function runs at startup for a deployment that loads its
settings with its builders, and wherever the deployment's own code calls
validate_settings_for_deploy_with, which is why the seam probe still repeatsits check inside its build function.
docs/guide/integration-guide.mdrecords the gap, and the operator path needs a decision, which is whether the
CLI is built per deployment with its vendor crates.
No auction runs through the probe's demand source or ad server (section 8,
item 10). Both reach the plan, the settings load and deploy validation from
outside core, and neither is driven through a request. What an implementation
outside core does during an auction is held by the tests in the
implementations' own crates, which drive APS and Prebid Server through core's
request driver, response reader and orchestrator, and the plain OpenRTB demand
through the request driver.
No test loads settings from a config store with registered builders on
Fastly (section 8, item 7). The Fastly adapter takes a module crate through
run_with, so no adapter is edited to compose one. The load with adeployment's builders is tested in core, and the Fastly adapter passes the
registered builders to it in one call that no test drives. The test that
registers through the process-wide list also leaves the probe on offer for
the tests that run after it in the same binary, where it runs only if the
settings select its module, which none of them do.
A module can still be resolved more than once per request (section 8, item
3). On
POST /auctionlocation is looked up to build the Edge Cookie contextand again in
handle_auction, and the seam probe's proxy route calls its geomodule as well. This pull request adds no per-request module 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.
The
ts auditdetection patterns stay in the CLI. Thets auditcommand 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. What
the audit writes for a module it detects, being the section that selects the
module and its name there, comes from the stock list.
What the round trip does not exercise. The seam probe declares a proxy, a
carried browser module, a request preparer, and geo, Edge Cookie and device
modules. 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. The probe's device module classifies a request in the
Fastly adapter's tests, through the function the entry point calls for every
request, and no request sent to a router is classified by it.