diff --git a/docs/superpowers/specs/2026-09-17-split-integrations-crates-design.md b/docs/superpowers/specs/2026-09-17-split-integrations-crates-design.md new file mode 100644 index 000000000..f91010352 --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-split-integrations-crates-design.md @@ -0,0 +1,3533 @@ +# Split Integrations into Dedicated Crates and One Ordered Configuration + +**Date:** 2026-09-17 + +**Status:** Proposed + +**Scope:** Move all concrete Rust and browser integrations into two statically +compiled workspace crates, introduce typed multi-capability registration, and +make ordered `[integrations]` configuration the single inventory for concrete +integrations and their auction providers. + +## Summary + +Trusted Server will separate concrete integrations from its neutral Rust and +browser runtimes. + +The workspace gains two crates: + +- `trusted-server-integrations`, containing all concrete Rust integrations. +- `trusted-server-integrations-js`, containing all integration-specific + TypeScript, JavaScript assets, fixtures, tests, and generated bundles. + +`trusted-server-core` will retain neutral integration contracts and execution +engines. `trusted-server-js` will retain the neutral browser runtime. The +adapters and CLI will use `trusted-server-integrations` as the statically linked +application composition root. + +Each Rust integration will expose one definition that may register multiple +typed capabilities. APS, for example, may register proxy, head-injection, +JavaScript, and OpenRTB-profile capabilities. Capabilities are subsystem-owned +types and traits, not a single `IntegrationType` enum and not a discriminator +in operator configuration. + +`[integrations]` will become the only operator inventory for concrete +integrations. An integration will own all of its settings, including any named +auction provider instances. Global `[auction]` settings will continue to own +cross-integration orchestration such as the auction timeout, bidder routing, +creative policy, and mediator selection. + +Cross-integration runtime order within the same comparable capability phase +will come exclusively from TOML declaration order. Fixed engine seams such as +immediate versus deferred loading and the diagnostics post-unified position are +named phases, not hidden integration priorities. The config push and +config-store representation will preserve order explicitly; filesystem +discovery order will never affect execution. For auction providers, that order +is also operational priority: it controls launch and response order, mediator +input order, and local equal-price tie-breaking. + +Repository discovery started from `origin/main` at `a4e01eb55`, not either +prior pull request discussed below. That historical discovery commit contains +the known P1 output defects listed as prerequisites here and is not an +acceptable compatibility golden. The 2026-09-29 review checkpoint is +`666953a0d`; it includes the post-discovery configuration-store, EC/EID, and +stored-request changes recorded below. It is a review checkpoint, not the final +implementation baseline. Implementation branching is blocked until every +prerequisite fix lands; the recorded baseline is then the resulting +`origin/main` commit plus captured Next.js/GTM/RSC and browser-runtime output +goldens. This design changes only the configuration, activation, and ordering +behavior called out explicitly in this document; all other corrected-baseline +runtime, browser, CLI, and cache behavior is preserved. + +This remains one design, but it has two merge milestones. The crate and runtime +boundary moves first without changing operator configuration. The ordered, +integration-owned configuration cuts over only after the compatibility-focused +boundary, including its explicitly listed baseline bug fixes, is running. The +milestones share one target architecture without +forcing the packaging move and configuration migration into one deployment. + +## Context + +At the historical discovery commit `a4e01eb55`, neutral registry machinery and +concrete integrations share `crates/trusted-server-core/src/integrations`. The +concrete Rust units are: + +- `adserver_mock` +- `aps` +- `datadome` +- `didomi` +- `google_tag_manager` +- `gpt` +- `gpt_diagnostics` +- `js_asset_proxy` +- `lockr` +- `nextjs` +- `osano` +- `permutive` +- `prebid` +- `sourcepoint` +- `testlight` + +The ordinary builder table registers twelve units. APS and Prebid are also +constructed from the compiled auction plan, and `adserver_mock` supplies the +current mediator. Core additionally imports concrete APS and Prebid code from +the OpenRTB profile and provider paths, concrete DataDome response state, and +GPT diagnostics lifecycle functions. + +Integration browser code shares a Node project with browser core under +`crates/trusted-server-js/lib/src/integrations`. The build discovers +directories containing `index.ts`, emits an IIFE for each entry point, and +embeds bundles and hashes into the `trusted-server-js` Rust crate. APS renderer +code is imported directly by browser core even though APS does not currently +have its own `index.ts`. + +The same baseline includes the parser-aware streaming Next.js processor from PR +#1135, managed Prebid User IDs and their OpenRTB EID/EC +flow, LiveRamp configuration through that existing managed-ID facility, +analytics-adapter selection in external Prebid bundles, cookie-keyed publisher +template caching, additional CLI ad-template and audit config consumers, and +documentation-snippet verification. These are current behavior and remain in +scope for parity even though they landed after this design was first drafted. + +`core/src/ec/prebid_eids.rs` is historically named after the first browser +producer, but its `ts-eids` ingestion, consent checks, EC finalization, +partner-graph ingestion, and admin diagnostics are shared identity machinery. +They remain in core under their current name for this split. Renaming the +neutral module is unrelated cleanup and is deferred. Prebid-owned configuration +and browser-module management move out; the shared EID/EC machinery does not +become a new capability family, and LiveRamp does not become another +integration definition. + +Configuration is also split by implementation detail. Browser/page settings +use `[integrations.]`, while server auction providers use +`[auction.providers.]` plus `profile = "aps"` or +`profile = "prebid-server"`. One logical APS integration is therefore +configured in two inventories and may be activated implicitly by an auction +plan. `IntegrationSettings` currently uses `HashMap`, so integration +declaration order is discarded before registry construction. + +These conditions produce five related problems: + +1. Core owns both neutral contracts and concrete implementations. +2. Rust registration, auction profiles, deploy validation, and migration guards + maintain separate concrete inventories. +3. Browser core imports integration-specific code. +4. One logical integration can be configured and activated through unrelated + locations. +5. Runtime integration order is not a stable property of the operator + configuration. + +## Related Pull Requests and Disposition (Non-Normative) + +The following pull requests explain how some current code arrived in the +repository. They are not design authorities for this specification. The +normative inputs are the decisions in this document and behavior present at the +review checkpoint above. + +### PR #1016 + +PR #1016 introduced the ancestor of the current compiled auction plan. The +following properties are now baseline repository behavior and are preserved +because current consumers depend on them, not because the PR is authoritative: + +- Multiple configured instances may use one integration implementation. +- One validated provider identity remains shared by bidder routing, backend + correlation, diagnostics, and telemetry; this design changes its serialized + value from a local ID to a qualified ID. +- One validated plan remains authoritative across every adapter and runtime + consumer. +- The generic OpenRTB transport remains shared. +- Existing routing, timeout, notification, response admission, mediation, and + telemetry attribution behavior remains unchanged except where provider order + is observable. + +This design changes the configuration location and identity spelling. Provider +instances move below their owning integration, and cross-integration references +use a qualified `.` identifier. It also intentionally +changes deterministic provider priority from lexical provider-ID order to +operator declaration order. The pricing algorithm is unchanged, but the first +configured provider retains an equal-price tie and later providers receive the +remaining shared auction budget after earlier providers launch. + +### PR #1084 + +PR #1084 explores a broader external-provider and plugin ecosystem. That scope +and its alternate configuration convention are not inputs to this design. This +specification independently chooses static workspace crates, typed capabilities +needed by current implementations, one ordered `[integrations]` inventory, +explicit `enabled`, and APS as an integration. No `[integration]`, `[demand]`, +or `[adserver]` selector convention is carried forward. + +PRs #1043 through #1047 and #1094 implement parts of that alternate convention. +They cannot merge concurrently with this configuration contract. Their tests or +neutral wire-format work may be reused after independent verification, but their +inventory, discriminator, provider-naming, and crate-layout decisions are +superseded for in-tree integrations by this specification. That disposition is +coordination, not evidence for the architecture chosen here. + +PR #1135 is part of the review checkpoint. The parser-aware streaming Next.js +implementation and integration-owned fixtures move with their owner. Its +cross-adapter end-to-end case remains in +`trusted-server-integration-tests/tests/parity.rs` and consumes +integration-owned test support; the removed HTML post-processor is not +recreated by this work. + +### Baseline Delta, Open Defect, and In-Flight Work Disposition + +The following items are open as of 2026-09-29. They are coordination inputs, +not design authorities. “Prerequisite” means the focused fix lands on `main` +and this specification records a new baseline before extraction begins; the +crate split does not absorb that bug fix into a move commit. + +| Item | Disposition | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| #1196 | Prerequisite. Restore cross-IIFE context/log state on the current layout, remove or debug-gate Creative's unconditional log-level bump, and add a production-artifact test. The milestone-one typed facade supersedes any transitional `Symbol.for` storage without reverting behavior. | +| #1197 | Prerequisite. Make set-valued configuration serialization deterministic before EdgeZero or template hashes depend on it. | +| #1198 | Prerequisite. Add the deterministic build digest defined below to the current template key before extraction, with the canonical generator owned by core build support; the split reuses the same generator for component digests. | +| #1199 | Prerequisite. Replace substring browser guards and Rust/HTML script-source matchers, including Permutive, DataDome, Lockr, and Testlight, with canonical parsed URL ownership on the current layout. Declaration and execution use one positive/negative corpus. | +| #1200 | Prerequisite. Remove the shared-`dist` partial-build race before two Rust crates consume browser outputs; milestone one then adopts owner-private outputs and the lock contract below. | +| #1201 | Milestone two. New source and schema 2 reject unknown catalog IDs and fields. The temporary schema-1 reader deliberately retains baseline acceptance with a warning and is not “fixed” retroactively. | +| #1202 | Prerequisite. Make `ts prebid bundle` use the permission-preserving atomic writer and non-disclosing parse errors; the moved command retains that corrected behavior. | +| #1203 | Prerequisite and adopted decision. A parsed non-2xx mediator response is a mediation failure and falls back to local ranking as specified below. | +| #1204 | Prerequisite. Every Rust CI job that can trigger a browser build installs the pinned Node toolchain and dependencies; obsolete direct Rust browser dependencies are removed. | +| #1205 | Proposed remedy superseded. This specification keeps operator order and uses the pure script-source claim validation below instead of an unconditional hidden first phase. The issue must be revised to that contract or closed. | +| #1206, #1207 | Prerequisites resolved by #1208. Their output regressions are not accepted as differential-harness goldens. | +| #1208 | Prerequisite. Compose script-text rewriters on the current layout and add Next.js/GTM output goldens before the baseline is captured. | +| #1098 | Prerequisite. Make neutral Cookie parsing lenient per header field and pair, preserving valid pairs when another pair is malformed and recording only counts/reasons. The catalog normalizer then removes and merges only its reserved names. This chosen remedy is broader and more explicit than the issue text. | +| #791 | Covered by extraction step 3. The Fastly-SDK guard follows the complete moved source set and dependency graph; this design does not add a second migration guard. | + +The review checkpoint already contains changes that landed after discovery. +They are baseline behavior, not optional input from their former pull requests: + +| Item | Landed baseline contract | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #879 | Preserve manifest-derived logical config-store defaults, service-scoped runtime store-name/root-key selection, Cloudflare's primary/legacy outer-binding fallback, and the rule that a runtime key override does not change the CLI push destination without an explicit `--key`. | +| #900–#903, #1157 | Preserve request-scoped EC snapshots and generation binding, conditional EID writes and conflict follow-up, snapshot-gated pull sync, idempotent withdrawal tombstones, grouped batch-sync ordering/accounting, and removal of the legacy consent-store input. These remain neutral core behavior rather than integration capabilities. | +| #1159 | Preserve `Disabled`/`Explicit`/legacy-inferred stored-request intent, post-override usable-impression admission, zero-impression no-transport `Skip` metadata, and response parsing bound to the exact impressions actually sent. The extraction carries the complete existing regression matrix across the prepared-exchange seam. | + +Other open work is coordinated without making it architectural authority or an +automatic prerequisite: + +| Item | Coordination contract | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #1169 | Coordinate cache-policy, key-schema, and fixture changes. Preserve its separate origin-shareability decision if it merges before the baseline freeze. | +| #1179 | Reconcile per-document buffer work with #1208 and select one reviewed EdgeZero pin. Reuse remains optional; composition works per request. | +| #1193 | Rebase caching onto the immutable composition and its fingerprint; do not retain a parallel composition or fingerprint path. | +| #1191 | Preserve whichever claim/lifecycle contract lands before the baseline freeze; move its bootstrap/runtime harness and shared-state shape together. | +| #1185 | External Prebid fork-source selection remains separate. Same-checkout enforcement here covers Trusted Server-owned package and input paths; future vendor-source selection requires an explicit extension. | +| #1002 | Its alternate release catalog, bootstrap transport, and hard cutover do not enter this design. Independently verified renderer tests may be reused; the release lines must otherwise be reconciled explicitly. | +| #1121, #1154 | Preserve accepted diagnostic wire behavior while moving GPT presentation and state outward; core retains only neutral diagnostic facts. | +| #1188 | Independent neutral EC/EID correctness stays in core. Preserve body ingestion and consent checks if it merges; it creates no integration definition. | +| #1107 | Separate proposed product feature, deferred. Extraction implies no trace endpoint or general tracing framework. | + +PR #1052 may contribute neutral renderer diagnostics, but APS-named browser +core members do not enter core; they move with APS or become generic renderer +reason fields. PR #1175 and EdgeZero PR #381 are milestone-one step-3 +prerequisites: the typed-config extension and Fastly store procedure build on +their reviewed environment-selector line or an equivalent merged successor, +never on v0.0.8. + +This specification partially supersedes the following earlier design documents +only where they conflict with the new ownership, configuration, or fingerprint +contracts: + +- `2026-08-10-config-first-auction-provider-architecture-design.md`: its + provider inventory, selection, and ordering syntax are superseded; reusable + neutral auction-engine findings remain evidence to reverify. +- `2026-07-15-gam-ts-cohort-attribution-design.md` and + `2026-08-19-gam-attribution-review-resolution-design.md`: attribution wire + behavior remains, while concrete GPT code and assets move to their integration + owner. +- `2026-09-08-1138-per-cookie-template-cache-policy-design.md`: cookie/Vary + policy remains, while complete-settings fingerprinting is replaced by the + composition/build digest defined here. +- `2026-07-24-prevent-duplicate-gpt-slot-requests-design.md`: duplicate-slot + behavior remains, while concrete GPT state and browser ownership move outward. + +Before either milestone branches for implementation, all prerequisite issues in +the disposition table must have landed, then the baseline commit, output +goldens, and locked EdgeZero revision are recorded again and every +baseline-dependent inventory in this document is rechecked. Open or previously +approved pull requests never override a decision in this specification merely +because of their review status. + +## Rejected Alternatives and Rationale + +The following alternatives were considered and are deliberately not part of +the target design. Each rejection is narrow: where review exposed a valid +failure mode, the concern is accepted even when the proposed remedy is not. +The objection and the replacement decision are stated separately so an +implementation cannot quietly reintroduce the rejected mechanism. + +- **One crate per integration or an external plugin ABI.** Concrete ownership + boundaries and independent testability are required; that concern is + accepted. **Objection:** per-vendor crates, dynamic discovery, or a public SDK + would multiply dependency, compatibility, release, and governance surfaces + without a current external consumer. **Decision:** use one statically linked + Rust integrations crate, one integration-browser crate, per-integration + directories, and one compile-checked catalog. +- **A separate top-level auction-provider inventory, including APS.** APS does + provide auction behavior, but it also owns browser, renderer, route, and page + behavior. **Objection:** classifying APS as only a provider would split one + implementation's activation and ownership across unrelated top-level + sections and would make `[integrations]` incomplete. **Decision:** APS is an + integration that registers several capabilities. Its provider instances live + below `[integrations.aps.auction.providers.]`, as do provider instances + owned by other integrations. +- **A config `type`, `kind`, or `implementation` discriminator.** Multiple + configured instances must be able to reuse one implementation; that concern + is accepted. **Objection:** an extra discriminator would duplicate the static + integration ID already present in the TOML path, admit contradictory ID/type + combinations, and expose internal registration types to operators. + **Decision:** `[integrations.]` selects the statically cataloged + integration, while nested instance names identify reusable provider + configurations. Capability types remain Rust contracts, not configuration. +- **Keeping local provider IDs as the permanent global identity.** Stable + external values during the dual-reader rollout are required; that concern is + accepted. **Objection:** local IDs can collide across owning integrations and + cannot safely key one cross-integration plan. **Decision:** the plan always + uses `QualifiedProviderId`, while an explicit external label preserves local + schema-1 values and changes to qualified values only at the schema-2 cutover. +- **Keeping concrete browser integrations in the neutral JS crate or splitting + them into one package per vendor.** One tested Node dependency graph is + required. **Objection:** leaving vendor sources in the neutral crate preserves + the ownership inversion, while per-vendor packages multiply lockfiles, + resolution rules, and release surfaces without independent consumers. + **Decision:** use one `trusted-server-integrations-js` Rust/source crate for + concrete integration browser code and one canonical Node project shared with + neutral browser core. +- **Keeping the existing mediator trait object unchanged.** One reusable + mediator implementation and stable fallback behavior are required. + **Objection:** the existing object couples configuration selection, request + preparation, transport, response parsing, and provider-specific state, which + forces concrete knowledge and downcasts across the core boundary. + **Decision:** use a typed mediator registration, prepared request, and bound + response parser while retaining the existing wire protocol and the corrected + fallback policy. +- **An operator-written `[auction] provider_order` list.** Provider priority + must represent schema-1's globally interleaved lexical order; that concern is + accepted. **Objection:** a second operator list would duplicate every provider + identity, permit the inventory and priority to drift, and separate priority + from the configuration block an operator is reviewing. It would make the + promised single integrations configuration untrue. **Decision:** schema 2 + derives priority from declaration order in the one `[integrations]` + inventory. The neutral compatibility model carries a flat legacy sequence so + schema 1 remains exact; that internal sequence is not a second operator + syntax. +- **A hidden engine override that always runs `js_asset_proxy` first.** Attribute + overlap and terminal-removal behavior must be explicit and tested; that + concern is accepted. **Objection:** a hidden first phase would make the TOML + order contract false and still would not reproduce baseline Prebid removal + behavior, because Prebid precedes `js_asset_proxy` today. **Decision:** legacy + compatibility paths reproduce the complete baseline sequence. Schema 2 + exposes chained replacement and terminal removal in declaration order. Pure + per-definition script-source transitions validate the complete final outcome + for each operator asset. No position, including placing `js_asset_proxy` + first, is a privileged override: an operator may reorder integrations, but + validate/diff/push accept the result only when the entire chain finishes in + the declared asset policy. +- **Treating PR #1016, PR #1084, or earlier review statements as design + authority.** Their code and tests can reveal compatibility constraints, and + conflicting in-flight work needs an explicit disposition. **Objection:** + review or merge status does not make a prior proposal correct for this design, + and importing its architecture would silently expand this spec's scope. + **Decision:** current behavior is evidence and prior proposals are context. + This specification records its own ownership, ordering, activation, and + rollout decisions and records the disposition of incompatible work above. +- **Requiring the Node package and lockfile to move to a common ancestor.** One + Node project must reliably resolve, type-check, lint, format, test, and build + both source roots; that concern is accepted. **Objection:** moving the package + root is not required to meet that contract and would add unrelated + repository-wide path and automation churn. **Decision:** keep one canonical + project with explicit, tested resolver and tool-root configuration for the + sibling sources. Moving the root remains a fallback only if that contract + cannot be made reliable. +- **A new configuration-status endpoint, public or authenticated.** Runtime settings must be + loaded and the expected schema must be observable during rollout; that + concern is accepted. **Objection:** even an authenticated endpoint would add + a new route, authorization contract, and public support surface unrelated to + the crate split. + **Decision:** use adapter-native version/binding inspection, startup + schema-and-digest logging, and an existing authenticated or + settings-dependent probe. +- **Filesystem snapshots around EdgeZero config commands.** Config commands must + validate and serialize the same app-config bytes; that concern is accepted. + **Objection:** same-directory manifest copies add write + requirements, can survive process termination, expose operator configuration, + and require log/path rewriting without freezing every adapter manifest and + store target. **Decision:** a narrow two-stage EdgeZero typed-config extension + gives the app the exact source bytes and the effective typed command context, + providing the required consistency without filesystem snapshots. +- **Hashing resolved secret values into template identity.** Current integration + configuration changes that alter document bytes must invalidate templates; + that concern is accepted. **Objection:** current integration secrets authorize + upstream calls and do not form HTML variants, so hashing their values adds + rotation churn and sensitive derived material without improving correctness. + **Decision:** the verified stored-data hash covers secret references. A future + secret that shapes output must declare a non-secret behavior fingerprint or + force private output. +- **A second generic per-capability enablement system.** A simple master kill + switch and precise optional-feature activation are both required; that + concern is accepted. **Objection:** operator-visible capability kinds or a + parallel browser/provider activation inventory would recreate the + discriminator-driven configuration this design is removing and introduce two + answers to whether an integration is active. **Decision:** `enabled` remains + the integration master gate, existing typed fields decide which optional + capabilities are configured, and routes to known-disabled integrations are + pruned so `enabled = false` remains a kill switch. +- **Silently accepting unknown integrations or providers in schema 2.** Exact + baseline acceptance must remain exact while schema 1 can still be loaded; + that concern is accepted. **Objection:** extending that permissiveness to new + source would turn typos into silently inactive configuration and prevent the + static catalog from validating ownership. **Decision:** exact legacy + acceptance belongs only to the schema-1 compatibility reader. New source and + stored schema 2 fail on unknown IDs; only references to an explicitly + disabled, known integration receive the kill-switch treatment defined below. + +## Goals + +1. Move every concrete Rust integration implementation out of core and into one + `trusted-server-integrations` crate. +2. Move all integration-specific browser sources and artifacts into one + `trusted-server-integrations-js` crate. +3. Give each concrete integration one Rust directory and, when applicable, one + same-named browser directory. +4. Keep one explicit compile-checked Rust catalog and discover browser modules + from integration directories at build time. +5. Let one integration register multiple typed capabilities without a global + integration-kind enum. +6. Remove concrete integration construction, auction-profile, validation, and + lifecycle imports from core. +7. Make `[integrations]` the single ordered inventory for concrete integration + configuration, including auction providers. +8. Preserve behavior on the current `origin/main` baseline except for the + explicitly documented configuration, activation, and ordering changes. +9. Make core compile without depending on either integrations crate. +10. Keep all integrations statically linked; no runtime loading is introduced. + +## Non-Goals + +This design does not introduce: + +- One Cargo crate per vendor. +- External vendor crate injection or adapter-supplied registration. +- Runtime-loaded integrations, dynamic linking, or an ABI. +- A stable public plugin or integration SDK. +- Independent vendor release, compatibility, security-response, or governance + policies. +- New identity, EC, geo, device, or permission-signal provider systems. The + existing neutral `ts-eids`/EC flow and managed Prebid User ID behavior remain + supported. +- A jurisdiction or permission-policy redesign. +- Client-cycle EC resolution or provider-code allocation. +- Upstream EdgeZero lifecycle, host-evidence, store, or adapter changes. One + narrow typed-config validation extension is allowed: a source-bytes check and + a post-parse command-validation callback over the same loaded value. It does + not change target selection, deployment, storage, or adapter behavior. +- New auction protocols, pricing algorithms, notification policies, or + telemetry schemas. Configuration order intentionally replaces lexical + provider-ID order wherever deterministic provider priority is observable. +- A reorganization of CLI audit detection that is unrelated to configuration + validation and composition. + +The design adds only capabilities needed to move current implementations. A +future non-OpenRTB provider, external crate, or new provider family requires a +separate design with a real consumer. + +## Terms + +The following terms are distinct: + +- **Integration definition:** the statically cataloged code definition for a + stable integration ID such as `aps`. +- **Capability registration:** one typed contribution made by an integration, + such as a proxy, HTML rewriter, JavaScript module, OpenRTB profile, or + mediator. +- **Integration configuration:** the single ordered operator block at + `[integrations.]` that activates and configures the definition. +- **Auction provider instance:** one named endpoint and policy configuration + below an integration, such as `aps.aps-main`. Multiple instances may use the same + integration implementation. +- **Local provider ID:** the provider name within one integration, such as + `aps-main`. +- **Qualified provider ID:** the strong, globally unique pair of an integration + ID and local provider ID, such as `aps.aps-main`, serialized as + `.`. +- **Integration registry:** core runtime state containing the enabled page, + request, response, and browser capabilities in configuration order. +- **Auction plan:** core runtime state containing the validated configured + provider instances, routes, and orchestration policy. + +An integration is therefore a container for capabilities; it is not itself a +single capability type. + +## Target Workspace Layout + +```text +crates/ + trusted-server-core/ + src/ + integration/ + mod.rs + registry.rs + + trusted-server-js/ + lib/ + package.json + package-lock.json + build-all.mjs + build-prebid-external.mjs + src/core/ + test/core/ + src/ + + trusted-server-integrations/ + Cargo.toml + src/ + lib.rs + integrations/ + adserver_mock/ + mod.rs + aps/ + mod.rs + datadome/ + mod.rs + protection.rs + protection_scope.rs + didomi/ + mod.rs + ... + nextjs/ + mod.rs + rsc.rs + rsc_placeholders.rs + rsc_stream.rs + script_rewriter.rs + shared.rs + fixtures/ + openrtb/ + mod.rs + + trusted-server-integrations-js/ + build.rs + Cargo.toml + lib/ + src/integrations/ + aps/ + index.ts + render.ts + renderer-document.html + creative/ + index.ts + datadome/ + index.ts + prebid/ + index.ts + user_id_modules.json + ... + test/ + integrations/ + fixtures/ + src/ + lib.rs +``` + +Every current flat Rust integration file becomes +`src/integrations//mod.rs`. Restricting the completeness scan to that +directory prevents helper modules from being mistaken for integrations. +Existing nested modules and fixtures stay with their owner. The current +Next.js `rsc_stream` implementation moves; the removed `html_post_process` +module does not return. `openrtb` is a built-in Rust-only, directory-backed +integration adapter that exposes configuration for the current standard +OpenRTB profile without turning the neutral OpenRTB execution engine into +concrete code. The target Rust catalog therefore has sixteen definitions: +fifteen moved implementations plus the new `openrtb` adapter. + +JavaScript-only `creative` remains valid without a Rust directory. Rust-only +integrations remain valid without a browser directory. Cross-adapter Playwright +and parity tests remain in `trusted-server-integration-tests`; their paths and +load-order assertions change, but system tests do not become source owned by +one integration crate. + +`creative` is the sole fixed, non-configurable browser prelude in this design; +it is runtime support rather than an operator integration. Directory discovery +may build other JavaScript-only assets, but `[integrations]` cannot activate one +unless a Rust definition with that ID registers its browser-module capability. + +## Dependency Direction + +The Cargo dependency graph is one-way: + +```text +trusted-server-integrations ──→ trusted-server-core ──→ trusted-server-js + │ │ + │ └──→ trusted-server-openrtb + └───────────────→ trusted-server-integrations-js + +adapters and CLI ────────────→ trusted-server-integrations +adapters and CLI ────────────→ trusted-server-core +trusted-server-integration-tests ──→ trusted-server-integrations +``` + +The rules are: + +1. Core never depends on either integrations crate. +2. Concrete Rust integrations use public neutral core contracts and domain + types. +3. `trusted-server-integrations` links Rust definitions with generated browser + modules from `trusted-server-integrations-js`. +4. Integration TypeScript may use the explicit browser-core API, but browser + core never imports a concrete integration and integration bundles never + embed a private copy of stateful browser-core modules. +5. The CLI uses the source-validation APIs from + `trusted-server-integrations`; every adapter uses its single runtime + composition entry point from that crate. Both phases resolve the same static + catalog and integration schemas. +6. No adapter reconstructs a concrete catalog or imports `aps`, `prebid`, or + another integration module directly. + +### Application composition ownership + +`trusted-server-integrations` is the application composition root, not only a +directory of implementations. It owns: + +- `TrustedServerAppConfig`, the typed operator-facing app-config root used by + the CLI. +- Source-aware TOML structure validation and ordered integration + deserialization. +- Aggregation of core and integration secret metadata. +- Integration-owned preprocessing for conditionally active secrets. +- Catalog-aware validation and capability construction. +- Pure source parsing, structural validation, and deploy-validation APIs used by + the CLI at the validation level appropriate to each command. +- The public runtime entry points that load a config-store blob and return one + composed runtime value. +- A structural `SourceConfigView` and a deploy-validated + `ValidatedSourceConfig` used by non-runtime CLI commands. +- A generic `PartialSourceConfigView` for recovery-oriented commands that + intentionally type only one owned subtree of an otherwise invalid document. + +`TrustedServerAppConfig` contains neutral core configuration plus the ordered +integration-owned source configuration. Concrete integration configuration is +not added to core's `Settings`. Composition consumes the integration portion +into capabilities and returns a neutral runtime `Settings` value containing +only state that core execution engines understand. + +The public-API snapshot classifies exports as stable neutral contract, +integration-owned facade, or milestone-one transitional delegation. A +transitional export must name its replacement and owning move step; adding one +requires review, and the class must be empty before milestone 1 exits. + +The delta allowlist applies only to public items newly added or visibility- +widened by this design and to concrete public paths removed by extraction. +Existing neutral core exports outside the touched integration, auction, +configuration-loading, browser-composition, and lifecycle seams are captured in +a grandfathered baseline snapshot; they need not map to this table and may not +change in an extraction commit. The allowed delta is deliberately +category-sized but not open ended: + +| Owner/export class | Allowed surface | Consumer and lifetime | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Core stable neutral contracts | Neutral `Settings` subsets; `AuctionPlan` and ordinal-bearing provider/slot/header inputs; OpenRTB profile, compiled profile, prepared exchange-or-skip, consuming bound response parser, and response-admission contracts; bounded backend/body transport; neutral renderer descriptors; route/script claims; request-processing requirements; browser assets; registry builders; verified-envelope/chunk helpers; and neutral stubs behind `test-utils`. | The integrations composition root and adapters. These are behavior-oriented contracts and contain no vendor configuration or concrete implementation type. | +| Integrations-owned facade | Source/partial/validated config views, catalog metadata, the runtime composition attempt/result, CLI read models, and the production catalog test factory. | Adapters and CLI. These may expose catalog-backed application behavior but never a concrete `aps`, `prebid`, or other vendor module. | +| Transitional delegation | One named catalog entry or browser path still delegated to its old owner, with its replacement and removal step recorded in the snapshot. | Milestone 1 only; the class is empty at its exit. No new consumer may adopt it. | + +Before implementation changes visibility, step 1 generates and commits a +symbol-level delta manifest from the baseline snapshot. Every added, widened, +or removed item must map to one row and record its exact path, owning module, +stability class, direct workspace consumers, and removal condition when +transitional. The expected new entries include the ordinal-bearing auction +inputs and transport-header view; OpenRTB profile, prepared outcome/exchange, +and consuming parser traits; mediator traits; bounded backend/body transport and +response-admission diagnostics; `IntegrationDeclaration`, route/script and +reserved-route claims, and registry construction; renderer descriptor, +`CompiledBrowserAsset`, and `BrowserDocumentAssets`; request-processing +requirements; `ResolvedConfigLocation` and `ConfigurationUnavailable`; +integration-owned `CompositionAttempt`; and neutral `test-utils` stubs. A needed +symbol outside this closed set requires a spec amendment rather than an +unreviewed allowlist addition. + +Current `trusted_server_core::integrations::` paths are +workspace-internal migration paths, not a compatibility API: imports move +atomically with their owners and the concrete modules are deleted without +re-export shims. The snapshot rejects piecemeal `pub` widening of current +auction/OpenRTB helpers when a smaller neutral DTO or trait satisfies the +consumer, and rejects unrelated changes to grandfathered neutral exports. + +The public API has four explicit levels: + +1. `SourceConfigView` performs the TOML pre-pass, typed parse, catalog + resolution, and structural validation. It does not run deploy validation. + Read-only diagnostics that already require a complete typed root use this + level so unrelated deployment checks do not become new failures. +2. `PartialSourceConfigView` retains the source document plus one typed, + command-owned subtree. Recovery-oriented mutators and generators use it when + their current contract tolerates an invalid unrelated root. It cannot be + converted into a storage DTO or runtime composition, and each command names + the only source paths it may read or write. +3. `ValidatedSourceConfig` applies the selected environment overlay, aggregates + secret metadata, runs every secret-independent integration and + cross-integration check, serializes the storage DTO, and validates its order + sidecars. It invokes the same pure auction-plan compiler used at runtime, + including provider routing, browser/server bidder ownership, mediator + capability and enablement, and duplicate routes, then discards the + validation-only plan. A separate pure `validate_for_targets` operation runs + the resulting plan against the command's resolved target set. Neither path + creates executable capabilities or attempts to use unresolved secret values. +4. Runtime composition begins after envelope verification, inactive-secret + preprocessing, and secret resolution. It repeats the shared pure validation + kernel against resolved values, compiles the authoritative plan, constructs + plan-dependent capabilities, and returns the final composition. + +This distinction makes "runtime-only construction" precise: it does not move +any currently push-time, secret-independent plan failure to startup. Diff and +push have one selected adapter and fail when its target validation fails. The +existing EdgeZero `config validate --strict` meaning is unchanged: it enforces +the manifest-completeness and handler-path checks already owned by EdgeZero. +Trusted Server target-plan validation is exposed separately as a repeatable +`ts config validate --adapter ` option. With no `--adapter`, the +command runs target-neutral Trusted Server checks plus existing EdgeZero +validation; with one or more adapters, every selected target is checked and any +target failure makes the command fail. Auction-testing documentation and smoke +commands use this same adapter spelling. A fixture rejected by runtime +composition for a secret-independent reason must produce the same +target-neutral error or the same named target result in the CLI. + +The complete source and validated views retain the typed operator configuration +and expose only the data their CLI consumers need: neutral global settings, +ordered integration and qualified-provider metadata, and integration-owned read +models. A partial view retains only its owned typed subtree and the source +document needed for a bounded edit. None is a runtime registry or contains +resolved secret values or executable capability objects. + +The runtime value, conceptually `TrustedServerComposition`, contains the +validated neutral `Arc`, one `Arc`, the plan-backed auction +orchestrator including the selected mediator, one `IntegrationRegistry`, the +composed `BrowserDocumentAssets`, the validated deployment target, and a lazy +canonical composition digest. Adapters consume this value; they do not +separately compile the auction plan, construct the orchestrator, rebuild the +integration registry, enumerate browser bundles, or reconstruct the digest. + +The composition is immutable and may live for a request, a Fastly sandbox, or a +bounded adapter cache without changing semantics. Stored capabilities are +`Send + Sync` and request-stateless. Script text buffers, Next.js stream state, +document observations, and other mutable transformation state are created by +per-document factories and live in request/processor state, never in a reused +registry object. Composition work is O(configuration); exact asset hashes are +build-time inputs and template identity is lazy/memoized as described below. + +Runtime construction returns a staged `CompositionAttempt`, not only +`Result`. The attempt contains +`validated_settings: Option>` and +`composition: Result, Report>`. +The settings view becomes `Some` only after envelope/schema decoding, +inactive-secret preprocessing and resolution, resolved-value validation, +auction-plan compilation, plan-dependent enabled-integration validation, and EC +partner validation. It is captured before target validation, mediator lookup, +route insertion, and executable capability construction. A failure before that +point returns `None`; a failure after it returns the same `Arc` beside +the error. Fastly retains its JA4 gate and failed-startup finalization paths +through that value without re-reading the config store. Other adapters may +ignore the view, but none reconstructs it independently. + +Adapter configuration resolution produces one `ResolvedConfigLocation` and one +verified byte source before composition. It identifies the adapter, logical +selector, platform store/binding name, physical store identity when the platform +exposes one, and exact root key. Defaults and service-scoped overrides are +resolved once by the adapter/EdgeZero boundary; core chunk reconstruction never +re-derives them, and the integrations crate never reads process configuration. +Root/chunk absence or an unavailable platform read becomes a redacted +`ConfigurationUnavailable` error category. Envelope, pointer, chunk hash, +length, schema, or parse failures remain configuration corruption. The category +does not define one cross-adapter HTTP status; adapters retain their baseline +startup policy except for the named Fastly correction: + +| Adapter | Unavailable source | Verified-data corruption | +| ---------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| Fastly | Missing, unreadable, or not-yet-propagated root/chunk uses the intentional transient 503 startup path. | Existing configuration/500 path. | +| Axum | Existing configuration/500 startup router on every route, including `/health`. | Same existing configuration/500 path. | +| Cloudflare | Existing configuration/500 startup router on every routed request. | Same existing configuration/500 path. | +| Spin | Existing startup-error router: application routes return 503 and `/health` remains 200. | The same baseline router/status behavior; structured logs retain the corruption classification. | + +No error includes configuration bytes, store contents, or secret values. + +Core retains neutral config-store access, Fastly chunk reconstruction, blob +envelope verification, preprocessing for inactive neutral/global secret +references, secret-resolution primitives, global settings types, and +auction-plan compilation. Today the neutral preprocessor removes inactive +Tinybird token references and disabled EC-partner pull-token references; that +behavior remains in core. These helpers accept or return neutral data and never +call the concrete catalog. Adapter-specific readers consume the resolved +location and produce one verified envelope/data value; Fastly chunk +reconstruction remains a core loader helper, while Cloudflare and Spin adapt +their existing binding/KV inputs to the same verified-data boundary. The +integration crate composes it in this order: + +```text +config-store bytes + → core chunk reconstruction and envelope verification + → application-schema dispatch + → schema-2 sidecar/map bijection or schema-1 normalization + → core-owned neutral/global inactive-secret preprocessing + → catalog-owned integration inactive-secret preprocessing + → aggregated core + integration secret resolution + → catalog-aware resolved-value validation + → core AuctionPlan compilation + → catalog-aware plan-dependent and core EC-partner validation + → validated settings-only neutral view + → target validation + → mediator resolution + → plan-dependent capability construction + → core IntegrationRegistry and orchestrator construction + → browser asset composition and document fingerprint + → TrustedServerComposition +``` + +The CLI imports `TrustedServerAppConfig`, the complete, partial, and validated +source views, and pure validation APIs from `trusted-server-integrations`. The +host-only CLI continues to own command dispatch and the direct `edgezero-cli` +dependency; `trusted-server-integrations`, which is linked into every WASM +adapter, never depends on `edgezero-cli`. + +The current locked EdgeZero revision does not expose enough context for this +contract. Building on EdgeZero PR #381 and Trusted Server PR #1175, EdgeZero +therefore gains narrow `run_*_typed_with_hooks` entry points with source and +command hook objects supplied by the downstream caller; their defaults are +no-op. Trusted Server owns a wrapper argument type that flattens EdgeZero's +non-exhaustive `ConfigValidateArgs` and adds repeatable `--adapter`; it does not +add fields to EdgeZero's public argument type. Each command resolves its +adapter set before either hook: zero or more for validate and the existing one +for diff/push. One exact source read supplies both the raw/source-aware parse +and the typed overlay view, secret checks, command callback, diff, envelope, and +serialization; neither EdgeZero nor a callback may reopen the path. Hook context +contains the command kind, resolved adapter set, app environment-variable +prefix, `--no-env`, EdgeZero's existing `strict` flag, raw path and bytes, and a +borrow of the overlay-applied typed value. EdgeZero invokes command validation +and serializes that same value. Validate, diff, and push therefore cannot +validate one app-config read or typed value and serialize another. + +All workspace `edgezero-*` dependencies are then repinned together to one +immutable, reviewed successor tag or commit containing both the environment +selector work and this extension. The review checkpoint happens to use v0.0.8; +the design does not assume that version is the successor's immediate parent. +The extension does not alter manifest parsing, target selection, +environment-overlay mechanics, logging, storage, or adapter behavior. It +replaces the filesystem snapshot wrapper entirely: config commands do not write +temporary operator or manifest copies, require a writable checkout, rewrite +logged paths, or rely on destructor cleanup around `process::exit` and signals. +If the extension cannot land and the workspace cannot repin to its reviewed +revision, milestone 1 is blocked rather than restoring the snapshot design. +Push-time selected-target validation is an explicit milestone-one CLI delta +needed to place all deploy validation behind the new composition root; it does +not enable schema 2 or configuration-order semantics. + +Read-only `config ad-templates` and `audit ad-templates` commands load the +effective `SourceConfigView` with the existing optional environment overlay when +they currently require the complete typed root. Mutating or generator commands +load file bytes without the overlay, so environment-only values are never +persisted. Recovery-oriented ad-template generation uses an explicitly bounded +`PartialSourceConfigView` against an otherwise invalid +baseline, preserving its current warning and non-disclosure behavior. +"Structural-only" means valid TOML, explicit parent/descendant structure, ID +grammar, and typed parsing of the subtree the command reads or writes; it +excludes unrelated required values, plan compilation, target validation, active +secret values, and publisher-domain deploy checks. Candidate and baseline use +the same selected validation level: a valid candidate is written; an invalid +candidate is refused when the baseline was valid; and an invalid candidate over +an already-invalid baseline retains the current warning and atomic-write escape +hatch without disclosing source values. Final validate, diff, and push always +use the complete `ValidatedSourceConfig` path. + +The migration updates ad-template lint/help text and `--explain` rule counts in +the same commit so neither describes `[auction.providers]` or reports a stale +number of checks. Documentation-snippet and exact-message tests pin the new +integration-owned paths and counts. + +`ts prebid bundle` obtains typed bidder, User ID, analytics, and managed-module +requirements through an integration-owned +`PartialSourceConfigView` rather than a duplicate CLI schema. It +intentionally does not require unrelated app +configuration or an `external_bundle_url` to be deploy-valid: `bundle.modules`, +`external_bundle_sha256`, and `external_bundle_sri` are inert staging metadata, +while `external_bundle_url` activates the browser capability. The command +retains its current ability to build first, patch hash/SRI metadata through the +permission-preserving `write_file_atomically` path, and tell the operator to +upload and set the URL. It validates the structural +pre-pass and affected Prebid subtree before writing; full app validation remains +the contract of config validate/push. Because parent position is runtime order, +the command no longer invents or appends a missing `[integrations.prebid]` +parent. It requires an existing parent with explicit `enabled`, or exits with a +placement example; it may create only owned descendants beneath that parent. +Provider diagnostics display qualified providers in configuration order rather +than alphabetizing a detached map. TOML parse failures report only the path and +line/column; diagnostics never echo source lines or configuration values. The +command canonicalizes both the Node package root and every integration-owned +source input and rejects realpaths that do not belong to the same workspace and +worktree checkout. + +`ts audit generate` may retain detector and edit metadata for concrete +integrations under this design's audit-reorganization non-goal. That metadata is +not a second runtime schema: audit candidates and final writes are parsed and +validated through `SourceConfigView`/`ValidatedSourceConfig`, and the audit code +does not define activation, defaults, secret metadata, or capability +construction independently. + +## Static Rust Catalog and Browser Discovery + +### Rust + +`trusted-server-integrations/src/lib.rs` declares a small, explicit static +catalog. Each entry names an `IntegrationId` and a crate-private `definition` +function from a same-named directory. Rust compilation checks every listed +module and definition signature. A host-target completeness test enumerates +immediate `src/integrations//mod.rs` directories and fails if a valid +directory is missing from the catalog or a catalog ID has no directory. IDs +must parse as the `IntegrationId` defined by this design and must be unique. + +There is no directory-local numeric order. Runtime order belongs to +configuration. + +This intentionally avoids a Rust build script and generated module +declarations for a sixteen-entry table. The catalog-completeness test provides +the missing-registration guard without making filesystem discovery part of +compilation. + +### JavaScript + +`trusted-server-js` and `trusted-server-integrations-js` use one browser Node +toolchain project, one browser lockfile, and one coordinated set of Vitest, +ESLint, Prettier, Vite, and Prebid resolution rules. The canonical project root remains +`crates/trusted-server-js/lib`; `trusted-server-integrations-js` does not add a +second `package.json` or lockfile. Neutral browser sources remain under +`trusted-server-js`; integration sources, owned unit/artifact fixtures, and +owned unit/artifact tests live under `trusted-server-integrations-js`. The +shared configuration does more than include that sibling source root: + +- Vite and Vitest resolve bare packages through the canonical project's + exports-aware resolver. Runtime state is externalized behind the facade; the + build does not depend on bundler deduplication of stateful entry points. +- TypeScript includes both source roots and supplies exact package paths where + Node's ancestor walk cannot reach the canonical `node_modules`. Milestone 1 + first adds ambient `?inline`/`?raw` module declarations, a production + two-root `tsconfig` with `vite/client`, `DOM.Iterable`, and the current target, + and fixes the existing duplicate `w`/`h` production errors. It then enables a + no-emit typecheck of production sources; legacy Vitest typing is not hidden + behind that production gate. +- Vitest names the sibling test directory and includes its runtime tests. A + separate isolated canary command intentionally typechecks a fixture with one + expected error and asserts that exact diagnostic, so an empty glob or disabled + source checking cannot pass silently. The canonical package provides a real + `npm run typecheck` script for both production roots. +- ESLint is invoked from their common ancestor with explicit source globs; it + does not require moving `package.json`. Prettier always + receives the canonical `--config` path for sibling files. +- Generated external-Prebid entries live under the canonical project or use an + explicit resolver that preserves the `prebid.js` package `exports` map; a + directory alias that bypasses package exports is forbidden. +- Because the current `gpt_bootstrap.js` fails the canonical formatter and + legacy browser lint rules, its move preserves output behind one exact-path + Prettier ignore and one documented, exact-path ESLint legacy override. The + shared rules are not weakened; modernizing that file is separate work. + +Separate build targets emit neutral and integration artifacts directly into +private owner-specific directories below their Cargo `$OUT_DIR` and validate +per-target manifests before embedding them. They never clean, discover, or copy +from one shared `dist` directory. A checked-in, dependency-free Node runner +outside `node_modules` acquires one atomic-directory lease at the canonical +project root and records holder PID, process-start identity, nonce, start time, +and child process-group/session identity. One public command acquires the outer +lease. It passes an unguessable inherited nonce to nested npm scripts or helper +runners; a nested runner verifies that nonce against the live lease and joins +the ownership scope without acquiring or releasing it. A missing, mismatched, +or externally supplied token fails rather than granting re-entrancy. This makes +the concrete Cargo-build-script → runner → `npm run` → nested runner → Node +chain non-deadlocking while keeping build, manifest validation, and output copy +under one lease. + +The outer runner starts the requested command in a dedicated process group or +the host's equivalent job/session and does not release the lease until that +ownership scope is gone. Stale recovery requires the recorded holder and the +complete recorded child scope to be absent for the grace interval; if the host +cannot prove that condition, recovery refuses with a diagnostic instead of +replacing `node_modules` beneath a possible orphan. PID reuse is rejected using +the recorded process-start identity. `npm ci`, Vite, Vitest, TypeScript, ESLint, +Prettier, both Cargo embed builds, and the CLI Prebid builder all use this outer +or authenticated nested path. Owner manifests record a digest of their complete +artifact-affecting source inputs and reject stale output. Browser npm scripts, +build scripts, and workflows use the same runner and lock path; a repository +guard rejects bare `npx` or unwrapped production-browser-tool invocations. +Per-crate locks are invalid. Independent docs and Playwright projects retain +their own dependency trees and commands and are outside this production-browser +lease unless they invoke the canonical browser project. + +The integration build discovers immediate directories containing `index.ts` +and emits one IIFE per entry point. An IIFE may call the external versioned +browser runtime facade and therefore is not described as self-contained. Its Cargo build embeds each +output and its SHA-256 hash. CI, browser integration scripts, and the CLI +Prebid builder use the single workspace root rather than maintaining a second +dependency graph. Dependabot's existing browser entry continues to watch this +one lockfile; the existing docs entry is unchanged. + +Canonical npm build, typecheck, lint, format, and test commands include both +source roots explicitly, and CI invokes those commands rather than core-only +paths. A resolution test imports `vitest`, `prebid.js`, and one exported Prebid +module from a sibling integration file. The neutral Rust build script watches +and hashes only neutral source plus shared configuration that can affect its +artifact. The integration Rust build script watches and hashes the integration +source plus the public facade declaration and shared configuration that can +affect its artifacts. The production two-root typecheck and other repository +validation commands remain separate validation inputs; they do not make sibling +integration bytes part of the neutral owner manifest. Clean, incremental, and +concurrent build tests change one integration source and prove the integration +manifest is regenerated without spuriously changing the neutral manifest or +observing another build's partial output. + +`build-prebid-external.mjs` and its npm command remain at the canonical Node +root as build orchestration, not browser runtime. The Prebid registry, aliases, +shims, and other integration-owned source inputs move with Prebid into +`trusted-server-integrations-js`. The launcher receives their resolved sibling +paths explicitly and has no hard-coded `src/integrations/prebid` assumption. +The CLI resolves the canonical package root for dependencies and obtains the +integration-owned input paths and typed module requirements from the +integrations facade; it does not locate a registry through its own relative +path constant. The facade returns repository-relative paths, never absolute +compile-checkout paths. The CLI resolves both the package root and inputs under +one selected runtime repository/worktree root, canonicalizes them, and rejects +any realpath that escapes or crosses that checkout. A two-worktree test proves +it cannot fall back to a compile-time `CARGO_MANIFEST_DIR` in the other checkout. + +When a browser build is required, a missing `npm` or failed dependency setup is +a hard error. `TSJS_SKIP_BUILD=1` is a local-development escape hatch only when +every expected owner manifest and artifact exists and its recorded input digest +matches; CI never sets it. Build scripts emit `rerun-if-env-changed` for +`TSJS_SKIP_BUILD`, `TSJS_TEST`, and every tool environment variable that changes +output, in addition to complete `rerun-if-changed` input coverage. Every Rust CI +job that can build either browser crate installs the repository-pinned Node +version and dependencies, satisfying prerequisite #1204. + +The generated Rust API exposes typed module identifiers rather than accepting +unchecked strings. A Rust registration referencing a missing browser module +therefore fails compilation. JavaScript-only modules are valid and need no Rust +definition. + +The explicit Rust catalog and generated browser catalog are independent; +neither inventory is treated as the canonical list for the other. + +## Neutral Core Contracts + +The neutral contents of `integrations/registry.rs` move to singular +`trusted_server_core::integration`. Core continues to own: + +- `IntegrationDefinition` and `IntegrationRegistration` contracts. +- `IntegrationRegistry` and registry execution. +- Proxy, request-filter, attribute-rewriter, script-rewriter, + HTML-stream-processor, and head-injector traits and contexts. +- Neutral request-preparation and response-finalization hooks. +- Neutral request-processing and response-sharing annotations. +- Neutral browser-asset metadata, byte/hash access, load modes, and composed + document fingerprints. +- OpenRTB profile registration contracts consumed by the generic plan and + transport engines. +- Mediator registration contracts consumed by auction orchestration. +- Duplicate route, ID, renderer-type, and capability detection. +- Empty and stub registrations for core tests. + +Core does not own a global `IntegrationType` enum. The builder has typed methods +for each supported contribution, and one definition may supply any compatible +combination. The initial methods correspond only to behavior present in the +repository. + +An APS definition conceptually registers: + +```text +APS +├── proxy capability +├── head-injection capability +├── JavaScript renderer module +└── OpenRTB profile capability +``` + +Capability multiplicity is explicit. Collection capabilities such as routes +and rewriters may register multiple entries. Single-valued capabilities such as +an OpenRTB profile or mediator may appear at most once per integration +definition; duplicate registration fails composition. Because provider tables +have no profile discriminator, an integration that owns +`auction.providers` must register exactly one OpenRTB profile capability. + +Before constructing executable capabilities, each definition produces a pure +`IntegrationDeclaration` from its validated, unresolved-secret source view. It +contains provider/profile facts, mediator presence, browser modules, reserved +inputs, exact or pattern-based route claims, and ordered script-source claims. +These declarations are the shared input to CLI and runtime validation; they do +not allocate clients, read secrets, or execute a rewriter. + +A script-source claim is a pure transition +`apply_script_source(context) -> Unchanged | Replace(new_url) | Remove` with its +owning integration ID and source field. `context` carries the exact element and +attribute names, parsed URL, and relevant `rel`/`as` tokens; it therefore does +not confuse arbitrary `src`/`href` attributes with `