diff --git a/docs/superpowers/plans/2026-10-06-split-integrations-crates-implementation-plan.md b/docs/superpowers/plans/2026-10-06-split-integrations-crates-implementation-plan.md new file mode 100644 index 000000000..e5246907e --- /dev/null +++ b/docs/superpowers/plans/2026-10-06-split-integrations-crates-implementation-plan.md @@ -0,0 +1,224 @@ +# Split Integrations into Dedicated Crates Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move all concrete Rust and browser integrations into two statically linked crates, then make one ordered `[integrations]` configuration the operator inventory. + +**Architecture:** Milestone 1 introduces neutral core contracts, one Rust composition root, and separately owned browser artifacts while retaining the current operator/storage schema and behavior through one temporary normalizer. Milestone 2 replaces only the source schema and ordering contract, adds stored schema 2 with explicit order sidecars and a read-only schema-1 decoder, and cuts over each adapter only after its rollback gate passes. + +**Tech Stack:** Rust 2024, Cargo workspace, EdgeZero, Fastly/Axum/Cloudflare/Spin adapters, TOML 1.1, serde, Node 24, TypeScript, Vite, Vitest, Playwright. + +**Spec:** `docs/superpowers/specs/2026-09-17-split-integrations-crates-design.md` + +**Tracking:** [Issue #1237](https://github.com/IABTechLab/trusted-server/issues/1237), [draft design PR #1194](https://github.com/IABTechLab/trusted-server/pull/1194). + +--- + +## Execution rules and entry gate + +This is an implementation plan, not permission to start extraction from the spec worktree. As checked on 2026-10-06, draft PR #1194 is at `c6c34cdcd`, remote `main` is `7f610c0fc`, and the spec checkout still pins EdgeZero v0.0.8. The final implementation baseline is **not** either of those commits. Start a fresh implementation worktree from the post-prerequisite `origin/main`; do not merge the spec branch's old source tree into it. Re-read `AGENTS.md` there. + +Before Task 1, prove that independent fixes for #1098, #1196, #1198, #1199, #1200, #1202, #1203, #1204, and #1208 have landed with their focused tests. #1197 is closed, but the broader set-valued serialization audit remains an evidence gate. Confirm #1206/#1207 are resolved by #1208; #1201 belongs to milestone 2; #1205's hidden-first-phase remedy is rejected. Confirm the reviewed EdgeZero typed-config hook and environment-selector revision is available before Task 8. If any requirement is absent, stop that dependent task and land the prerequisite separately. Do not absorb a baseline bug fix into a rename commit. Record the exact `origin/main`, EdgeZero revision, test results, and output-golden hashes in the first implementation PR. + +Use the same public API manifest for every extraction commit. Every new/widened/removed symbol must have an owner, direct consumer, stability class, and (if transitional) removal step; an unlisted symbol needs a spec amendment. One active Rust catalog and one browser manifest exist at every revision; a transitional delegation replaces, rather than duplicates, an owner. No per-vendor crate, plugin ABI, runtime loading, second operator inventory, or unrelated audit-code reorganization. + +For each task below: write the named failing test or guard first; run it and record the expected failure; make the smallest change; run the focused target-matched command and the neighboring regression set; inspect `git diff --check`; commit one independently reviewable change using the repository's sentence-case imperative convention. The exact path list is based on the reviewed checkout and must be rechecked against the recorded post-prerequisite baseline before editing. + +## File and ownership map + +| Owner | Create or change | Responsibility | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Neutral Rust | `crates/trusted-server-core/src/integration/{mod,registry,claims,browser_assets,processing}.rs`; `auction/{plan,profile,provider,orchestrator}.rs`; `config_payload.rs`, `settings_data.rs`, `publisher.rs`, `html_processor.rs`, `tsjs.rs` | Typed contracts, plan compilation/execution, verified envelopes, neutral processing and browser-asset consumption; no concrete imports. Keep files split by responsibility rather than growing one registry file. | +| Rust application | `crates/trusted-server-integrations/{Cargo.toml,src/lib.rs}`, `src/{catalog,composition,config,legacy_config,source_config,validation,secret_metadata}.rs`, `src/integrations//mod.rs` | Static definitions, concrete implementations, source views, schema conversion, validation, and final composition. `source_config` is host-feature-gated. | +| Neutral browser | `crates/trusted-server-js/{build.rs,src/bundle.rs}`, `lib/src/core/`, `lib/src/shared/`, `lib/package.json`, `lib/{build-all.mjs,tsconfig.json,vitest.config.ts,eslint.config.js}` | One versioned runtime facade, core IIFE/finalizer, canonical Node project, neutral owner-private artifacts. | +| Integration browser | `crates/trusted-server-integrations-js/{Cargo.toml,build.rs,src/lib.rs}`, `lib/src/integrations/`, `lib/test/integrations/` | Integration IIFEs, inline programs, owned fixtures/tests, typed asset catalog and private artifacts. | +| App edges | `crates/trusted-server-adapter-{fastly,axum,cloudflare,spin}/src/app.rs`, Fastly `src/main.rs`, `crates/trusted-server-cli/src/{app_config,run,prebid_bundle}.rs` | Exactly one composition call per loaded config; source/deploy command views and target gates. | +| Cross-cutting checks | `Cargo.toml`, `.cargo/config.toml`, `.github/workflows/{format,test,integration-tests}.yml`, `crates/trusted-server-integration-tests/`, `scripts/`, `docs/guide/`, `trusted-server.example.toml` | Target-matched gates, real embedded-artifact tests, parity, rollout evidence, docs and stale-path guard. | + +The names in the new crates are responsibility boundaries, not a requirement to create empty placeholder modules. Create each file when its first consumer and test arrive. Keep the existing Next.js nested implementation and fixtures together; leave neutral `ec/prebid_eids.rs` in core. + +## Milestone 1 — crate boundary with schema-one behavior + +### Task 1: Freeze the corrected baseline and public surface + +**Files:** Create `crates/trusted-server-integration-tests/tests/integration_split_baseline.rs` and a reviewed output-golden directory under `crates/trusted-server-integration-tests/fixtures/integration_split_baseline/`; create `scripts/check-integration-api-delta.sh` and its checked-in manifest; modify only baseline-test selectors in `.github/workflows/integration-tests.yml`. + +- [ ] Add differential cases for every current Rust definition and browser module, globally interleaved APS/Prebid/standard provider IDs, deferred Prebid, GPT diagnostics, Next.js/GTM/RSC, #1135, routes, trusted script attributes, CLI views, and relevant failure classifications. Store actual corrected-baseline output bytes and separate artifact hashes; do not hand-author expected runtime output. +- [ ] Run the harness against recorded post-prerequisite `main` twice. Expected: byte-identical behavior goldens; artifact hashes may be recorded separately. Prove each golden fails after a deliberate test-only mutation to its observed output, then restore the mutation. +- [ ] Capture the current public core symbol snapshot and write an explicit delta manifest for the spec's three classes. Test that an undeclared added/removed public item fails the guard, then restore the canary. +- [ ] Verify deterministic serialization by repeatedly parsing a fixture with at least two values in every set-valued field and comparing envelope SHA and template identity. If it fails, stop for the standalone prerequisite fix. +- [ ] Commit the baseline evidence and its exact commit/toolchain/EdgeZero pins. Do not proceed if a prerequisite output defect is still present. + +### Task 2: Introduce neutral registration, claims, and plan inputs + +**Files:** Create `crates/trusted-server-core/src/integration/{mod,registry,claims}.rs`; modify `crates/trusted-server-core/src/{lib.rs,integrations/registry.rs,integrations/mod.rs,auction/plan.rs,auction/profile.rs,auction/provider.rs,auction/mod.rs}`; test in those modules. + +- [ ] Write compile/runtime tests for `IntegrationRegistry::from_registrations`, capability-local order, duplicate/reserved route and script claims, and a flat ordinal-bearing provider sequence independent of lookup-map order. Expected initially: missing contracts or wrong order. +- [ ] Add `IntegrationDeclaration` and only the typed capabilities consumed by current implementations; make the registry accept registrations rather than constructing APS/Prebid or calling `builders()`. Keep the old fixed builder table as the sole schema-one source until the new composition root owns it. +- [ ] Introduce neutral profile and prepared exchange-or-skip contracts, a consuming bound response parser, renderer descriptors, transport-header view, and mediator registration. Remove vendor enum/downcast/constructor knowledge from core without changing OpenRTB wire bytes or the #1159 skip/admission matrix. +- [ ] Run `cargo test-fastly`, `cargo test-axum`, and the baseline differential harness. Expected: schema-one provider order and externally visible local labels match the golden. Run the API-delta guard and commit the neutral seam separately from any source moves. + +### Task 3: Move lifecycle decisions behind neutral processing requirements + +**Files:** Create `crates/trusted-server-core/src/integration/processing.rs`; modify `crates/trusted-server-core/src/{html_processor,publisher,cookies,cache_policy,response_privacy}.rs` and `crates/trusted-server-adapter-{fastly,axum,cloudflare,spin}/src/app.rs` plus Fastly `src/main.rs`. + +- [ ] Write failing matrices for DataDome origin/body/privacy requirements, GPT diagnostics preparation/finalization and cache vetoes, reserved Cookie/query normalization even when diagnostics is absent, and request-local mutable document state. Include every adapter's existing fallback and health behavior. +- [ ] Add narrow neutral per-request requirements and document-state factories. Make core apply requirements and fixed parser phases without importing `datadome` or `gpt_diagnostics`; keep current concrete implementations in their original owner until their individual move. +- [ ] Move fixed GPT-diagnostics input cleanup to the neutral ingress boundary. Preserve lenient Cookie handling and all-response privacy before cache/origin selection; integration requirements may only restrict baseline cache decisions. +- [ ] Run focused core module tests, `cargo test-fastly`, `cargo test-axum`, `cargo test-cloudflare`, `cargo test-spin`, and API/differential guards. Commit the lifecycle seam before moving DataDome or GPT diagnostics. + +### Task 4: Establish the one browser build lease and two-root toolchain + +**Files:** Create `scripts/with-browser-lease.mjs` and its tests; modify `crates/trusted-server-js/lib/{package.json,tsconfig.json,vitest.config.ts,eslint.config.js,build-all.mjs}`, `.github/workflows/{format,test,integration-tests}.yml`, `Cargo.toml`, `.cargo/config.toml`. + +- [ ] Add failing lease tests for outer and authenticated nested invocations, absent/wrong nonce, holder PID reuse, a live orphan child, stale recovery, and direct tool invocation. A nested `Cargo build → runner → npm → runner → Node` path must complete without deadlock. +- [ ] Implement one atomic-directory lease rooted at the canonical Node project. Keep the lock while the complete child process scope lives. Fail closed when ownership cannot be proven dead; no per-crate locks. Put `npm ci`, Vite, Vitest, TypeScript, ESLint, Prettier, Cargo browser builds, and Prebid bundle behind this runner. +- [ ] Configure the canonical package at `crates/trusted-server-js/lib` to resolve both source roots and `prebid.js` exports. Add production two-root `typecheck`, isolated failing canary, explicit test/lint/format globs, and exact GPT-bootstrap legacy formatter/linter exceptions. Keep one lockfile. +- [ ] Run runner unit tests and wrapped `npm run typecheck`, `npm run test`, `npm run lint`, `npm run format`. Expected: both roots are selected and the canary fails only in its isolated negative test. Commit tooling before moving browser files. + +### Task 5: Define the versioned browser facade and neutral APS/queue seams + +**Files:** Create focused facade/IDL/ABI snapshot and tests under `crates/trusted-server-js/lib/src/core/` and `lib/test/core/`; modify `lib/src/core/{index,auction,request,queue,registry}.ts`, `lib/src/shared/dom_insertion_dispatcher.ts`, `lib/src/integrations/{aps,gpt,prebid,permutive,testlight}/` only where required by the seam. + +- [ ] Write production-artifact tests proving one `window.tsjs`/first-impression service, Permutive context and log state across IIFEs, APS renderer registration without a core APS import, GPT pre-core adoption, Prebid deferred version mismatch rejection, and idempotent listener/queue behavior. Pin exact ABI IDL, generated record bytes, immutable published snapshot, and mismatch-before-initializer guards. +- [ ] Move shared state and stateful APIs behind a versioned facade; keep type-only cross-root imports and the one declared neutral bootstrap-safe GPT input. Do not duplicate core state inside IIFEs or change the #1191 first-impression algorithm. +- [ ] Replace numeric/lexical DOM-handler priority with synchronous registration order. Derive immediate-only DOM need from the dedicated accessor import; reject deferred use. Install the CSP-compatible neutral external queue finalizer after fixed synchronous post-unified assets, preserving success/failure fallback paths. +- [ ] Run wrapped browser Vitest, production typecheck, facade ABI/artifact tests, and the corrected #1196/#1199/initial-render golden cases. Commit the facade and dispatcher before relocating any browser owner. + +### Task 6: Split browser artifact ownership + +**Files:** Create `crates/trusted-server-integrations-js/{Cargo.toml,build.rs,src/lib.rs}` and the empty Rust crate shell `crates/trusted-server-integrations/{Cargo.toml,src/lib.rs}`; create owner-specific build modules under `crates/trusted-server-js/lib/`; modify `crates/trusted-server-js/{build.rs,src/bundle.rs}`, `Cargo.toml`, `.cargo/config.toml`. + +- [ ] Write failing clean/incremental/concurrent build tests: changing one integration source changes only its owner manifest/hash; changing the neutral first-impression input changes GPT's bootstrap input; stale or partial output and missing npm fail. Discovery includes every `index.ts` regardless of immediate, deferred, or standalone load mode. +- [ ] Build neutral core/finalizer and integration IIFEs/assets into separate private Cargo `$OUT_DIR` targets. Generate typed module IDs and exact-byte/hash catalogs; validate complete artifact-affecting input manifests, `TSJS_SKIP_BUILD` freshness, and Cargo rerun environment inputs. No shared `dist` scan or copy. +- [ ] Keep one explicit temporary browser manifest pointing to unmoved source paths, then replace each mapping in Task 11; do not copy an unused duplicate source tree. Create both new Cargo members and update target-matched aliases in this change. +- [ ] Run wrapped build/test/typecheck and owner-manifest tests, then `cargo check-fastly`, `cargo check-axum`, `cargo check-cloudflare`, `cargo check-spin`. Commit the build split; Task 9 exports exact composed bytes for browser harnesses after the composition root exists. + +### Task 7: Make neutral core consume composed browser assets + +**Files:** Create `crates/trusted-server-core/src/integration/browser_assets.rs`; modify `crates/trusted-server-core/src/{publisher,tsjs,html_processor}.rs` and `crates/trusted-server-js/src/bundle.rs`; test in core, integration tests, and browser harnesses. + +- [ ] Write failing tests for core/creative/immediate/fixed synchronous/finalizer/deferred/standalone order, exact static bytes and hashes, stale `?v=` response behavior, trusted attributes, APS immediacy, and cache fingerprint invalidation when an inline or external asset changes. +- [ ] Introduce `CompiledBrowserAsset` and `BrowserDocumentAssets` as neutral byte/hash/order inputs. Add and test injection-capable core serving, script-tag, and HTML APIs while the existing production caller remains the sole active asset source until Task 9's atomic composition switch. Core must never link `trusted-server-integrations-js` or look up a concrete module by unchecked string. +- [ ] Implement the spec's unambiguous document fingerprint and pure composition-digest function with an injected full artifact build ID. Pin raw-byte, length-framing, attribute, asset-order, and secret-reference vectors. Keep the corrected #1198 cache key active until Task 9 assembles the full adapter ID; do not replace it with a core-only digest. +- [ ] Run focused core tests, browser artifact vectors, template-cache smoke, and the baseline differential harness. Commit this neutral consumption boundary before adapter composition changes. + +### Task 8: Add the Rust composition root while retaining schema one + +**Files:** Fill `crates/trusted-server-integrations/src/lib.rs`; create `src/{catalog,composition,config,legacy_config,source_config,validation,secret_metadata}.rs`; modify `crates/trusted-server-core/src/{config,config_payload,settings_data,integrations/registry}.rs`, `Cargo.toml`, `.cargo/config.toml`, `Cargo.lock`. + +- [ ] Before edits, verify the reviewed EdgeZero `run_*_typed_with_hooks` revision is available and repin all `edgezero-*` dependencies together. Add tests proving one exact config read/value across pre-pass, overlay, validation, serialization, diff, and push, including source replacement and `--no-env`. If the extension is absent, stop; do not substitute filesystem snapshots. +- [ ] Write failing catalog completeness and schema-one differential tests. The catalog initially has one entry per current definition, each delegating to the old owner through an explicit transitional export. One legacy converter preserves omitted-default behavior, fixed builder sequence, globally lexical flat provider order, implicit APS, current local external labels, and baseline unknown-ID acceptance. +- [ ] Move `TrustedServerAppConfig`, catalog-owned secret metadata/validation, inactive integration-secret preprocessing, and source/partial/validated views outward. Keep only neutral envelope/chunk verification, global settings, global inactive-secret preprocessing, secret primitives, and plan compilation in core. Add `CompositionAttempt` with the exact settings-capture stage specified in the design. +- [ ] Reuse the prerequisite canonical generator from core build support to emit labeled raw component digest records for core, OpenRTB, both browser embedding crates, and the integrations crate. Each crate records its own authoritative source/build inputs and forwards direct runtime workspace dependency records; conflicting duplicate labels fail. Do not read another crate's `$OUT_DIR`. +- [ ] Make the new root produce one settings/plan/orchestrator/registry/assets/target/digest composition in focused tests, without activating a second production catalog. Leave deletion of core's `builders()`/concrete `with_plan` path to Task 9's atomic consumer switch. Add neutral core test stubs behind `test-utils`; do not create a dev-dependency cycle. +- [ ] Run `cargo test -p trusted-server-integrations`, `cargo test-fastly`, `cargo clippy-fastly`, API/differential guards, and `cargo tree -p trusted-server-core` to prove no reverse dependency. Commit the inert composition root separately from owner moves. + +### Task 9: Rewire every adapter, loader, and CLI command to that root + +**Files:** Modify four adapter `src/app.rs` files, Fastly `src/main.rs`, `crates/trusted-server-cli/src/{app_config,run,prebid_bundle}.rs`, `src/commands/{config/ad_templates,audit/ad_templates,audit/generate/validate}.rs`, `crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs`, `tests/common/config.rs`, and their tests; create `crates/trusted-server-integration-tests/src/bin/export-browser-artifacts.rs`; modify `browser/tests/shared/aps-renderer.spec.ts`, `browser/initial-render/run.cjs`, `scripts/{integration-tests-browser,template-cache-local-test}.sh`. + +- [ ] Write failing tests for each adapter's verified-byte source, config location, missing/unpropagated versus corrupt data, Fastly JA4/settings-only failed-startup path, and unchanged adapter health/status matrix. Add a test that no adapter reconstructs plan, registry, browser assets, or digest. +- [ ] Replace direct core config/catalog imports with the integrations facade. Keep one resolved config location and one verified byte source; pass the staged composition result to existing route construction. Fastly alone changes post-store-open missing root/chunk to transient 503, never partial decode; early store-open and JA4 failures stay 500. +- [ ] Give each adapter its own component record, collect the transitive runtime workspace records after static linking, and compute the spec's artifact-specific `build_id`. Add a target-matched `cargo metadata` graph test for each adapter that rejects missing/extra/conflicting labels and a perturbation test for adapter, OpenRTB, integration, browser, compiler/flag, and root-workspace inputs. Only then switch the publisher template key to the full composition digest while preserving its existing URL, host, scheme, cookie, Vary, and schema dimensions. +- [ ] Extract each adapter's fixed route registrations into a checked method/path/pattern manifest and derive the corresponding reserved claims from it. Add a parity test comparing actual router registration, precedence, and fallthrough against claims, including Fastly-only routes, Cloudflare's publisher fallback, and all cache-purge methods. Reject an integration claim that collides with any fixed route before startup. +- [ ] Route CLI read-only commands through `SourceConfigView`, recovery mutators through bounded `PartialSourceConfigView`, and deploy commands through `ValidatedSourceConfig` plus selected-target validation. Add repeatable `config validate --adapter` while preserving EdgeZero `--strict`. Move Prebid typed module requirements out of CLI schema; retain atomic, non-disclosing staging writes. +- [ ] In one atomic review unit, switch the CLI, all adapters, Viceroy generator, and parity helper to the facade/storage serializer and remove core's old concrete `builders()`/`with_plan` and asset lookup paths. Run CLI, all four adapter tests, Viceroy parity, failed-startup cases, and differential goldens. No revision may have two active catalogs or loaders. +- [ ] Add the host-only test exporter now that composition exists: for an explicit fixture, emit exactly its embedded bytes, hashes, IDs, and load phases to a fresh directory. Point out-of-Cargo browser harnesses at its manifest, verify every hash, and remove newest-file/shared-`dist` selection. Run exporter and browser tests before committing this separate tooling change. + +### Task 10: Move the fifteen concrete Rust owners and their tests + +**Files:** Move `crates/trusted-server-core/src/integrations/{adserver_mock,aps,datadome,didomi,google_tag_manager,gpt,gpt_diagnostics,js_asset_proxy,lockr,nextjs,osano,permutive,prebid,sourcepoint,testlight}` into same-named directories under `crates/trusted-server-integrations/src/integrations/`; add `openrtb/mod.rs`; modify both crates' `lib.rs`, catalog, test support, and migration guards. + +- [ ] For each ordinary owner, add a failing catalog/behavior test, move its code and fixtures with a rename-focused diff, remove exactly its transitional export, run its focused tests and API guard, then commit. Suggested independent batches: simple page owners; Next.js/GTM/script claims; DataDome/GPT diagnostics after Task 3; APS/Prebid after Task 2; `adserver_mock` after mediator contracts. +- [ ] Add the built-in Rust-only `openrtb` definition for the current standard profile. End with exactly sixteen catalog entries, one directory per Rust definition, and no concrete import in core. JavaScript-only `creative` remains a fixed prelude, not a Rust definition. +- [ ] Move integration-owned test fixtures/helpers outward; replace core tests needing the real catalog with neutral registrations and an integration-owned production-catalog test factory. Move the complete Fastly-SDK guard over moved sources, including nested Next.js, without weakening neutral core checks. +- [ ] After every batch run `cargo test -p trusted-server-integrations`, `cargo test-fastly`, `cargo clippy-fastly`, completeness/API/path guards, and the differential harness. At the end run native/WASM target checks and prove the transitional Rust-export set is empty. + +### Task 11: Move every concrete browser owner, inline program, and test + +**Files:** Move `crates/trusted-server-js/lib/src/integrations/` and `lib/test/integrations/` into `crates/trusted-server-integrations-js/lib/`; move integration-owned fixtures from `lib/test/fixtures/` case by case; move `crates/trusted-server-core/src/integrations/gpt_bootstrap.js` and integration-owned executable inline programs to their browser owner; modify `lib/build-all.mjs`, `lib/build-prebid-external.mjs`, `crates/trusted-server-cli/src/prebid_bundle.rs`, and owned Rust includes. + +- [ ] For each owner, first add a failing wrapped Vitest/artifact/load-order case, move source and tests with a rename-focused diff, remove its legacy manifest path, run wrapped build/typecheck/lint/format/test and the exporter-based production-artifact check, then commit. Keep owner-specific fixtures with their owner; leave cross-adapter Playwright/parity in the integration-test crate. +- [ ] Move APS renderer document and GPT bootstrap with their existing CSP/header behavior; audit Sourcepoint, GPT diagnostics, DataDome, Didomi, GPT, Prebid, and Sourcepoint executable inline inputs. Browser core retains no concrete APS import and Rust retains no handwritten production integration browser algorithm. +- [ ] Make `ts prebid bundle` resolve one canonical package root and all sibling source realpaths in the same checkout, preserve package exports and managed User ID/analytics selection, and prove a two-worktree test cannot use a compile-time path from another checkout. +- [ ] Run wrapped JS gates, APS/Prebid/GPT production artifact tests, Playwright initial-render and shared suites, all adapter parity tests, and the differential harness. Prove every moved test is selected, the legacy browser manifest path set is empty, and no duplicate unused source tree remains. + +### Task 12: Exit milestone 1 only with schema-one parity + +**Files:** Modify `.cargo/config.toml`, `.github/workflows/{format,test,integration-tests}.yml`, `AGENTS.md`, relevant `.claude/agents/` and `.claude/commands/`, `scripts/`, `docs/guide/`, crate READMEs, and `crates/trusted-server-core/src/migration_guards.rs`; create the stale-path/selected-test guard under `scripts/`. + +- [ ] Add a repository guard for live references to old concrete Rust and browser paths, including constructed paths and test selections; allowlist historical docs only. Update every explicit Fastly alias package list and a dedicated host integrations-crate test/clippy gate. Keep Fastly as sole default member and preserve CLI/codegen host gates. +- [ ] Prove the catalog has sixteen definitions, browser discovery includes deferred/standalone modules, core's dependency graph has no integrations crate, adapters/CLI do not import vendor modules, no transitional API/path remains, and all schema-one differential outputs match except the spec's explicit milestone-one corrections. +- [ ] Run every repository gate listed in `AGENTS.md`: Rust format, all target-matched clippy aliases and tests, host CLI/codegen gates, native/WASM checks/builds, cross-adapter parity, wrapped JS build/typecheck/lint/format/Vitest/Playwright, documentation snippets/VitePress and docs format. Record exact commands, exit codes, and intentional behavior deltas. +- [ ] Merge milestone 1 only as a complete crate boundary. Do not activate schema 2 or schema-2 operator source in this PR. + +## Milestone 2 — ordered source, storage, and rollout + +### Task 13: Add ordered source parsing and strong identities behind a fence + +**Files:** Create `crates/trusted-server-integrations/src/{source_config,provider_ids}.rs` (host feature for source parsing); modify `src/{config,validation}.rs`, `crates/trusted-server-core/src/{auction_config_types,auction/plan}.rs`, workspace `Cargo.toml` and `Cargo.lock`. + +- [ ] Add failing fixtures for nonlexical parent and provider order, descendant-before-parent, shorthand parent, missing `enabled`, disabled retained settings, unknown IDs/fields, old/new syntax mixtures, local/qualified ID grammar, and overlays that try to create/remove/reorder tables. +- [ ] Upgrade the direct `toml_edit` to the selected TOML-1.1 generation and enable direct `toml/preserve_order` only in the host `source-config` feature used by normal CLI/install graphs. Parse source structure before the EdgeZero typed overlay; check typed order equals the pre-pass order. Keep WASM runtime graphs free of host pre-pass code. +- [ ] Implement the new `[integrations.]` parser as an internal, test-only candidate path with explicit parent `enabled` and nested providers. Normal validate/diff/push still use only schema-one source, and no new-source candidate can serialize or reach a remote write yet. Add `IntegrationId`, `LocalProviderId`, `QualifiedProviderId`, and distinct `ExternalProviderLabel` with exact serde/Display and collision tests. +- [ ] Run host source-parser, CLI/install dependency-graph, WASM target, and parity tests. Assert an ordinary command rejects new-source syntax while the fence is active. Commit the inert parser/identity work before switching storage or runtime order. + +### Task 14: Add schema-2 storage DTO and the dual stored reader + +**Files:** Create `crates/trusted-server-integrations/src/{stored_config,legacy_config}.rs`; modify `src/{config,composition,secret_metadata}.rs`, `crates/trusted-server-core/src/config_payload.rs`, CLI and integration-test serializer callers. + +- [ ] Write failing TOML → typed → envelope → runtime round trips with permuted JSON map members, sidecar missing/duplicate/extra IDs, every stored integration `enabled` (including `false`), default omission, disabled source-shaped values, and unchanged DataDome secret object paths. +- [ ] Serialize schema 2 as object maps plus `integration_order` and local `provider_order` sidecars; always emit and require parent `enabled`. Validate the sidecar/map bijection at source-validation, serialization, and runtime read. Runtime order must never come from JSON map iteration. +- [ ] Keep one schema-one converter that preserves current unknown-ID/default acceptance, implicit APS, globally lexical flat provider sequence, and local external labels. Reject unknown schema marker values before secret resolution; prove archived rollback binaries reject schema-2 root markers. +- [ ] Keep the schema-2 writer and dual reader reachable only from tests/internal candidate validation until Tasks 15–17 have migrated CLI consumers, activated schema-2 ordering, and installed adapter write fences. Normal CLI commands must still reject new-source syntax or refuse its remote write; add an explicit negative test after this commit. One reviewed final cutover commit removes that fence only after Task 17's no-write gates pass. +- [ ] Add bidirectional release-compatibility corpus coverage: candidate writer → oldest rollback/deployed/candidate readers and frozen deployed/live schema-2 bytes → candidate reader, including generated leaf/variant coverage. Run serializer, secret, adapter-loader, and source parity tests; commit the storage boundary separately from order activation. + +### Task 15: Migrate CLI source consumers and provide a safe migration command + +**Files:** Modify `crates/trusted-server-cli/src/{app_config,run,prebid_bundle}.rs`, `src/commands/config/{mod,ad_templates}.rs`, `src/commands/audit/{ad_templates,generate/validate}.rs`; create `src/commands/config/migrate.rs`; modify `crates/trusted-server-integration-tests/{src/bin/generate-viceroy-config.rs,tests/common/config.rs}`. + +- [ ] Write failing CLI cases for one exact source read under concurrent replacement, source/pre-pass/overlay parity, stale environment path names, selected-target diff/push/validate, read-only overlay views, bounded recovery edits, and non-disclosing errors. Confirm `--strict` remains EdgeZero's manifest check. +- [ ] Implement `ts config migrate --dry-run` as a local-only, comment/permission-preserving legacy-source reader that emits a validated schema-2 candidate. Report explicit default normalization, every provider/hook/browser reorder, renamed overlays, unknown tables, and manual decisions; require per-ID unknown discard, explicit noninteractive reorder acceptance, and unchanged-file check before an interactive write. +- [ ] Implement the candidate-path Prebid bundle, ad-template, audit, and fixture-generator changes behind the same source cutover fence as Tasks 13–14. Before Task 18, ordinary commands retain their schema-one behavior and inventory, while explicit candidate/migrate tests exercise the new paths. At final cutover, Prebid requires an existing explicit parent and edits only descendants, preserving file mode and staged hash/SRI behavior; update help/rule counts and exact-message/docs snippets in that same review unit. +- [ ] Run CLI command tests, actual `cargo install --path crates/trusted-server-cli --locked` in a temporary install root, Viceroy fixture generation/parity, and secret-sentinel tests. Commit all source consumers before any schema-2 remote write is enabled. + +### Task 16: Switch Rust and browser execution to declaration order + +**Files:** Modify `crates/trusted-server-core/src/{integration/registry.rs,auction/plan.rs,auction/orchestrator.rs,auction/provider.rs,publisher.rs}`, `crates/trusted-server-integrations/src/{composition,catalog}.rs`, browser dispatcher and integration IIFEs, and relevant tests. + +- [ ] Write failing tests for each comparable hook phase, immediate/deferred browser lists, provider launch/response/mediator-input/tie order, remaining deadline before each launch, plan-order recovery after failure, and ordered diagnostics. Include globally interleaved schema-one provider IDs to prove old order remains unchanged under the dual reader. Cover known-disabled bidder suppression without client-side fallback and disabled-mediator local ranking. +- [ ] Add a stored-schema-2 catalog-drift test: widen one script-source claim and one integration-owned native route in a candidate catalog after a blob is stored. Runtime must stay available; the exact-URL asset-policy clamp restores the original first-party path for an enabled asset or removes a blocked asset, and asset-wins handling drops only the newly colliding integration route. It emits redacted sampled diagnostics. Fixed-route or unrelated native/native conflicts still fail composition. +- [ ] Feed ordinal-bearing neutral plan and registration inputs from schema-2 sidecars. Use definition-local order only within a single integration. Remove numeric/lexical browser handler sorting; keep fixed core/creative, diagnostics, finalizer, and deferred lifecycle phases explicit. +- [ ] Validate complete script-source and route claims before execution, including parent-disabled pruning and policy-consistent final chain outcome; do not give `js_asset_proxy` an invisible first phase. Keep corrected #1199/#1208 behavior identical for schema one. Use qualified external labels only for schema 2. +- [ ] Run ordering, auction, browser, route, cache, and differential tests on both schema versions and every adapter. Commit the intentional ordering change only with all dependent consumers and diagnostics ready. + +### Task 17: Fence writes and implement adapter-specific rollout tooling + +**Files:** Modify `crates/trusted-server-cli/src/run.rs` and config command modules; create focused copy/export/rollout-record helpers under `crates/trusted-server-cli/src/commands/config/`; modify Fastly, Cloudflare, Spin, Axum config-loader tests and `docs/guide/` rollout guidance. + +- [ ] Write failing no-mutation tests for schema-2 push to an unverified Fastly tuple, Cloudflare KV, and unproven remote Spin. Add a Fastly `config gc` test that refuses both protected physical stores and any missing/stale accepted-pair record before EdgeZero can sweep. +- [ ] Implement checked-in Fastly generation copy using explicit source/destination service-version/store/root-key tuples, chunk-first/root-last exact-byte copy and full readback hash/length verification. Record accepted active/rollback tuples only after both candidates and POP-visible probes pass; do not mutate currently selected immutable roots during paired edits. +- [ ] Add `ts config validate-stored-catalog --inventory --exports `. In a test-controlled temporary root, export every authoritative live environment's exact verified envelope and adapter/location metadata, prove inventory/export bijection, then run candidate pure route/script claims without resolving secret values. Refuse missing/tampered exports or any degraded conflict; log only redacted IDs and hashes. The command never deletes its input; release-pipeline cleanup removes only its own nonce-marked, non-symlink temporary directory. +- [ ] For every candidate release, decode each exported live schema-one envelope with both deployed and candidate readers under identical secret resolution and compare normalized settings, activation, hook/browser/route/provider order, external labels, and errors. A mismatch or missing environment blocks promotion even if the archived baseline fixture passes. Run the bidirectional same-schema-2 corpus gate from Task 14 over all live schema-2 exports too. +- [ ] Implement protected Cloudflare `export-binding` with exact selected outer property/envelope bytes and no stdout/overwrite. Keep old Worker code + schema-one binding paired for rollback. Reject remote Spin schema-2 push until versioned export/restore/selection drill exists; allow local Axum migration. No generic force flag bypass. +- [ ] Add authenticated unsampled unique-probe schema/artifact/digest observation and a settings-dependent response predicate without a new public status route. Block activation if live-environment inventory/export bijection, candidate claim validation, version/binding readback, or rollback isolation is unproven. After explicit rollback-window closure, GC may resume only with verified absence of protected version bindings. +- [ ] Run Fastly tuple/GC/503 tests, Cloudflare binding-fallback tests, Spin no-write tests, Axum local migration, live-export/candidate corpus tests, and documented staged rollback drills. Commit tooling and runbook together; do not perform a production cutover as part of the implementation PR. + +### Task 18: Exit milestone 2 and define later cleanup + +**Files:** Modify `trusted-server.example.toml`, `docs/guide/{configuration,auction-orchestration,integration-guide}.md`, `docs/guide/integrations/{aps,prebid}.md`, `AGENTS.md`, browser/CI scripts and repository path guards; retain `legacy_config.rs`. + +- [ ] After Tasks 13–17 pass, make one final source/write activation change in the same review unit as the operator guidance below: activate the new parser and all Prebid/ad-template/audit/fixture command paths together; ordinary commands accept only new `[integrations]` source, serialize schema 2, and call the installed per-adapter push fence. Assert old source, mixed source, and an unverified destination fail before any remote operation. The read-only schema-one blob decoder remains active. +- [ ] Convert examples, operator guides, environment names, CLI diagnostics, dashboards/telemetry migration notes, template-cache smoke, and documentation snippets to the single ordered inventory and qualified schema-2 labels. Pin plain-language explanation that TOML parent order controls comparable hooks and nested-provider priority. +- [ ] Run every milestone-one repository gate again, plus dual-schema, migrate, same-schema release-compatibility, exporter, candidate-live-inventory, and adapter rollback suites. Review exact diff against milestone-one behavior goldens; every intentional change must be identified in the spec. +- [ ] Merge the dual reader and CLI write fences before any schema-2 data is pushed. Keep the schema-one stored decoder and archived writer for the rollback window. Remove the decoder only in a later separately reviewed release after every production adapter, including Spin, has completed its rollback drill. + +## Review checkpoints + +1. **Neutral Rust contracts:** after Tasks 1–3, confirm the new API delta is closed and corrected schema-one behavior is unchanged. +2. **Browser boundary:** after Tasks 4–7, confirm exact embedded bytes are used by runtime and out-of-Cargo tests; no private state copy or shared-`dist` race exists. +3. **Concrete extraction:** after Tasks 8–12, confirm two crates own all concrete sources, every consumer uses one composition root, transitional sets are empty, and full schema-one gates pass. This is the first merge milestone. +4. **Ordered config and rollout:** after Tasks 13–18, confirm source/store/runtime order, labels, adapter write fences, and rollback evidence. This is the second merge milestone. + +At each checkpoint, perform a self-review against all acceptance criteria in the spec, inspect the dependency graph and public API diff, and request an independent code review. Treat a new capability or public symbol outside the reviewed manifest as a design question, not an opportunistic extraction shortcut. 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..86f41c308 --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-split-integrations-crates-design.md @@ -0,0 +1,3944 @@ +# 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-10-01 review checkpoint is +`cb9452545`; it includes the post-discovery configuration-store, EC/EID, +stored-request, origin-readthrough, first-impression, and deterministic +context-allowlist 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 tracked at the 2026-10-01 review checkpoint. Their +issue states can change before implementation; the final baseline gate checks +behavior, not only whether an issue is closed. 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 behavior. The issue was closed after the context-allowlist fix, but the final baseline gate still proves deterministic serialization for every set-valued configuration input 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. | +| #1169 | Preserve the separate origin-readthrough/shareability gate, method-complete authenticated cache-purge route, and origin diagnostics. Integration requirements may only veto the baseline cache decisions. | +| #1191 | Preserve first-impression ownership, including pending-render behavior, retained denial tokens, one fallback reservation, and no timer-based release of a Trusted Server claim before render. Its bootstrap and browser test harness move with their owners. | +| #1209 | Preserve deterministic serialized context allowlists and canonical hashing of config envelopes and template fingerprints. | + +The landed #1209 change fixes the context allowlist and its canonical hashing +path. Closing #1197 does not waive the broader deterministic-serialization +check for other set-valued inputs used by the new fingerprints. + +Other open work is coordinated without making it architectural authority or an +automatic prerequisite: + +| Item | Coordination contract | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #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. | +| #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 | A missing or not-yet-propagated root/chunk uses the intentional transient 503 startup path. A store-open or other platform I/O failure retains the baseline 500; `GET /health` remains 200 liveness. | Application routes use the existing configuration/500 path; `GET /health` remains 200 liveness. | +| 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. | + +Fastly's entry-point config-store open before app construction and pre-router +JA4 settings read retain their existing 500 failure path; the named 503 +correction applies only after a store opens successfully and its selected +root/chunk is absent or unpropagated. `GET /health` bypasses these reads. + +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. The GPT integration target bundles +one bootstrap-safe entry point owned by neutral browser source before its own +listener code in the existing tag. This source-level dependency is explicitly +declared and allowlisted in the cross-root import gate; it is not a general +permission to import stateful core facilities. The integration manifest hashes +that neutral entry point and its generated ABI record, so a service change +invalidates GPT output without making unrelated integration source part of the +neutral manifest. No build script reads a sibling Cargo `$OUT_DIR`, and GPT +does not maintain a second claim algorithm. The targets 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. + +Out-of-Cargo browser tests do not find the newest Cargo `$OUT_DIR` or read a +shared `dist`. A host-only integration-test artifact exporter links the +integrations composition root, which links both Rust browser crates, and +accepts an explicit test configuration fixture. It writes that composition's +embedded production bytes, IDs, SHA-256 hashes, and load phases to a fresh +caller-selected temporary directory plus a manifest. The export includes GPT +bootstrap, APS renderer document, the neutral finalizer, immediate, deferred, +and standalone assets selected by the fixture. The exporter refuses a +pre-existing output directory and verifies every written hash against the +embedded value. Playwright and initial-render harnesses receive that manifest +path explicitly, verify hashes before evaluation, and fail on a missing or +unexpected asset. CI builds the exporter and runs any canonical Node project +work through the shared lease; it never stages a second authoritative bundle +set or guesses an output by modification time. The exporter is test tooling, +not a runtime endpoint or operator CLI command. + +The integration build discovers all directories containing `index.ts` and +emits one IIFE per entry point. Typed Rust registration and composition classify +those built assets as immediate, deferred, or standalone; filesystem discovery +does not select a load phase. 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, the declared neutral +first-impression source entry point and ABI record consumed by GPT, 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. Changing the neutral entry point +regenerates GPT's bootstrap artifact and its integration manifest; changing an +unrelated integration leaves both neutral output and GPT's bootstrap unchanged. + +`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 `