Add the integration provider seam design spec - #1084
Conversation
6364acc to
18b65ff
Compare
18b65ff to
616b740
Compare
The five-PR series (IABTechLab#1043 to IABTechLab#1047) opens the identity, device and geo seams. The nine vendor integrations already in core sit behind the integration registry instead, which is a private table, so none of them can move out until that table is opened. This spec defines the one core change that opens it: public registration builders with a second input on IntegrationRegistry, browser JavaScript carried on the registration, startup validation as a hook, the same treatment for auction providers and the bid renderer contract, and neutral replacements for the two places where a vendor reaches into core. It then sets out the migration of all nine existing integrations, one PR each. The change is complete in itself: after it, no vendor move needs a core change. Written against the series' tree with the file and line references for every claim about the current code. Documentation only.
… registration capabilities Adopt the registration shape asked for in the review of PR 1043 and apply its rule consistently, so a module declares its identity, geo and device providers alongside its JavaScript and hooks (new section 3.6). Record the relationship to the pluggable-providers spec in PR 986, reorder the series so this spec and its core implementation precede PR 1043, and add the capabilities round trip to the acceptance list.
A probe integration built outside core and registered through an adapter exercised every seam end to end. Four things surfaced that reading the code did not: the operator CLI reaches deploy validation through a type that supplies no builders, so a vendor's own rules are skipped on the path an operator uses; a carried browser module's hash literal is hand-maintained and breaks on a checkout that rewrites line endings; a provider is resolved more than once per request, which the shared-backend principle needs a per-request context to fix; and one core reader still reads the APS renderer payload directly. Also records the construction-time verification of a carried module's declared hash.
The review of IABTechLab#1043 asked that spec changes land before the code that implements them, so a divergence is a decision taken in review rather than a ratification of something already merged. PRs IABTechLab#1043 to IABTechLab#1047 each carried the design document for their own step, and IABTechLab#1043 carried a 607-line spec describing device providers, geo providers, the permission model and the browser resolve endpoint, none of which is in that PR. Move all six series documents here, so this PR carries the complete normative set and no code: - 2026-07-30-pluggable-providers-design.md (from IABTechLab#1043) - provider-code-registry.md (from IABTechLab#1043) - 2026-07-30-permission-model-design.md (from IABTechLab#1045) - 2026-07-30-client-cycle-ec-resolve-design.md (from IABTechLab#1046, later revised by IABTechLab#1047) - 2026-07-30-integration-response-header-hook-design.md (from IABTechLab#1047) - 2026-07-30-provider-migration-rollout-design.md (from IABTechLab#1047) Each file is taken verbatim at the tip of the stack, so the later revisions are preserved: the provider-switching continuity section, the geo requires-signal floor, and the code-envelope paragraph IABTechLab#1047 added to the client-cycle spec. The revision-record tables are unchanged. No document's substance was edited. The only edits are to this spec's own status line, which said the PR adds one document and that the series specs land with IABTechLab#1047, and a revision-record row recording the move.
The rule that every trait method needs a production caller in the change that introduces it is right for a behavioural trait, where a method nothing calls is dead weight. It is wrong for an evidence interface, and we applied it there and should not have. An evidence interface describes what a request carries, not what today's code happens to read. Held to the caller rule it grows a method each time a vendor arrives, so a vendor cannot write against it and it cannot stay stable across a release. It also puts the boundary in the wrong place. What a provider may see is not the control, because what a provider may do with what it sees is the control, and that is the permission model's job. So RequestInfo carries what the request carries. The rule stands everywhere else. Addresses: docs/superpowers/specs/2026-07-30-pluggable-providers-design.md section 4, a rule applied to an interface it does not suit
"Mint" is jargon for something ordinary, which is making an identifier. It was defined in the glossary, so it was at least explained, but a reader should not need a glossary for the word that describes the main operation the specs are about. Fifty-one uses across five files, including the glossary row itself, now read create, creates, created or creating. Nothing else changed, so a reader comparing against the previous revision sees one word substituted and no argument altered. Addresses: plain language in documents others read
…style fixes Delete the reserved 51dd provider-code registry row so the unpublished vendor provider is not named in the public spec. Correct section 8's count from four to seven, replace several with many, and restructure clause-joining colons into separate sentences across the provider specs. The 51Degrees contribution attribution lines are kept deliberately.
The provider series design specs move to the spec-only PR (IABTechLab#1084) so they can be reviewed before the code that implements them. Three doc comments cited those files by repository path, which no longer resolves from this branch. Refer to each document by name instead, so the comment stays true whichever PR is read first.
3c7bc27 to
1800604
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Thank you for this, and for §8 in particular. Publishing what implementing the seam actually found, including three items that say your own work is not finished, is the right instinct and it is what makes the design half of this trustworthy.
The design is accepted. §3.6's rule is correct, and it is the rule the work below implements. Things the host supplies are platform services. Things a vendor supplies are capabilities of that vendor's module. What is being changed is the order and the structure, not the direction.
Five changes to sequencing and structure follow. Tell me where the reasoning is wrong and I will change my view, but I want these settled before more code lands on the current shape. The two items that need wider sign-off are named at the end.
1. One crates/trusted-server-integrations crate first, with a discovering build.rs
Move all thirteen integrations into a single crate and generate the registration table at build time by scanning the crate's own directories, in the manner crates/trusted-server-js/build.rs already scans dist/tsjs-*.js.
This is the change that delivers the neutrality goal. Core stops naming vendors in builders(), in validate_enabled_integrations, and in migration_guards.rs, and it stops naming them because nobody writes the list, not because the list moved. Three of §1's four hardcoded vendor tables become generated output.
It also resolves several of §8's findings without further design:
- §8.2, the hand-carried module hash and its line-ending fragility, goes away because hashes are generated exactly as core's are today.
- §8.6, a module's
validaterunning nowhere, is solvable in the generated table. - §1.2, the compile-time JS map, is solved by one scan that finds Rust and TypeScript together.
- The
migration_guards.rsdrift is structurally impossible once generated. Worth noting that this drift is not hypothetical:osanois registered inbuilders()and has no entry inmigration_guards.rsat all onmain. Nobody noticed because both lists are hand-written.
prebid stays in core as protocol support, as §3.4 already says.
The runtime injection seam in §3.1 is not required for any of the thirteen integrations you have, because they are all compiled from source in this repository. It becomes the right mechanism when a genuinely external vendor crate exists, and it should be added then, with that vendor as its first consumer.
2. Crate layout follows from that
§4 proposes nine vendor crates, one PR each. We are not doing that now. One crate holds all thirteen integrations, and a vendor gets its own crate when that vendor takes over maintaining it, which none of them does today. Every integration on main imports the same roughly twelve crates, all of which core already depends on, so nothing about dependencies argues for splitting them either. Splitting later is a directory move against a module that already has clean boundaries.
Two consequences for what is in #1094 now:
- Directory name equals package name, as everywhere else in the repository. The crate is
crates/trusted-server-integrations.crates/geo/fastlyas packagetrusted-server-geo-fastlybreaks that convention, andcrates/geo/is a category folder holding one item. - Host code stays in its adapter.
FastlyPlatformGeois 52 lines that already live atcrates/trusted-server-adapter-fastly/src/platform.rs:687, and the Fastly adapter is its only possible consumer. Extracting it into a crate gains nothing and contradicts §3.6's own rule that host-supplied things are platform services. The same applies to the 103-line Fastly device provider.crates/edgecookie/contains only a README and should be deleted until a vendor crate lands.seam-probeis a test fixture and should not be a permanent workspace member.
3. Permissions policy belongs in trusted-server.toml; permissions.yaml should not exist
Policy belongs in a [permissions] section of trusted-server.toml, beside the existing [consent]. We should not add a second configuration format, and policy should not be compiled into the binary.
The permission-model spec's §3.1 gives the reason for YAML. The draft required policy to flow through the runtime config pipeline, and that activation apparatus does not exist. That conflates two things. ts config push does exist. It publishes trusted-server.toml as a blob envelope, and TrustedServerAppConfig already carries the entire Settings struct. What does not exist is the staged multi-revision activation protocol the draft designed, with fleet quiescence, admission leases, and a hash-linked journal. No other setting needs that machinery. The publisher domain, the proxy secret, the EC passphrase, and every integration config all push atomically today, and policy is not different in kind.
What the move gains:
- One place to configure everything. Today policy is the only control that bypasses
ts config validateandts config push, and it is the most compliance-sensitive one. - The validation we already have, being
deny_unknown_fields,Validate, and startup errors, instead of a bespoke parser reimplementing duplicate-key and case-collision checks. - A publisher can correct a jurisdiction rule with a config push rather than rebuilding and redeploying a WebAssembly binary. For a compliance control that may have to change on a regulator's timeline, that difference is the point.
- §3.1's own recorded defect, that the
include_str!path reaches above the crate root, disappears.
Alongside the move, please trim the fifty-three Data Uses that have no signal mapping and no enforcement point. Only two carry enforcement weight today. Moving fifty-three inert flags into trusted-server.toml would carry the misleading operator view across rather than fix it, and the spec already acknowledges this departs from its own rule that a permission appears only with both a mapping and an enforcement point.
4. The EC provider spec rebases onto 1 and 3
Rewritten against a seam that already exists and policy that lives in settings, the EC provider work is materially smaller than #1043 as it stands:
- No
RuntimeServices::ec_providerslot to add and then delete, and noresolved_ec_providersecond field. Note that #1094 currently has both, plus the registration path, so identity has three injection paths where §3.6 specifies one. §3.6's sentence that theec_providerslot "goes" is not yet honored. - No per-adapter resolution divergence. Related, and worth fixing wherever this lands: on #1094
resolve_geo_providerandresolve_device_providerreturnResultand fail loudly on a bad selector, whileresolve_ec_providerreturns a bareOptionand falls back silently. §3.6 says an unresolvable selector is a startup error. - No double configuration migration. This is the strongest reason for the reordering. As it stands, #1043 migrates operators from
[ec] passphraseto[ec.providers.hmac], and §3.6 with sign-off row 8 then migrates them again to[integrations.<id>]. Two breaking configuration changes in one release cycle, the second withdrawing the first. Reordered, operators migrate once, into the final shape.
5. Some of this belongs in edgezero, not Trusted Server
edgezero-core already owns the platform layer, being app_config, blob_envelope, config_store, secret_store, key_value_store, store_registry, the router, the middleware, and the four adapters. Hooks::stores() returning StoresMetadata is already the seam run_app uses to inject store registries. Before we build more platform machinery in this repository, we should be explicit about which side of that line each piece sits on.
- The Fastly composition gap (§8 item 7) is probably an edgezero fix rather than a Trusted Server one.
trusted-server-adapter-fastlywrapsedgezero-adapter-fastly. If a deployment composes its modules throughHooks, the pattern already exists upstream and no Trusted Server adapter needs a new library target. Adding a[lib]here would work around the absence of something that may already exist one level down. RuntimeServicesoverlaps edgezero substantially. It carries a config store, a secret store, a KV store, an HTTP client, and a backend.edgezero-coremodels all five. Adding identity, device, and geo provider slots to it builds up a second platform abstraction beside edgezero's rather than using edgezero's.PlatformGeohas no edgezero counterpart. There is no geo abstraction inedgezero-coretoday. Either a geo lookup is a host capability edgezero should expose the way it exposes a KV store, or it stays here. Whichever it is, that should be a stated call rather than a default that follows from where the code happens to have been written.TrustedServerAppimplementsHooks::name()andHooks::routes()but notHooks::stores()(crates/trusted-server-adapter-cloudflare/src/app.rs:332). The store-injection seam edgezero already provides is unused here, which is worth resolving before we design another one.
Which of these should move is a question for whoever owns edgezero. I would rather ask it now than build a second mechanism here and answer it afterwards. It also makes change 1 smaller: if composition runs through Hooks, the integrations crate needs less new machinery than either this spec or #1094 assumes.
Spec corrections, verified against the tree
- Five of the seven specs are statused "Implemented" for code that is not on
main.EdgeCookieProvider,PermissionSet, andresolve_from_clientare all absent fromorigin/main. Merging this set as written would tell every later reader that shipped behavior exists when it does not. Please status everything unmerged as "Proposed". - §4, "thirteen vendor files" in
migration_guards.rs, is wrong. There are eighteen vendor files across twentyintegrations/entries;nextjsalone contributes six. Thirteen is the count of builders. And as above,osanohas no entry at all, which §4's migration plan depends on. - §3.5, GPT diagnostics "called by name from all four adapters", undercounts. There are nine
prepare_requestcall sites plus onefinalize_response, so the hook work in §3.5 is larger than stated. - §8.3, "a provider is resolved more than once per request", does not hold on
main. No integration or proxy calls geo. A real double resolution does exist, but it is core against core:handle_auctionatauction/endpoints.rs:262and the adapter'sbuild_ec_contextboth calllookuponPOST /auction. The underlying gap, that there is no per-request provider context, is real and worth keeping. - §8.5 understates the Spin gap. Spin does not merely skip inline admin stubs; it skips all six first-party route bindings (proxy, click, sign, proxy-rebuild), which carry real traffic. Cloudflare, by contrast, has no health route and so covers everything.
- Line references to correct:
settings.rs:166should be:215;auction/mod.rs:49points at the function header rather than the list;publisher.rs:4361should be:4369.
§8 item 7 is the load-bearing finding
Your assessment is right and the cause is more fundamental than the text states. The Fastly adapter has no [lib] section and no src/lib.rs, and every module is declared bare mod, so nothing in the crate is externally reachable. This is not a visibility modifier to widen; it is a missing library target. Axum, Cloudflare, and Spin each expose pub fn routes_with_settings and pub struct TrustedServerApp; Fastly exposes neither.
As things stand the seam is proven on the dev server and unreachable on the primary deployment target. §6's acceptance criteria should require the round trip on Fastly specifically, not on any adapter. Under change 1 this stops blocking the immediate work, since in-tree integrations compile in and nothing needs runtime injection. It must still be fixed before the first external vendor crate, and per change 5 the fix may belong in edgezero rather than here.
Disposition of the open pull requests
#1043 through #1047 and #1094 are superseded rather than rejected. The design in §3.6 is accepted, and most of the provider logic carries over onto the reordered base. Please do not read this as the work being discarded.
The order, with the first two independent and able to run in parallel:
- One
crates/trusted-server-integrationscrate with a discoveringbuild.rs, againstmain. - Permissions into
trusted-server.toml,permissions.yamldeleted. - The EC provider work, rebased onto 1 and 2.
- Device and geo selection, in the same shape.
- Per-vendor crate extraction, if and when a vendor takes over maintaining one.
Happy to discuss who does what. The Fastly composition gap is core infrastructure rather than vendor contribution, and per change 5 it may not belong in this repository at all.
The jurisdiction default should not name a country
[geo] default_country is required in every mode, and it must resolve to a rule in the policy file. There are three groups today, being gdpr-eu, gdpr-uk, and us-opt-out, so a deployment with global traffic and no geo provider has to nominate one country and then apply that country's law to every visitor. That is the wrong shape. It makes a neutral product take a position on whose rules apply, and the operator's honest answer is usually "I do not know where this visitor is", not "treat everyone as French".
The default should be a policy baseline rather than a country. Two forms worth considering, and I do not mind which:
- A named group, so an operator selects
gdpr-euor a strictest-available baseline directly without claiming a location. - A per-permission baseline written inline, for an operator who wants to state exactly what is allowed with no signal.
Either way the value says what is permitted, not where the visitor supposedly is. The requires-signal floor already exists for a failed lookup, so the machinery for a location-free baseline is there.
This also removes most of what I was going to send to the task force. The UK row shipping necessary.operations.storage: granted while the EU requires a signal, and Australia mapping to us-opt-out, are still policy positions that need someone with the relevant expertise to confirm with a citation, and the shipped table should carry that citation. But nobody has to ratify a default that no longer asserts a jurisdiction.
The crate layout, the configuration format, and the merge order are not task force questions. They are engineering calls and we should settle them here.
One further item to fix wherever the permission work lands: §7 of the permission-model spec records that publisher navigation and page-bids attach user.id under the provider gate rather than the sharing pair, so a storage-only grant can still place the EC identifier in user.id on those paths while /auction pair-gates it. That is an open inconsistency in the sharing boundary, and it should be closed rather than tracked.
|
Thank you for this. It is a serious review and it moves the design forward. Taking your points in order. 1. One integrations crate with a discovering 2. Crate layout. Here we differ. Whether a vendor wrote the code, or the Tech Lab team wrote it for them, should not decide whether it gets its own crate. Separating per vendor now makes ownership legible, and a crate boundary is enforced by the compiler, while a CODEOWNERS entry only assigns review ownership and constrains nothing about what code can reach. The maintainer can be the Tech Lab team today. If the Tech Lab team built the Permutive integration, the Permutive crate is theirs until Permutive take it over, and the handover then changes one line, not the structure. We take the point that host code stays in its adapter, which §3.6 already says, and we will follow the repository's crate-naming convention. The disagreement is only on timing. 3. Permissions. This one is fundamental for us and not something we can change 🙂. Deleting the dedicated permissions file breaks a deployment model that is very hard to change later. The transparency point first. A single, dedicated permissions policy that anyone can open and read is how Trusted Server shows exactly what a publisher permits, on its own rather than buried beside proxy secrets and backend hosts. Then the mechanism. Trusted Server should not decide policy for the deployer, and there should be no automatic default. Selecting the policy is a conscious choice when an environment is set up for the first time. The entity that compiles a deployment provides the policy it wants as part of the build, which is why the policy has to be a build-time artifact and not only a runtime config section, and that same entity can expose configuration override so the operator of the compiled variant can adjust it without rebuilding. Consider a provider in Europe that compiles Trusted Server with a European base and ships it to its customers, who get an interface to tune it. Two actors and two moments, with the integrator setting the base at build time and the operator adjusting it at runtime. A single runtime config section cannot express that, and neither can a compiled-only policy, so both layers are required. The repository should also carry multiple policy files for different test scenarios, because exercising the permission model across scenarios is part of the robustness of the service. Three things follow. Policy is not locked in the binary forever. The compiled base is the layer these PRs implement, and the operator override is the follow-on the permission-model spec already records as deferred, so a jurisdiction rule can change on a regulator's timeline without a rebuild. What the shipped jurisdiction rules should say, the UK storage row and the country baselines included, is a policy question for the group to confirm with citations, which is your point in the jurisdiction section too, and we would take it there rather than settle it in code review. And which file carries the compiled-in base and which one arrives through configuration at runtime is not something engineers need to debate. That choice sits with the people deploying, and testing proves every mutation of base and override works. 4. The EC provider and one injection path. Agreed. §3.6 specifies one identity injection path and we will honor it, collapsing to the single registration path. An unresolvable selector will fail loudly, while an absent selector keeps today's behavior of running with no Edge Cookie provider. Rebased onto a seam that already exists, the double configuration migration also goes, so operators migrate once. 5. What belongs where. The distinction that matters is which repository these live in. The geo provider, the device provider and the Edge Cookie provider are part of Trusted Server, not EdgeZero. EdgeZero supplies the host plumbing, and the provider capabilities are Trusted Server's domain, which is where a vendor goes to plug in. They are also deliberately off by default. Only User-Agent device detection runs by default, geo resolves no location until a provider is configured, and the Edge Cookie provider is selected by configuration, which is the same conscious-choice principle as point 3. Vendor code already lives in Trusted Server as the registered integrations, and the provider seam exists so a vendor can be added the same way. We are asking to use it for the purpose it was built. Where you are right is the platform plumbing rather than the providers. The config, secret and KV stores that Spec corrections. Taken, and thank you for checking against the tree.
On merging. We agree with most of this and will make the changes. The differences are narrow, being per-vendor separation now rather than later and keeping a dedicated permissions file. Rather than treat the current PRs as superseded and reset, could we agree a concrete sequence and owners for landing this so the provider work merges soon? We would like to keep this a direct back and forth to a merge. Thanks again for the depth of the review. |
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Requesting changes for three security or authorization failures and three contract gaps in the normative design. These findings are distinct from the existing feedback on sequencing, implementation status, Fastly composition, policy placement, and jurisdiction defaults.
Reviewed 180060402f91b4efa96f5823da8934d19c1c78f8 against d516a9e94249e10cbc36e41beb4269f9255cf407.
| page-script endpoint. The 2026-07-31 draft asked for an exact origin | ||
| allowlist as new configuration plus an optional session-bound CSRF | ||
| token. v1 derives the allowed set from `publisher.domain` (existing, | ||
| operator-trusted configuration) and admits the publisher's own |
There was a problem hiding this comment.
🔧 P1: Do not authorize every publisher subdomain to set identity
Authorizing every origin below publisher.domain recreates the identity-fixation path for a delegated, user-controlled, or compromised sibling subdomain. Such a page passes this suffix check and can issue the simple credentialed POST described in the threat model, while the EC cookie is scoped to .publisher.domain. Control of the DNS namespace does not imply that every subdomain is a trusted identity-setting origin. Please use an explicit serialized-origin allowlist and match scheme, normalized host, and effective port exactly. Add rejection cases for a sibling subdomain, the wrong scheme, and the wrong port.
| permission state and consent context so a provider can read them for | ||
| behavior beyond gating. The gate itself has already run before `generate` | ||
| is called, so a provider cannot use the fields to authorize itself. | ||
| - `required_permissions` on `DeviceProvider` and `PlatformGeo`: **present, |
There was a problem hiding this comment.
🔧 P1: Enforce or reject device-provider permission declarations
DeviceProvider::required_permissions is public but deliberately has no enforcement point, while the integration seam exposes DeviceProvider as an external vendor capability with request evidence and a possible shared backend. An external provider can therefore declare that its use requires permission and still run on every request. Operator selection is not a per-request consent decision. Please enforce the declaration before invoking a device provider, or reject nonempty device requirements until that gate exists. If the built-in classifier must run without advertising permissions, split that security role into a separate, narrowly scoped authority with restricted evidence and no general vendor egress.
| and reserialized. | ||
| - Every other admitted value-bearing source header in the exact mapping above | ||
| is singleton. Zero lines means omit the DataDome field. Exactly one valid | ||
| line is OWS-normalized and processed. Two or more lines, even identical, |
There was a problem hiding this comment.
🔧 P1: Duplicate request headers must not disable DataDome
Two instances of any admitted singleton header, including user-agent, origin, or authorization, currently skip the entire DataDome call through the fail-open path. Header multiplicity is attacker-controlled, and this spec requires adapters to preserve repeated field lines, so a bot can deliberately add a duplicate and receive the publisher response without validation. Please reject ambiguous security-singleton requests at the HTTP boundary, or omit an optional ambiguous field while still calling DataDome. Do not convert multiplicity into a protection bypass. Add duplicate-header cases to every adapter's qualification suite.
| marker shares the Edge Cookie's scope and lifetime and is expired | ||
| together with it on withdrawal, so a visitor who later re-establishes | ||
| the permission can resolve again. | ||
| - **The page leg is permission-gated before vendor contact.** The demo |
There was a problem hiding this comment.
🔧 P2: Define the browser permission-gating mechanism
This requires the vendor module to check the server's resolved permissions before vendor contact, but the design does not say how those permissions reach TSJS or combine with a fresh CMP check. Integration modules are self-executing IIFEs selected by provider configuration, while the endpoint gate runs only after the module may already have derived identity or contacted its vendor. A CMP signal alone also cannot reproduce the server's jurisdiction policy. Please define the browser-side authorization mechanism before the first vendor module lands, require module initialization to be side-effect free until that check passes, and add a browser test proving the vendor endpoint receives no request when permission is absent or withdrawn, including after BFCache restoration.
|
|
||
| - **The re-post guard must not depend on reading an HttpOnly cookie.** | ||
| Implemented as the draft's first option. The resolve response sets a | ||
| non-HttpOnly companion marker cookie (`ts-ecr=1`) carrying no identity, |
There was a problem hiding this comment.
🔧 P2: Bind the resolve marker to the selected provider
ts-ecr=1 records only that some client-cycle provider resolved in the past. On a switch from provider A to provider B, A's coded EC becomes unrecognized, but this marker remains until withdrawal or its one-year expiry. B's page script therefore sees the marker and never calls resolve, even though the migration contract says the switch restarts identity. Please bind the marker to the selected provider code and behavior revision, or expire it whenever the incoming EC is not recognized by the selected provider. Add an A-to-B client-provider switch test.
| APS can register from its own crate. Prebid stays in core as protocol | ||
| support rather than as a vendor integration. | ||
|
|
||
| The bid renderer contract is generalized in the same change. Today |
There was a problem hiding this comment.
🔧 P2: Add a browser renderer registration seam before moving APS
Generalizing Rust's BidRenderer is not enough to move APS out whole. TSJS core still imports parseApsRendererDescriptor from APS in core/auction.ts, imports APS dispatch in core/request.ts, and fixes AuctionBidRenderer to ApsRendererV1 in core/types.ts. Because build-all.mjs builds core/index.ts as a self-contained IIFE, those static imports remain in tsjs-core.js independently of carried integration modules. Please specify a browser renderer registry keyed by descriptor type. Integration modules should register their parser and dispatcher, core should reject unknown types safely, and a cross-language test should prove APS can be removed without changing or breaking the core bundle.
Why this PR exists
PRs #1043 to #1047 open the identity, device and geo seams. A vendor can
ship an Edge Cookie provider in its own crate and an adapter injects it,
with no core change.
The nine vendor integrations already inside
trusted-server-core(APS,DataDome, Didomi, Google Tag Manager, GPT, Lockr, Osano, Permutive,
Sourcepoint) do not sit behind those seams. They hang off the integration
registry, which is a private table in core, so none of them can move out
until that table is opened.
This PR adds the design spec for that one core change, so the migration
of every existing vendor is a single defined piece of work rather than a
question asked again for each vendor.
Spec:
docs/superpowers/specs/2026-08-27-integration-provider-seam-design.mdThis PR adds one file, the spec, and no code. It targets
maindirectly so the diff is only that file. It reads alongside the series'
specs, which land with #1047.
What the spec says
Read against the tree at
split/5-response-hook-docs, with file and linereferences for every claim:
IntegrationRegistry::newtakes only&Settingsand iterates apub(crate)table(
integrations/registry.rs:797,integrations/mod.rs:290). Thepayload type is already public, so an outside crate can build a
registration but has nowhere to hand it.
js_module_idsservesa module only when
trusted_server_js::module_bundle(id).is_some()(
registry.rs:1169).config.rs:136to:166).(
auction/mod.rs:49).privacy and origin fetch decision (
publisher.rs:4367to:4387)and GPT diagnostics through direct calls in all four adapters.
The proposed change opens each of those in turn, keeps
new's signatureso no existing caller changes behavior, and names no vendor in core. It
also generalizes the bid renderer contract (
BidRendererinauction/types.rs:216has one variant,Aps), so APS moves out whole.The change is complete in itself: after it, no vendor move needs a core
change. It then sets out the migration of all nine integrations, one PR
each, with what each one needs.
Acceptance the spec sets for the implementation
trusted-server-core, carrying its own JavaScript, registeringthrough an adapter and appearing in the served bundle with the right
hash. A seam is only proven by an implementation that is not the
built-in one.
CLAUDE.md, on all four adapters.Governance question this raises
The spec's §2 states a principle for the task force rather than for the
compiler: Tech Lab engineering owns core and reviews vendor crates, and
vendors ship and maintain their own integrations. That is the change that
removes the per-vendor core work the project pays for today, most
recently in #1054.
Declared interest
51Degrees is a vendor and would use this seam for its own integration
(#1072). The reasoning here applies to every vendor on the same terms,
ourselves included, and our own provider follows the same route.
Provenance
An AI assistant produced this spec under my direction, from a read of the
current code. I have reviewed it. It needs human review before the
implementation is written.