Prebid external first-party bundle loading#743
Conversation
7699203 to
9385864
Compare
14a91cc to
bd85e3e
Compare
fc56059 to
580207c
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Reworks Prebid to load an external first-party bundle proxied through the server (hard cut: external_bundle_url required when enabled, no embedded tsjs-prebid), adds the trusted-server-cli crate, the Osano consent mirror, and a hash-verified config-store payload. Overall this is high-quality, security-conscious work — the proxy path in particular is well-built. Verdict is REQUEST_CHANGES for one validation-parity bug plus a few open questions; the blocking fixes are small.
Reviewed against base
feature/ts-cli-next(this is a stacked PR — the range also carries Osano #773 and tester-cookie-clear #797). 1 of the inline comments carries a one-click, scratch-verified GitHubsuggestion; the rest describe fixes in prose because they span multiple files, touch new files, or are design questions.
Blocking
🔧 wrench
ts config validateskips external-bundle validation (CLI passes, server boot-fails) — see inline atcrates/trusted-server-core/src/integrations/prebid.rs:233(one-click suggestion, verified: 130 tests green)
❓ question
- External-bundle builder mutates git-tracked generated files; restore is
finally-only, not signal-safe — see inline atcrates/js/lib/build-prebid-external.mjs:263 config push --dry-runsafety rests entirely on the pinned edgezero-adapter rev — see inline atcrates/trusted-server-cli/src/edgezero_delegate.rs:329request_body_bytesdead scaffolding with a latent stream-drop (rotate-key → default-key rotation) — see inline atcrates/trusted-server-core/src/request_signing/endpoints.rs:27
Non-blocking
🤔 thinking / 🌱 seedling / 📝 note / ⛏ nitpick
- Infallible
Result+ unused arg churn on body helpers — see inline atcrates/trusted-server-core/src/publisher.rs:35 - CLI env/secret handling diverges between manifest-command and in-process paths — see inline at
crates/trusted-server-cli/src/edgezero_delegate.rs:108 - Redundant/split HTTPS redirect check — see inline at
crates/trusted-server-core/src/proxy.rs:1413 - Tester set/clear endpoints unauthenticated (design carry-over) — see inline at
crates/trusted-server-core/src/tester_cookie.rs:78 OsanoConfiglacksdeny_unknown_fields— see inline atcrates/trusted-server-core/src/integrations/osano.rs:21validate_enabled_integrationshardcodes the integration list — see inline atcrates/trusted-server-cli/src/config_command.rs:187- CLI crate is
edition = "2021"vs documented 2024 — see inline atcrates/trusted-server-cli/Cargo.toml:5
Cross-cutting / body-level findings
- 🤔 Open-proxy fallback when
proxy.allowed_domainsis empty —validate_external_bundle_config(prebid.rs:245) only enforces the allowlist when non-empty, andredirect_is_permitted(proxy.rs:1211) treats an empty allowlist as "permit all". With an empty list, a redirecting origin can send the bundle fetch to any HTTPS host (≤4 hops) and it's streamed as first-party JS with immutable cache;require_httpsis the only backstop. The host match itself is correctly dot-boundary-safe (evil-example.com≠*.example.com— verified). Consider requiring a non-empty allowlist wheneverexternal_bundle_urlis set. - 🤔 Two
edgezero-corerevs in the dependency graph —trusted-server-clipins@2eeccc97(edgezero-cli/adapter) while its deptrusted-server-corepins@38198f98, so the CLI's graph compiles two full copies ofedgezero-core. Verified benign for correctness today (onlyString/BTreeMapcross the boundary), but it's build-cost + a latent footgun the moment anedgezero_coretype crosses that seam. Align to one rev — the split looks accidental. - 🌱 Test-coverage gaps — no end-to-end test for the proxy fetch+sanitize happy path (
handle_external_bundlewith a mock streaming client); no mjs-level test assertingfilename sha256 == hash(bytes)and SRI derivation (the load-bearing CLI↔runtime contract);buildMirrorPlanuntested with USP+GPP+TCF all present at once. - 🤔 Build-tooling robustness (benign today) —
--outresolves against cwd in the Rust CLI but__dirnamein the mjs (safe only because the CLI always passes an absolute path); the mjs temp file is a fixed name withemptyOutDir:false, so concurrent/overlapping runs into one dir can hash a partial file. Align the mjs base toprocess.cwd()and PID-suffix the temp file. - 📝 Insecure-
certificate_checkwarning relocated — the old startuplog::warn!("INSECURE: proxy.certificate_check is disabled")inload_settingsis gone; the warning still fires per-backend at connect time (adapter-fastly/src/backend.rs:220), so it's not lost but the semantics shifted from once-at-startup to per-connection. Confirm operators still see it.
👍 Praise
- Proxy path disables both EC-ID and client-header forwarding for the static-asset fetch (
without_ec_id().without_forward_headers()), rebuilds responses from a strict header allowlist, stripsSet-Cookie, and forcesno-storeon non-200 to protect the immutable cache. Cache-mode/version logic robustly resists cache poisoning (exact-hash immutable mode, multi-vrejection). - sha256/SRI formats line up across all three layers (mjs emitter → CLI manifest validator → runtime config validator) — the invariant this whole PR rests on.
- Osano consent mirror is genuinely fail-safe (never fabricates a permissive signal on missing/malformed input) with a real
mirrorGenerationrace guard and test. write_atomic/ensure_output_dir_writableand the argv-arraynpminvocation (no shell) are careful, well-tested systems code; the config payload is hash-verified with no panics on operator input; real domains/secrets were removed from source control.
CI Status
- All GitHub checks: not run — the host-target CLI clippy/test workflows are added by this PR but haven't executed on this branch. Recommend pushing to trigger CI before merge, since the two-rev edgezero graph and the new CLI crate are only exercised there.
580207c to
2f0b7d0
Compare
Co-authored-by: Aram Grigoryan <132480+aram356@users.noreply.github.com>
b82ab1e to
0526575
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary — Approve, conditioned on green CI ✅ (pending 🔧)
Second review pass (retargeted to main; force-pushed since pass 1). The Prebid external-bundle code is merge-ready and every finding from the first review was addressed correctly. I'm approving the code — but this approval is contingent on the two required CI gates below going green; branch protection should hold the merge until they do.
Prior review fully addressed (verified)
- CLI ↔ runtime validation parity —
validate_config_for_startupnow callsvalidate_external_bundle_config(prebid.rs:234); runs at both deploy-audit and registry-build time. - Streaming-body guard —
request_body_bytesnow rejectsis_stream()withBadRequest. - Open-proxy on empty allowlist — now a hard startup error requiring
proxy.allowed_domainswhen an external bundle is configured (prebid.rs:436), so the open-mode path is unreachable for this route. OsanoConfigdeny_unknown_fields— added, with a rejection test and deploy-validation coverage.- mjs hash test — added and genuine (asserts
filename == trusted-prebid-${sha256(bytes)}.jsand the sha384 SRI against independently-computed digests).
Security posture verified sound: EC-ID + client-header forwarding disabled for the bundle fetch, https_only on initial and redirect hops, origin Set-Cookie stripped, no-store on non-200, multi-v/off-version → 404, and the one expect() on external_bundle_sha256 is guarded by schema validation. sha256/SRI consistent across mjs → manifest → CLI validator → runtime.
Blocking on CI (must be green before merge)
- 🔧
cargo fmt --checkfails (required gate). Confirmed on CI and locally with the pinned rustfmt 1.95.0 —mainpasses locally, this branch does not (e.g. the PR-addeduse super::{register, OsanoConfig};atosano.rs:53, which rustfmt wants as{OsanoConfig, register}). The reordering pattern spans nearly every changed file, indicating the branch was formatted with a newer rustfmt (1.96/nightly) than pinned 1.95.0. Fix: runcargo fmt --allunder the pinned toolchain and commit. - 🔧
prepare integration artifactsfails — dependency drift (required gate). This PR bumped the workspaceCargo.lock(aws-lc-rs → 1.17.1, time → 0.3.53, …) but leftcrates/trusted-server-integration-tests/Cargo.lockon the old versions, socheck-integration-dependency-versions.shfails on 5 packages. Fix (from the CI output):
cargo update --manifest-path crates/trusted-server-integration-tests/Cargo.toml -p aws-lc-rs --precise 1.17.1
cargo update --manifest-path crates/trusted-server-integration-tests/Cargo.toml -p aws-lc-sys --precise 0.42.0
cargo update --manifest-path crates/trusted-server-integration-tests/Cargo.toml -p rustls-pki-types --precise 1.15.0
cargo update --manifest-path crates/trusted-server-integration-tests/Cargo.toml -p time --precise 0.3.53
cargo update --manifest-path crates/trusted-server-integration-tests/Cargo.toml -p time-macros --precise 0.2.31Non-blocking (optional polish — see inline)
- 🌱 Content-addressed rebuild leaves
external_bundle_urlstale (patch_config_metadatarewrites hash/SRI but not the URL) — see inline. - 🌱 Error-path bundle responses drop
Content-Type/ set nonosniff— see inline. - ⛏
build-all.mjsdiscovery is filesystem-ordered — see inline (one-click suggestion). - 📝 Example config omits the runtime
external_bundle_*keys — see inline. - ⛏
HashSet::with_capacity(DEPLOY_VALIDATED_INTEGRATION_IDS.len())atconfig.rs:136oversizes a set that holds ≤3 auction-provider IDs — cosmetic;HashSet::new()reads clearer. - 🌱 The bundle generator buffers all stdout/stderr (
prebid_bundle.rs:70) and shows nothing until esbuild exits — consider inherited/streamed stdio.
👍 Praise
Strong, responsive iteration — all pass-1 findings fixed correctly. The IntegrationBuilder/registered_builder_ids() refactor plus the deploy_validation_covers_registered_integration_builders test is nice defensive design; the argv-array npm invocation stays injection-free; the mjs hash test is a real known-answer test.
CI Status (against main)
cargo fmt: FAIL (required) ·prepare integration artifacts: FAIL (required)- CodeQL,
cargo test,cargo test (axum native),cargo check (cloudflare),cargo check/build/test (spin),cargo test (cross-adapter parity),vitest,format-typescript,format-docs,Analyze (rust/js/actions): PASS integration tests,browser integration tests,integration tests (EdgeZero entry point): SKIPPED
|
Follow-up commit 5904598 addresses the approval-pass follow-ups:
Local verification:
|
…-update # Conflicts: # crates/trusted-server-adapter-fastly/src/route_tests.rs # crates/trusted-server-cli/src/commands/audit/analyzer.rs # crates/trusted-server-cli/src/commands/audit/mod.rs # crates/trusted-server-cli/src/run.rs # crates/trusted-server-core/src/integrations/prebid.rs # crates/trusted-server-core/src/publisher.rs # crates/trusted-server-integration-tests/Cargo.lock # docs/guide/integrations/prebid.md
Main squash-merged the same server-side ad-templates feature (#680), then added Tinybird auction telemetry (#818), edgezero v0.0.4 pins (#862), and external first-party Prebid bundle loading (#743). Resolutions: - Files where this branch matched the impl branch tip take main's version (telemetry/prebid-bundle additions on top of identical feature code): orchestrator, endpoints, openrtb, prebid integration, fastly app, .gitignore, prebid docs - creative_opportunities.rs keeps this branch's version (adds the shared ad-stack gate and pattern machinery used by the ts CLI; main's copy was identical minus those additions) - publisher.rs takes main's version with this branch's should_run_server_side_ad_stack delegation to evaluate_ad_stack_gate re-applied - run.rs keeps both sides' CLI tests (ad-templates/audit + prebid bundle)
Summary
bundle_mode, no embedded/deferredtsjs-prebidloading, andexternal_bundle_urlis required whenever Prebid is enabled./integrations/prebid/bundle.jsscript and keep publisher Prebid script interception/removal to prevent duplicate instances.proxy.allowed_domainsenforcement.external_bundle_sha256optional; when present it drives versioned first-party URLs, immutable cache headers, andsha256:ETags. SRI is validated if configured but is not required.build-prebid-external.mjs.Quick how-to: generate and configure a Prebid bundle
cd crates/js/lib npm run build:prebid-external -- \ --adapters=rubicon,appnexus,openx \ --user-id-modules=sharedIdSystem,uid2IdSystem \ --out=dist/prebidUpload the generated
trusted-prebid-<sha256>.jsfromdist/prebid/to an HTTPS asset host that is allowed byproxy.allowed_domains.Copy values from
dist/prebid/manifest.jsoninto config:Trusted Server injects the same-origin
/integrations/prebid/bundle.jsroute for browsers; publishers should not reference the external asset URL directly in page markup.Testing
cargo fmt --all -- --checkTSJS_SKIP_BUILD=1 cargo check -p trusted-server-coreTSJS_SKIP_BUILD=1 cargo test -p trusted-server-core prebid --libTSJS_SKIP_BUILD=1 cargo test -p trusted-server-core publisher::tests --libTSJS_SKIP_BUILD=1 cargo test -p trusted-server-core registry::tests --libTSJS_SKIP_BUILD=1 cargo test --workspaceTSJS_SKIP_BUILD=1 cargo clippy --workspace --all-targets --all-features -- -D warningscargo check -p trusted-server-jscd crates/js/lib && npm run buildcd crates/js/lib && npm run formatcd crates/js/lib && npx vitest runcd crates/js/lib && npm run build:prebid-external -- --adapters=rubicon --user-id-modules=sharedIdSystem --out=/tmp/trusted-server-prebid-external-hard-cutovercd docs && npm ci && npm run format