From 5bf4a9d4ad2e964537580b3ba880d79a01daa40c Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:20:09 -0700 Subject: [PATCH 1/2] Organize spec and design documents into docs/superpowers Move spec-like documents from root and docs/internal into a structured layout under docs/superpowers with timestamped filenames: - specs/: active design documents (SSC PRD, technical spec, EdgeZero migration, auction orchestration flow, production readiness report) - archive/: completed or historical specs (optimization, sequence, publisher IDs audit) --- .../archive/2026-03-19-sequence-design.md | 5 +- .../archive/2026-03-24-optimization-design.md | 144 +++-- .../2026-03-24-publisher-ids-audit-design.md | 14 +- ...3-11-production-readiness-report-design.md | 609 ++++++++++++++++++ ...03-19-auction-orchestration-flow-design.md | 46 +- .../2026-03-19-edgezero-migration-design.md} | 0 .../specs/2026-03-24-ssc-prd-design.md} | 0 .../2026-03-24-ssc-technical-spec-design.md} | 0 8 files changed, 729 insertions(+), 89 deletions(-) rename SEQUENCE.md => docs/superpowers/archive/2026-03-19-sequence-design.md (99%) rename OPTIMIZATION.md => docs/superpowers/archive/2026-03-24-optimization-design.md (75%) rename PUBLISHER_IDS_AUDIT.md => docs/superpowers/archive/2026-03-24-publisher-ids-audit-design.md (96%) create mode 100644 docs/superpowers/specs/2026-03-11-production-readiness-report-design.md rename AUCTION_ORCHESTRATION_FLOW.md => docs/superpowers/specs/2026-03-19-auction-orchestration-flow-design.md (97%) rename docs/{internal/EDGEZERO_MIGRATION.md => superpowers/specs/2026-03-19-edgezero-migration-design.md} (100%) rename docs/{internal/ssc-prd.md => superpowers/specs/2026-03-24-ssc-prd-design.md} (100%) rename docs/{internal/ssc_technical_spec.md => superpowers/specs/2026-03-24-ssc-technical-spec-design.md} (100%) diff --git a/SEQUENCE.md b/docs/superpowers/archive/2026-03-19-sequence-design.md similarity index 99% rename from SEQUENCE.md rename to docs/superpowers/archive/2026-03-19-sequence-design.md index b835a8dd..e6745bfb 100644 --- a/SEQUENCE.md +++ b/docs/superpowers/archive/2026-03-19-sequence-design.md @@ -106,12 +106,12 @@ sequenceDiagram TS->>TS: ✅ Reconstruct full URL
✅ Validate tstoken (enc+SHA256) TS->>CS: GET original_url CS-->>TS: 200 (image/HTML) - + opt 📄 HTML Response TS->>TS: 🔏 Generate signed target URLs
🔄 Rewrite resource URLs TS-->>U: 200 text/html (secured) end - + opt 🖼️ Image Response TS->>TS: ✅ Verify content-type
📊 Log pixel tracking TS-->>U: 200 image/* (proxied) @@ -128,6 +128,7 @@ sequenceDiagram ``` ## Notes + - TSJS - Served first-party at `/static/tsjs=tsjs-unified.min.js?v=`. The server dynamically concatenates core + enabled integration modules based on config. - Discovers ad units and renders placeholders; either uses slot-level HTML (`/first-party/ad`) or JSON auction (`/auction`). diff --git a/OPTIMIZATION.md b/docs/superpowers/archive/2026-03-24-optimization-design.md similarity index 75% rename from OPTIMIZATION.md rename to docs/superpowers/archive/2026-03-24-optimization-design.md index 7e6c84ab..d2097144 100644 --- a/OPTIMIZATION.md +++ b/docs/superpowers/archive/2026-03-24-optimization-design.md @@ -12,31 +12,31 @@ This document presents a performance analysis and optimization plan for the Trus ### CPU Breakdown — Top Level -| % CPU | Function | Notes | -|-------|----------|-------| -| ~96% | `trusted_server_adapter_fastly::main` | Almost all time is in application code | -| ~90% | `route_request` → `handle_publisher_request` | Publisher proxy is the hot path | -| **~76%** | **HTML processing pipeline** (`streaming_processor` → `lol_html`) | **Dominant bottleneck** | -| ~~5-8%~~ → **3.3%** | `get_settings()` | ~~Redundant config crate parsing~~ **Fixed** — now uses `toml::from_str` | -| ~5-7% | `handle_publisher_request` (non-HTML) | Backend send, cookie handling | +| % CPU | Function | Notes | +| ------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------ | +| ~96% | `trusted_server_adapter_fastly::main` | Almost all time is in application code | +| ~90% | `route_request` → `handle_publisher_request` | Publisher proxy is the hot path | +| **~76%** | **HTML processing pipeline** (`streaming_processor` → `lol_html`) | **Dominant bottleneck** | +| ~~5-8%~~ → **3.3%** | `get_settings()` | ~~Redundant config crate parsing~~ **Fixed** — now uses `toml::from_str` | +| ~5-7% | `handle_publisher_request` (non-HTML) | Backend send, cookie handling | ### CPU Breakdown — HTML Processing (~76% total) -| % CPU | Function | Notes | -|-------|----------|-------| -| **~47%** | `lol_html::parser` state machine | HTML tokenizer/parser — character-by-character parsing | -| ~11% | `create_html_processor` | Building the lol_html rewriter with all handlers | -| ~18% | Processing callbacks | URL rewriting, attribute scanning, output sink handling | +| % CPU | Function | Notes | +| -------- | -------------------------------- | ------------------------------------------------------- | +| **~47%** | `lol_html::parser` state machine | HTML tokenizer/parser — character-by-character parsing | +| ~11% | `create_html_processor` | Building the lol_html rewriter with all handlers | +| ~18% | Processing callbacks | URL rewriting, attribute scanning, output sink handling | ### CPU Breakdown — Other Components -| % CPU | Function | Notes | -|-------|----------|-------| -| ~2% | `IntegrationRegistry` | Route lookup + attribute rewriting + initialization | -| ~0.8% | Memory allocation (`RawVec::reserve`) | Buffer growth during processing | -| ~0.5% | Logging (`fern` / `log_fastly`) | Minimal overhead | -| ~0.5% | Synthetic ID generation | HMAC computation | -| ~0.5% | Header extraction | `fastly::http::handle::get_header_values` | +| % CPU | Function | Notes | +| ----- | ------------------------------------- | --------------------------------------------------- | +| ~2% | `IntegrationRegistry` | Route lookup + attribute rewriting + initialization | +| ~0.8% | Memory allocation (`RawVec::reserve`) | Buffer growth during processing | +| ~0.5% | Logging (`fern` / `log_fastly`) | Minimal overhead | +| ~0.5% | Synthetic ID generation | HMAC computation | +| ~0.5% | Header extraction | `fastly::http::handle::get_header_values` | ### Key Takeaways @@ -53,12 +53,12 @@ This document presents a performance analysis and optimization plan for the Trus Measured on `main` branch. Value is in **relative comparison between branches**, not absolute values. -| Endpoint | P50 | P95 | Req/sec | Notes | -|---|---|---|---|---| -| `GET /static/tsjs=tsjs-unified.min.js` | 1.9 ms | 3.1 ms | 4,672 | Pure WASM, no backend | -| `GET /.well-known/trusted-server.json` | 1.3 ms | 1.4 ms | ~770 | Server-side only | -| `GET /` (publisher proxy) | 400 ms | 595 ms | 21 | Proxies to golf.com, 222KB HTML | -| `POST /auction` | 984 ms | 1,087 ms | 9.3 | Calls Prebid + APS backends | +| Endpoint | P50 | P95 | Req/sec | Notes | +| -------------------------------------- | ------ | -------- | ------- | ------------------------------- | +| `GET /static/tsjs=tsjs-unified.min.js` | 1.9 ms | 3.1 ms | 4,672 | Pure WASM, no backend | +| `GET /.well-known/trusted-server.json` | 1.3 ms | 1.4 ms | ~770 | Server-side only | +| `GET /` (publisher proxy) | 400 ms | 595 ms | 21 | Proxies to golf.com, 222KB HTML | +| `POST /auction` | 984 ms | 1,087 ms | 9.3 | Calls Prebid + APS backends | - **WASM heap**: 3.0-4.1 MB per request - **Init overhead**: <2ms (settings parse + orchestrator + registry) @@ -68,11 +68,11 @@ Measured on `main` branch. Value is in **relative comparison between branches**, Measured externally against staging deployment (golf.com proxy), `main` branch. -| Endpoint | TTFB | Total | Size | Notes | -|---|---|---|---|---| -| `GET /static/tsjs=tsjs-unified.min.js` | ~204 ms | ~219 ms | 28 KB | No backend; includes client-network + edge path from benchmark vantage | -| `GET /` (publisher proxy, golf.com) | ~234 ms | ~441 ms | 230 KB | Backend + processing | -| `GET /.well-known/trusted-server.json` | ~191 ms | - | - | Returns 500 (needs investigation) | +| Endpoint | TTFB | Total | Size | Notes | +| -------------------------------------- | ------- | ------- | ------ | ---------------------------------------------------------------------- | +| `GET /static/tsjs=tsjs-unified.min.js` | ~204 ms | ~219 ms | 28 KB | No backend; includes client-network + edge path from benchmark vantage | +| `GET /` (publisher proxy, golf.com) | ~234 ms | ~441 ms | 230 KB | Backend + processing | +| `GET /.well-known/trusted-server.json` | ~191 ms | - | - | Returns 500 (needs investigation) | **Key insight**: Static JS has ~204ms TTFB with zero backend work **from this specific benchmark vantage point**. That number includes client-to-edge RTT, DNS, TLS/connection state, and edge processing — it is **not** a universal Fastly floor. @@ -153,9 +153,9 @@ fn process_gzip_to_gzip(&mut self, input: R, output: W) -> Re } ``` -| Impact | LOC | Risk | -|--------|-----|------| -| **High** (most responses are gzip; reduces peak memory) | -15/+3 | Low | +| Impact | LOC | Risk | +| ------------------------------------------------------- | ------ | ---- | +| **High** (most responses are gzip; reduces peak memory) | -15/+3 | Low | #### 1.2 Fix `HtmlRewriterAdapter` — enable true streaming @@ -197,8 +197,8 @@ impl StreamProcessor for HtmlRewriterAdapter { } ``` -| Impact | LOC | Risk | -|--------|-----|------| +| Impact | LOC | Risk | +| --------------------------------------------------------------------- | -------------- | ---------------------------- | | **High** (HTML is most common content type; eliminates 222KB+ buffer) | ~30 refactored | Medium — needs test coverage | #### 1.3 ~~Eliminate redundant `config` crate parsing in `get_settings()` — ~5-8% CPU~~ DONE (~3.3% post-fix) @@ -236,8 +236,8 @@ let settings: Settings = postcard::from_bytes(SETTINGS_DATA) **Recommendation**: Start with the `toml::from_str()` fix (1-line change, no new deps). If profiling still shows meaningful time in TOML parsing, upgrade to `postcard`. -| Impact | LOC | Risk | -|--------|-----|------| +| Impact | LOC | Risk | +| ---------------------------------------- | --- | -------------------------------------------- | | **Medium** (~5-8% → ~3.3% CPU, verified) | 1-3 | Low — `build.rs` already resolves everything | **Status**: Done. Replaced `Settings::from_toml()` with `toml::from_str()` + explicit `normalize()` + `validate()`. Profiling confirmed: **~5-8% → ~3.3% CPU per request**. @@ -258,21 +258,21 @@ let settings: Settings = postcard::from_bytes(SETTINGS_DATA) .max_level(log::LevelFilter::Info) ``` -| Impact | LOC | Risk | -|--------|-----|------| -| Low (~0.5% CPU) | ~3 | None | +| Impact | LOC | Risk | +| --------------- | --- | ---- | +| Low (~0.5% CPU) | ~3 | None | #### 1.5 Trivial fixes batch -| Fix | File | LOC | -|-----|------|-----| -| Const cookie prefix instead of `format!()` | `publisher.rs:207-210` | 2 | -| `mem::take` instead of `clone` for overlap buffer | `streaming_replacer.rs:63` | 1 | -| `eq_ignore_ascii_case` for compression detection | `streaming_processor.rs:47` | 5 | -| `Cow` for string replacements | `streaming_replacer.rs:120-125` | 5-10 | -| Remove base64 roundtrip in token computation | `http_util.rs:286-294` | 10-15 | -| Replace Handlebars with manual interpolation | `synthetic.rs:82-99` | ~20 | -| Cache `origin_host()` result per-request | `settings.rs` | 5-10 | +| Fix | File | LOC | +| ------------------------------------------------- | ------------------------------- | ----- | +| Const cookie prefix instead of `format!()` | `publisher.rs:207-210` | 2 | +| `mem::take` instead of `clone` for overlap buffer | `streaming_replacer.rs:63` | 1 | +| `eq_ignore_ascii_case` for compression detection | `streaming_processor.rs:47` | 5 | +| `Cow` for string replacements | `streaming_replacer.rs:120-125` | 5-10 | +| Remove base64 roundtrip in token computation | `http_util.rs:286-294` | 10-15 | +| Replace Handlebars with manual interpolation | `synthetic.rs:82-99` | ~20 | +| Cache `origin_host()` result per-request | `settings.rs` | 5-10 | --- @@ -285,6 +285,7 @@ The high-impact architectural change. Uses Fastly's `stream_to_client()` API to **Files**: `crates/trusted-server-core/src/publisher.rs`, `crates/trusted-server-adapter-fastly/src/main.rs` **Current flow** (fully buffered): + ``` req.send() → wait for full response → take_body() → process_response_streaming() → collects into Vec @@ -292,6 +293,7 @@ req.send() → wait for full response → take_body() ``` **New flow** (streaming): + ``` req.send() → take_body() → set response headers → stream_to_client() → returns StreamingBody (headers sent immediately) @@ -300,6 +302,7 @@ req.send() → take_body() → set response headers ``` **Key enablers**: + - `StreamingPipeline.process()` already accepts `W: Write` — `StreamingBody` implements `Write` - With Phase 1 fixes (gzip streaming + HTML rewriter streaming), the pipeline is already chunk-based - Non-text responses can use `streaming_body.append(body)` for O(1) pass-through @@ -307,6 +310,7 @@ req.send() → take_body() → set response headers **Architecture change in `main.rs`**: The publisher proxy path calls `stream_to_client()` directly instead of returning a `Response`. Other endpoints (static, auction, discovery) continue returning `Response` as before. **Error handling for streaming**: Once `stream_to_client()` is called, response headers (including status 200) are already sent. If processing fails mid-stream: + - We cannot change the status code — the client already received 200 - The `StreamingBody` will be aborted on drop (client sees incomplete response) - We should log the error server-side for debugging @@ -339,8 +343,8 @@ match pipeline.process(backend_body, &mut client_body) { } ``` -| Impact | LOC | Risk | -|--------|-----|------| +| Impact | LOC | Risk | +| -------------------------------------------------------------------------- | ------- | ----------------------------------------------- | | **High** — reduces time-to-last-byte and peak memory for all proxied pages | ~80-120 | Medium — error handling requires careful design | #### 2.2 Concurrent origin fetch + auction (future) @@ -353,8 +357,8 @@ This would overlap origin fetch time with auction execution, so the browser star **Note**: This requires significant refactoring of the auction orchestrator and HTML processor to support async injection. -| Impact | LOC | Risk | -|--------|-----|------| +| Impact | LOC | Risk | +| ----------------------------------------------------------------------- | -------- | --------------------------- | | **Very High** for auction pages — browser starts loading ~400ms earlier | ~150-200 | High — complex coordination | --- @@ -371,6 +375,7 @@ After implementing Phases 1-2: 6. If improvement is marginal, don't ship the streaming architecture (Phase 2) **Success criteria**: + - Peak memory per request reduced by 30%+ (measurable via Fastly logs) - Time-to-last-byte reduced for large HTML pages - No regression on static endpoints or auction @@ -380,15 +385,15 @@ After implementing Phases 1-2: ## Optimization Summary Table -| # | Optimization | Measured CPU | Impact | LOC | Risk | Phase | -|---|---|---|---|---|---|---| -| **1.1** | Gzip streaming fix | Part of ~76% HTML pipeline | **High** (memory) | -15/+3 | Low | 1 | -| **1.2** | HTML rewriter streaming | Part of ~76% HTML pipeline | **High** (memory) | ~30 | Medium | 1 | -| **1.3** | ~~Eliminate redundant `config` crate~~ | ~~5-8%~~ → **3.3%** | **Done** | 1-3 | Low | 1 | -| **1.4** | Reduce verbose logging | ~0.5% | Low | ~3 | None | 1 | -| **1.5** | Trivial fixes batch | <1% combined | Low | ~50 | None | 1 | -| **2.1** | `stream_to_client()` integration | N/A (architectural) | **High** (TTLB) | ~80-120 | Medium | 2 | -| **2.2** | Concurrent origin + auction | N/A (architectural) | **Very High** | ~150-200 | High | 2 (future) | +| # | Optimization | Measured CPU | Impact | LOC | Risk | Phase | +| ------- | -------------------------------------- | -------------------------- | ----------------- | -------- | ------ | ---------- | +| **1.1** | Gzip streaming fix | Part of ~76% HTML pipeline | **High** (memory) | -15/+3 | Low | 1 | +| **1.2** | HTML rewriter streaming | Part of ~76% HTML pipeline | **High** (memory) | ~30 | Medium | 1 | +| **1.3** | ~~Eliminate redundant `config` crate~~ | ~~5-8%~~ → **3.3%** | **Done** | 1-3 | Low | 1 | +| **1.4** | Reduce verbose logging | ~0.5% | Low | ~3 | None | 1 | +| **1.5** | Trivial fixes batch | <1% combined | Low | ~50 | None | 1 | +| **2.1** | `stream_to_client()` integration | N/A (architectural) | **High** (TTLB) | ~80-120 | Medium | 2 | +| **2.2** | Concurrent origin + auction | N/A (architectural) | **Very High** | ~150-200 | High | 2 (future) | --- @@ -473,13 +478,13 @@ The script builds, starts the profiling server, fires requests, stops the server ### What the Tools Measure -| Tool | What it tells you | -|---|---| -| `benchmark.sh` — TTFB analysis | 20 sequential requests — detects cold start patterns | -| `benchmark.sh` — Cold start | First vs subsequent request latency | -| `benchmark.sh` — Endpoint latency | Per-endpoint timing breakdown (DNS, connect, TTFB, total) | -| `benchmark.sh` — Load test (hey) | Throughput (req/sec), latency distribution (P50/P95/P99) | -| `profile.sh` | Per-function CPU time inside WASM — flame graph via `--profile-guest` | +| Tool | What it tells you | +| --------------------------------- | --------------------------------------------------------------------- | +| `benchmark.sh` — TTFB analysis | 20 sequential requests — detects cold start patterns | +| `benchmark.sh` — Cold start | First vs subsequent request latency | +| `benchmark.sh` — Endpoint latency | Per-endpoint timing breakdown (DNS, connect, TTFB, total) | +| `benchmark.sh` — Load test (hey) | Throughput (req/sec), latency distribution (P50/P95/P99) | +| `profile.sh` | Per-function CPU time inside WASM — flame graph via `--profile-guest` | **Use `profile.sh` first** to identify which functions are bottlenecks, then use `benchmark.sh` to measure the impact of fixes on external timing. @@ -516,6 +521,7 @@ A teammate has prepared changes to `streaming_processor.rs` that address items 1 - **HTML rewriter fix**: `HtmlRewriterAdapter` rewritten to use `lol_html::OutputSink` trait with `Rc>>` for incremental streaming **Review notes on the HTML rewriter change**: + - `lol_html::OutputSink` is a public trait (verified in lol_html 2.7.1) - The `Rc` pattern is necessary because `HtmlRewriter::new()` takes ownership of the sink, but we need to read output in `process_chunk()` - `Option` with `.take()` is correct — `end()` consumes self diff --git a/PUBLISHER_IDS_AUDIT.md b/docs/superpowers/archive/2026-03-24-publisher-ids-audit-design.md similarity index 96% rename from PUBLISHER_IDS_AUDIT.md rename to docs/superpowers/archive/2026-03-24-publisher-ids-audit-design.md index 37c5b7f3..56bea026 100644 --- a/PUBLISHER_IDS_AUDIT.md +++ b/docs/superpowers/archive/2026-03-24-publisher-ids-audit-design.md @@ -7,20 +7,24 @@ This document lists all publisher-specific IDs and configurations found in the c ### trusted-server.toml **GAM Configuration:** + - `publisher_id = "3790"` (line 14) - `server_url = "https://securepubads.g.doubleclick.net/gampad/ads"` (line 15) **Equativ Configuration:** + - `sync_url = "https://adapi-srv-eu.smartadserver.com/ac?pgid=2040327&fmtid=137675&synthetic_id={{synthetic_id}}"` (line 8) - Page ID: `2040327` - Format ID: `137675` **Test Publisher Domain:** + - `domain = "test-publisher.com"` (line 2) - `cookie_domain = ".test-publisher.com"` (line 3) - `origin_url = "https://origin.test-publisher.com"` (line 4) **KV Store Names (user-specific):** + - `counter_store = "jevans_synth_id_counter"` (line 24) - `opid_store = "jevans_synth_id_opid"` (line 25) @@ -29,6 +33,7 @@ This document lists all publisher-specific IDs and configurations found in the c ### /Users/jevans/trusted-server/crates/trusted-server-core/src/gam.rs **Permutive Segment Data (lines 295 and 486):** + ```rust .with_prmtvctx("129627,137412,138272,139095,139096,139218,141364,143196,143210,143211,143214,143217,144331,144409,144438,144444,144488,144543,144663,144679,144731,144824,144916,145933,146347,146348,146349,146350,146351,146370,146383,146391,146392,146393,146424,146995,147077,147740,148616,148627,148628,149007,150420,150663,150689,150690,150692,150752,150753,150755,150756,150757,150764,150770,150781,150862,154609,155106,155109,156204,164183,164573,165512,166017,166019,166484,166486,166487,166488,166492,166494,166495,166497,166511,167639,172203,172544,173548,176066,178053,178118,178120,178121,178133,180321,186069,199642,199691,202074,202075,202081,233782,238158,adv,bhgp,bhlp,bhgw,bhlq,bhlt,bhgx,bhgv,bhgu,bhhb,rts".to_string()) ``` @@ -38,27 +43,32 @@ This large string contains Permutive segment IDs that appear to be captured from ### /Users/jevans/trusted-server/crates/trusted-server-core/src/prebid.rs **Equativ Integration:** + - `"pageId": 2040327` (matches config) - `"formatId": 137675` (matches config) ### Test Files **Test Support Files:** + - GAM publisher ID `"3790"` in test configurations - `"test-publisher.com"` and related test domains in multiple test files ## Impact Assessment ### High Priority (Publisher-Specific) + 1. **GAM Publisher ID (3790)** - Core identifier for ad serving 2. **Permutive Segments** - Large hardcoded segment string from test traffic 3. **Equativ Page/Format IDs (2040327, 137675)** - Ad network integration ### Medium Priority (Environment-Specific) + 1. **Test Publisher Domains** - Should be configurable per deployment -2. **KV Store Names** - Currently user-specific (jevans_*) +2. **KV Store Names** - Currently user-specific (jevans\_\*) ### Low Priority (Infrastructure) + 1. **Server URLs** - Generally standard but should be configurable ## Recommendations @@ -74,4 +84,4 @@ This large string contains Permutive segment IDs that appear to be captured from - `trusted-server.toml` - Add permutive segments configuration - `crates/trusted-server-core/src/gam.rs` - Remove hardcoded segments (lines 295, 486) - `crates/trusted-server-core/src/prebid.rs` - Use configuration for Equativ IDs -- Test files - Use environment-agnostic test data \ No newline at end of file +- Test files - Use environment-agnostic test data diff --git a/docs/superpowers/specs/2026-03-11-production-readiness-report-design.md b/docs/superpowers/specs/2026-03-11-production-readiness-report-design.md new file mode 100644 index 00000000..d49fa647 --- /dev/null +++ b/docs/superpowers/specs/2026-03-11-production-readiness-report-design.md @@ -0,0 +1,609 @@ +# Production Readiness Report + +**Date:** 2026-03-04 +**Branch:** `split-prebid-deferred-bundle` +**Scope:** Correctness, security foot-guns, optimization +**Out of scope:** Test coverage gaps (tracked separately) + +## Verdict + +**Not production-ready for an internet-exposed deployment.** There are several +high-risk correctness/security foot-guns plus clear performance wins left on the +table. The codebase is well-structured and uses Rust's type system effectively, +but the issues below need resolution before a public-facing deployment. + +--- + +## Summary + +| Severity | Count | Areas | +| -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CRITICAL | 5 | Request signing coherence, admin auth, creative XSS, prototype stacking | +| HIGH | 5 | Secret logging, auction timeouts, weak secret validation, regex HTML rewriting, config error swallowing | +| MEDIUM | 16 | Host spoofing, non-constant-time comparison, cookie injection, SSRF, memory buffering, per-request CPU waste, prototype perf, SDK polling, observer leaks, prebid host trust, bid truncation | +| LOW | 14 | Cache headers, error details, MIME types, regex compilation, status codes, allocations | + +--- + +## CRITICAL + +### C-1: Request-signing store selection is inconsistent (hardcoded vs config-driven) + +Store names are hardcoded as `"jwks_store"` / `"signing_keys"` in standalone +functions but read from `settings.request_signing` in the admin endpoints. If +config and hardcoded values diverge, signing produces keys the verifier cannot +find, and key rotation writes to the wrong store. + +**Refs:** + +- [signing.rs:20](crates/common/src/request_signing/signing.rs#L20) -- `FastlyConfigStore::new("jwks_store")` hardcoded +- [signing.rs:122](crates/common/src/request_signing/signing.rs#L122) -- `FastlyConfigStore::new("jwks_store")` hardcoded +- [signing.rs:130](crates/common/src/request_signing/signing.rs#L130) -- `FastlySecretStore::new("signing_keys")` hardcoded +- [jwks.rs:63](crates/common/src/request_signing/jwks.rs#L63) -- `FastlyConfigStore::new("jwks_store")` hardcoded +- [rotation.rs:44](crates/common/src/request_signing/rotation.rs#L44) -- `FastlyConfigStore::new("jwks_store")` hardcoded, ignores `config_store_id` constructor arg +- [endpoints.rs:151](crates/common/src/request_signing/endpoints.rs#L151) -- reads `config_store_id`/`secret_store_id` from settings + +**Recommendation:** Single source of truth -- either always read store IDs from +`Settings` and thread them through, or document + assert the hardcoded names +match config. + +--- + +### C-2: Admin endpoints unprotected unless handler regex covers them + +`/admin/keys/rotate` and `/admin/keys/deactivate` are always routed. The +`enforce_basic_auth` gate only triggers for paths that match a configured +`handlers[].path` regex. The default config (`^/secure`) does not cover +`/admin/*`. An operator who doesn't add an explicit admin handler has +**publicly-accessible key rotation/deletion endpoints**. + +**Refs:** + +- [main.rs:97-98](crates/fastly/src/main.rs#L97-L98) -- admin route matching +- [auth.rs:10](crates/common/src/auth.rs#L10) -- `enforce_basic_auth` checks `handlers` list +- [settings.rs:381](crates/common/src/settings.rs#L381) -- `handlers` parsing +- [trusted-server.toml:1](trusted-server.toml#L1) -- default handler only covers `^/secure` + +**Recommendation:** Either hard-require auth for `/admin/*` paths regardless of +handler config, or validate at startup that an admin handler exists. + +--- + +### C-3: Unsanitized creative HTML injected into iframe with weakened sandbox (JS) + +Creative HTML (`bid.adm`) from the upstream bidder response is injected into an +iframe via `srcdoc` with no sanitization. The iframe sandbox includes both +`allow-scripts` and `allow-same-origin`, which together allow script inside the +iframe to remove its own sandbox attribute and gain full access to the +publisher's page context, cookies, and localStorage. + +**Refs:** + +- [request.ts:77-102](crates/js/lib/src/core/request.ts#L77-L102) -- `iframe.srcdoc = buildCreativeDocument(creativeHtml)` +- [render.ts:104-111](crates/js/lib/src/core/render.ts#L104-L111) -- sandbox with `allow-scripts` + `allow-same-origin` +- [render.ts:133-137](crates/js/lib/src/core/render.ts#L133-L137) -- `buildCreativeDocument` does raw string replace +- [auction.ts:141-172](crates/js/lib/src/core/auction.ts#L141-L172) -- `parseAuctionResponse` passes `adm` straight through + +**Recommendation:** Either (a) remove `allow-same-origin` if creatives don't +need it, (b) serve creatives from a separate origin, or (c) sanitize `adm` with +DOMPurify before injection. + +--- + +### C-4: Multiple global prototype patches stack without coordination (JS) + +Five integrations (Lockr, Permutive, DataDome, GTM, GPT) independently +monkey-patch `Element.prototype.appendChild` and `Element.prototype.insertBefore`. +Each captures the current prototype method at install time, creating a chain of +4+ wrappers. Every single `appendChild` call on the publisher page (including +text nodes, divs, analytics pixels) now executes 4+ function calls with string +checks. + +The shared guard's `reset()` only flips a boolean -- it does not restore the +original prototype methods, so SPA contexts and test runs accumulate wrappers. + +**Refs:** + +- [shared/script_guard.ts:155-191](crates/js/lib/src/shared/script_guard.ts#L155-L191) -- shared factory patches +- [gpt/script_guard.ts:432-450](crates/js/lib/src/integrations/gpt/script_guard.ts#L432-L450) -- independent GPT patch +- [shared/script_guard.ts:197-199](crates/js/lib/src/shared/script_guard.ts#L197-L199) -- `reset()` doesn't restore originals + +**Recommendation:** Use a centralized dispatcher -- single prototype patch, +register per-integration handlers. Implement proper `reset()` that restores +originals. + +--- + +### C-5: `.expect()` on regex compilation from user configuration + +Configuration-derived regex patterns use `.expect()` which will panic at runtime +if the pattern is invalid. If `handler.path` or integration config contains +invalid regex metacharacters and validation is bypassed (env override, manual +TOML edit), the service crashes on first matching request. + +**Refs:** + +- [settings.rs:255](crates/common/src/settings.rs#L255) -- `Regex::new(&self.path).expect(...)` +- [script_rewriter.rs:125](crates/common/src/integrations/nextjs/script_rewriter.rs#L125) -- `Regex::new(&pattern).expect(...)` +- [script_rewriter.rs:141](crates/common/src/integrations/nextjs/script_rewriter.rs#L141) -- `Regex::new(&pattern).expect(...)` +- [shared.rs:83](crates/common/src/integrations/nextjs/shared.rs#L83) -- `Regex::new(...).expect(...)` + +**Recommendation:** Return `Result` from these constructors; catch at startup +with a descriptive error message. + +--- + +## HIGH + +### H-1: Secrets and PII logged at INFO/DEBUG level + +The full `Settings` struct (including `proxy_secret`, `synthetic.secret_key`, +handler passwords) is logged via `Debug` format at `INFO` level on every +request. Synthetic ID generation logs client IP, user agent, and other PII. +Integration responses (full bid payloads) are logged at debug. Logger is +globally set to debug level. + +**Refs:** + +- [main.rs:42](crates/fastly/src/main.rs#L42) -- `log::info!("Settings {settings:?}")` +- [main.rs:177](crates/fastly/src/main.rs#L177) -- logger level set to debug +- [synthetic.rs:99](crates/common/src/synthetic.rs#L99) -- logs HMAC input (IP, UA) +- [synthetic.rs:112](crates/common/src/synthetic.rs#L112) -- logs synthetic ID details +- [prebid.rs:832](crates/common/src/integrations/prebid.rs#L832) -- logs full bid response +- [aps.rs:444](crates/common/src/integrations/aps.rs#L444) -- logs APS response +- [adserver_mock.rs:284](crates/common/src/integrations/adserver_mock.rs#L284) -- logs mock response + +**Recommendation:** Implement a `Redacted` wrapper for secret fields that +prints `[REDACTED]` in `Debug`/`Display`. Set production log level to `INFO` or +`WARN`. Move payload logging to `TRACE`. + +--- + +### H-2: Auction timeout config not enforced by orchestrator wait logic + +`settings.auction.timeout_ms` is passed to `AuctionContext` but the orchestrator +uses `select()` which blocks until each pending request completes or hits the +backend's `first_byte_timeout` (15s). There is no mechanism to abort remaining +requests when the auction timeout is reached. + +**Refs:** + +- [endpoints.rs:51](crates/common/src/auction/endpoints.rs#L51) -- `timeout_ms: settings.auction.timeout_ms` +- [provider.rs:54](crates/common/src/auction/provider.rs#L54) -- `fn timeout_ms(&self) -> u32` +- [orchestrator.rs:287](crates/common/src/auction/orchestrator.rs#L287) -- `while !remaining.is_empty() { select(remaining) }` +- [backend.rs:118-119](crates/common/src/backend.rs#L118-L119) -- hardcoded 15s first_byte_timeout + +**Recommendation:** Implement a deadline-based loop that drops remaining pending +requests when `timeout_ms` elapses, returning partial results. + +--- + +### H-3: Weak/inconsistent secret default validation + +Only `synthetic.secret_key` is checked against the literal `"secret-key"`. +`publisher.proxy_secret` defaults to `"change-me-proxy-secret"` and +`synthetic.secret_key` defaults to `"trusted-server"` -- neither is caught by +the single check. A deployment using defaults has predictable encryption keys. + +**Refs:** + +- [trusted-server.toml:10](trusted-server.toml#L10) -- `proxy_secret = "change-me-proxy-secret"` +- [trusted-server.toml:15](trusted-server.toml#L15) -- `secret_key = "trusted-server"` +- [settings.rs:197](crates/common/src/settings.rs#L197) -- `Settings::validate()` -- no proxy_secret check +- [settings_data.rs:37](crates/common/src/settings_data.rs#L37) -- only checks `== "secret-key"` + +**Recommendation:** Reject all known placeholder values for both secrets. +Consider minimum entropy requirements. + +--- + +### H-4: Regex-based HTML rewriting in `document.write` interception can fail-open (JS) + +The GPT guard uses a regex to match and rewrite GPT domain script URLs in +`document.write` calls. If the regex fails to match (escaped quotes, unusual +spacing, mixed quote styles), the original unproxied URL is passed through, +causing the browser to load the GPT script directly from Google's CDN instead of +through the first-party proxy. + +**Refs:** + +- [gpt/script_guard.ts:206-230](crates/js/lib/src/integrations/gpt/script_guard.ts#L206-L230) -- `rewriteHtmlString` regex + +**Recommendation:** Use DOM-based parsing (`DOMParser`) instead of regex, or +fail-closed (block unmatched URLs rather than passing through). + +--- + +### H-5: Configuration errors silently disable integrations + +When `integration_config()` returns an error (typo in TOML, wrong type), `.ok()` +converts it to `None`, making the integration appear "not configured" rather than +"misconfigured". Operators get no feedback that their config is broken. + +**Refs:** + +- [prebid.rs:211-212](crates/common/src/integrations/prebid.rs#L211-L212) -- `.ok().flatten()?` +- [nextjs/mod.rs:97-99](crates/common/src/integrations/nextjs/mod.rs#L97-L99) -- `.ok().flatten()?` +- [adserver_mock.rs:373](crates/common/src/integrations/adserver_mock.rs#L373) -- `BackendConfig::from_url(...).ok()` +- [aps.rs:521](crates/common/src/integrations/aps.rs#L521) -- `BackendConfig::from_url(...).ok()` +- [prebid.rs:950](crates/common/src/integrations/prebid.rs#L950) -- `BackendConfig::from_url(...).ok()` + +**Recommendation:** Log a warning with the error before converting to `None`, or +fail the integration registration with a clear message. + +--- + +## MEDIUM + +### M-1: X-Forwarded-Host / Forwarded header spoofing + +`RequestInfo` trusts `Forwarded`, `X-Forwarded-Host`, and `X-Forwarded-Proto` +headers without validation against trusted proxies. An attacker setting +`X-Forwarded-Host: evil.com` causes the HTML rewriter to replace all origin URLs +with `evil.com`. + +**Refs:** + +- [http_util.rs:55-75](crates/common/src/http_util.rs#L55-L75) -- `extract_request_host` trusts forwarded headers + +**Recommendation:** Strip or validate forwarded headers at the Fastly VCL layer, +or validate against a configured allowlist. + +--- + +### M-2: Non-constant-time token/password comparison + +Signature verification (`tstoken`, clear URL signatures) and basic auth use +standard `==` comparison, enabling timing side-channel attacks. + +**Refs:** + +- [proxy.rs:1054-1058](crates/common/src/proxy.rs#L1054-L1058) -- `expected != sig` +- [http_util.rs:289-291](crates/common/src/http_util.rs#L289-L291) -- `sign_clear_url(...) == token` +- [auth.rs:17-18](crates/common/src/auth.rs#L17-L18) -- `password == handler.password` + +**Recommendation:** Use `subtle::ConstantTimeEq` (already in dependency tree via +crypto crates). + +--- + +### M-3: Synthetic ID cookie missing HttpOnly flag + +The `synthetic_id` cookie is set with `Secure; SameSite=Lax` but no `HttpOnly`. +Any XSS on the publisher's page can exfiltrate this tracking identifier via +`document.cookie`. + +**Refs:** + +- [cookies.rs:67-72](crates/common/src/cookies.rs#L67-L72) -- `create_synthetic_cookie` format string + +**Recommendation:** Add `HttpOnly` if client-side JS doesn't need to read this +cookie directly (it already gets the value via the `x-synthetic-id` header). + +--- + +### M-4: No synthetic ID format validation on inbound values + +The synthetic ID from cookies or headers is accepted without format validation. +An attacker can inject arbitrary strings (very long, special characters, +newlines) which are then set as response headers, cookies, and forwarded to +third-party APIs. + +**Refs:** + +- [synthetic.rs:129-153](crates/common/src/synthetic.rs#L129-L153) -- `get_synthetic_id` accepts any string +- [publisher.rs:336](crates/common/src/publisher.rs#L336) -- set as response header +- [proxy.rs:442](crates/common/src/proxy.rs#L442) -- forwarded as query parameter + +**Recommendation:** Validate against the expected format (64 hex + dot + 6 +alphanumeric) in production code, not just tests. + +--- + +### M-5: Cookie value not sanitized in Set-Cookie construction + +`synthetic_id` is interpolated directly into the `Set-Cookie` header string +without escaping. A controlled synthetic ID containing semicolons could alter +cookie attributes (e.g., `evil; Domain=.attacker.com`). + +**Refs:** + +- [cookies.rs:67-72](crates/common/src/cookies.rs#L67-L72) -- `format!("...={}; Domain=...", synthetic_id, ...)` + +**Recommendation:** Validate/sanitize the value before interpolation, or use a +cookie builder library. + +--- + +### M-6: SSRF via first-party proxy -- no target domain allowlist + +The `/first-party/proxy` endpoint proxies to arbitrary URLs (protected only by +`tstoken` signature). `proxy_with_redirects` follows up to 4 redirects with no +domain or IP range restriction, allowing SSRF to internal services if a signed +URL redirects. + +**Refs:** + +- [proxy.rs:600-621](crates/common/src/proxy.rs#L600-L621) -- `handle_first_party_proxy` +- [proxy.rs:463-582](crates/common/src/proxy.rs#L463-L582) -- `proxy_with_redirects` follows redirects + +**Recommendation:** Validate redirect targets against an allowlist or block +private IP ranges. + +--- + +### M-7: "Streaming" processing buffers whole bodies in key paths + +The gzip+HTML path reads the entire decompressed body into memory, then the +`HtmlRewriterAdapter` accumulates it again. Processing a 1MB HTML page creates +3+ full copies in memory simultaneously. + +**Refs:** + +- [streaming_processor.rs:196](crates/common/src/streaming_processor.rs#L196) -- `decoder.read_to_end(&mut decompressed)` +- [streaming_processor.rs:398](crates/common/src/streaming_processor.rs#L398) -- `HtmlRewriterAdapter::accumulated_input` +- [publisher.rs:129](crates/common/src/publisher.rs#L129) -- `process_response_streaming` collects into `Vec` + +**Recommendation:** Feed chunks incrementally to `lol_html::HtmlRewriter` +instead of accumulating. Use the streaming `process_through_compression` path +for gzip. + +--- + +### M-8: Per-request CPU waste -- settings parse/validate and registry rebuild + +`get_settings()`, `build_orchestrator()`, and `IntegrationRegistry::new()` all +run on every request. TOML parsing, regex compilation, and router construction +are repeated for every incoming request. + +**Refs:** + +- [main.rs:35](crates/fastly/src/main.rs#L35) -- `get_settings()` per request +- [main.rs:45](crates/fastly/src/main.rs#L45) -- `build_orchestrator()` per request +- [main.rs:47](crates/fastly/src/main.rs#L47) -- `IntegrationRegistry::new()` per request +- [settings_data.rs:28-32](crates/common/src/settings_data.rs#L28-L32) -- TOML parsing + validation + +**Recommendation:** Cache parsed settings and registry in `OnceLock` or +equivalent per-instance state (Fastly Compute instances can reuse across +requests within the same isolate). + +--- + +### M-9: Prebid response rewriting trusts host/scheme from upstream response body + +`request_host` and `request_scheme` for URL rewriting are read from the Prebid +server's response JSON (`ext.trusted_server.request_host`), not from the local +request context. A compromised or misconfigured bidder can inject arbitrary +host/scheme values. + +**Refs:** + +- [prebid.rs:904](crates/common/src/integrations/prebid.rs#L904) -- reads `request_host` from response JSON +- [prebid.rs:911](crates/common/src/integrations/prebid.rs#L911) -- reads `request_scheme` from response JSON +- [prebid.rs:922](crates/common/src/integrations/prebid.rs#L922) -- passes them to `transform_prebid_response` + +**Recommendation:** Use the local request's host/scheme from `RequestInfo` +instead of the bidder's response body. + +--- + +### M-10: `concatenated_hash()` allocates full bundle just to hash it, every HTML response + +`concatenate_modules()` builds a full `String` of all JS modules (potentially +hundreds of KB) solely to hash it, then drops it. This runs on every HTML +response. Since module content is `&'static str`, the hash is constant and +should be computed once. + +**Refs:** + +- [bundle.rs:50-55](crates/js/src/bundle.rs#L50-L55) -- `concatenated_hash` allocates full bundle +- [tsjs.rs:7](crates/common/src/tsjs.rs#L7) -- called on every HTML response + +**Recommendation:** Hash modules incrementally without concatenation, and cache +the result in a `OnceLock`. + +--- + +### M-11: `UrlPatterns` allocates 5+ Strings per HTML attribute + +`rewrite_url_value()` calls 5 `format!()` methods for origin/replacement URLs on +every `href`, `src`, `action`, `srcset` attribute in the HTML. A typical page has +dozens to hundreds of these. + +**Refs:** + +- [html_processor.rs:128-177](crates/common/src/html_processor.rs#L128-L177) -- 5 methods each allocating a String + +**Recommendation:** Pre-compute these strings once and store as fields in +`UrlPatterns`. + +--- + +### M-12: Integer truncation on bid dimensions from external input + +Bid width/height from external bidder responses are cast from `u64` to `u32` via +`as u32`, silently wrapping on values > `u32::MAX`. + +**Refs:** + +- [prebid.rs:751-755](crates/common/src/integrations/prebid.rs#L751-L755) -- `as u32` truncation +- [adserver_mock.rs:235-236](crates/common/src/integrations/adserver_mock.rs#L235-L236) -- `as u32` truncation + +**Recommendation:** Use `u32::try_from()` or `.min(u32::MAX as u64) as u32`. + +--- + +### M-13: MutationObservers on full document subtree never disconnected (JS) + +Three separate modules install `MutationObserver` on `document` with +`subtree: true`. None expose cleanup APIs. In SPA contexts, observers accumulate +on each bundle re-evaluation, creating memory leaks and callback overhead. + +**Refs:** + +- [creative/click.ts:355-359](crates/js/lib/src/integrations/creative/click.ts#L355-L359) +- [creative/dynamic_src_guard.ts:160-165](crates/js/lib/src/integrations/creative/dynamic_src_guard.ts#L160-L165) +- [gpt/script_guard.ts:499-504](crates/js/lib/src/integrations/gpt/script_guard.ts#L499-L504) + +**Recommendation:** Expose `disconnect()` APIs. Consider a shared observer with +multiple handlers. Only install when needed. + +--- + +### M-14: Fixed-timeout SDK polling with no retry or event-based fallback (JS) + +Lockr and Permutive integrations poll for SDK availability with a fixed 2.5s +window (`50 attempts * 50ms`). On slow networks, the SDK loads after the window +closes, and the shim is silently skipped. + +**Refs:** + +- [lockr/index.ts:82-101](crates/js/lib/src/integrations/lockr/index.ts#L82-L101) +- [permutive/index.ts:81-100](crates/js/lib/src/integrations/permutive/index.ts#L81-L100) + +**Recommendation:** Increase timeout, use exponential backoff, or add +event-based detection (MutationObserver on script insertion). + +--- + +### M-15: `requestAds` callback fires synchronously before bids arrive (JS) + +The `requestAds` callback is invoked immediately after initiating the auction +fetch, not after bids return. Publishers expecting Prebid-style "bids ready" +semantics will find an empty bid state. + +**Refs:** + +- [core/request.ts:15-60](crates/js/lib/src/core/request.ts#L15-L60) -- callback at line 55 + +**Recommendation:** Fire callback inside `.then()` after rendering, or document +the difference from Prebid's contract. + +--- + +### M-16: `with_body_text_plain` may override `APPLICATION_JSON` Content-Type + +All JSON API responses chain `.with_content_type(APPLICATION_JSON)` then +`.with_body_text_plain(...)`. The latter likely clobbers the Content-Type to +`text/plain`. Automated clients checking Content-Type before parsing will fail. + +**Refs:** + +- [endpoints.rs:49-51](crates/common/src/request_signing/endpoints.rs#L49-L51) -- repeated across 6 endpoints + +**Recommendation:** Use `.with_body()` instead of `.with_body_text_plain()`. + +--- + +## LOW + +### L-1: Lockr regex compiled per request + +**Ref:** [lockr.rs:123-126](crates/common/src/integrations/lockr.rs#L123-L126) + +Should use `Lazy` like other integrations (datadome, nextjs). + +### L-2: `serve_static_with_etag` rehashes body already hashed for URL + +**Ref:** [http_util.rs:182-186](crates/common/src/http_util.rs#L182-L186) + +The SHA-256 hash is computed twice for the same static content. + +### L-3: Handlebars engine created per request in synthetic ID generation + +**Ref:** [synthetic.rs:84](crates/common/src/synthetic.rs#L84) + +Template engine should be initialized once. + +### L-4: ETag comparison doesn't handle multi-value `If-None-Match` + +**Ref:** [http_util.rs:188-192](crates/common/src/http_util.rs#L188-L192) + +Per RFC 7232, `If-None-Match` can contain comma-separated ETags. + +### L-5: `image/*` is not a valid Content-Type + +**Ref:** [proxy.rs:247-248](crates/common/src/proxy.rs#L247-L248) + +Wildcard MIME types are only valid in Accept headers. Use +`application/octet-stream`. + +### L-6: Proxy errors return 502 for client-caused failures + +**Ref:** [error.rs:107](crates/common/src/error.rs#L107) + +"Missing tsurl", "invalid tstoken", "expired tsexp" should be 400/403. + +### L-7: Body re-sent on 301/302 redirects + +**Ref:** [proxy.rs:503-506](crates/common/src/proxy.rs#L503-L506) + +Per HTTP spec, only 307/308 should preserve body. + +### L-8: `rewrite_attribute` allocates String even when no rewriters match + +**Ref:** [registry.rs:696](crates/common/src/integrations/registry.rs#L696) + +Use `Cow` to avoid allocation on the common no-match path. + +### L-9: `StreamingReplacer` clones overlap buffer + N+1 String allocs per chunk + +**Ref:** [streaming_replacer.rs:63](crates/common/src/streaming_replacer.rs#L63) + +Clone on empty buffer and chained `String::replace()` per replacement pattern. + +### L-10: `Compression::from_content_encoding` allocates String for case comparison + +**Ref:** [streaming_processor.rs:46-53](crates/common/src/streaming_processor.rs#L46-L53) + +Use `eq_ignore_ascii_case` instead of `.to_lowercase()`. + +### L-11: `all_module_ids()` allocates Vec on every call + +**Ref:** [bundle.rs:17-19](crates/js/src/bundle.rs#L17-L19) + +Return from a `OnceLock` or return an iterator. + +### L-12: No `X-Content-Type-Options: nosniff` on server-generated responses + +**Refs:** [http_util.rs:204](crates/common/src/http_util.rs#L204), [error.rs:18](crates/fastly/src/error.rs#L18) + +### L-13: Error responses expose internal error context to clients + +**Ref:** [error.rs:17-18](crates/fastly/src/error.rs#L17-L18) + +`user_message()` includes configuration/proxy error strings. + +### L-14: Static JS bundles cached for only 300 seconds despite cache-busting hash + +**Ref:** [http_util.rs:207-208](crates/common/src/http_util.rs#L207-L208) + +With `?v={hash}` query strings, `max-age` could be much longer (1 year). + +--- + +## Assumptions / Open Questions + +1. Assumes `handlers` are the only auth gate for admin endpoints (C-2). +2. If Fastly reuses instances, repeated logger init via `.apply()` could become + a runtime foot-gun; worth validating in the runtime model + ([main.rs:197](crates/fastly/src/main.rs#L197)). +3. The creative HTML injection (C-3) severity depends on whether upstream bidder + responses are already considered trusted. In an open RTB context they are not. +4. Per-request settings parsing (M-8) may be inherent to Fastly Compute's + per-request isolate model -- verify whether instance reuse is possible. + +--- + +## Positive Findings + +- No `unsafe` code anywhere in the codebase +- No `unwrap()` in production code (all use `expect("should ...")`) +- Proper scheme validation in proxy (`http`/`https` only) +- Internal header filtering prevents leaking `x-synthetic-id`, `x-geo-*` to third parties +- `tstoken` signature validation on first-party proxy URLs +- Insecure default secret key detection (partial -- see H-3) +- Max redirect limit (4) prevents infinite redirect loops +- Ed25519 request signing with canonical JSON payloads +- Module filename allowlisting prevents path traversal in JS serving +- Well-structured error handling with `error-stack` throughout diff --git a/AUCTION_ORCHESTRATION_FLOW.md b/docs/superpowers/specs/2026-03-19-auction-orchestration-flow-design.md similarity index 97% rename from AUCTION_ORCHESTRATION_FLOW.md rename to docs/superpowers/specs/2026-03-19-auction-orchestration-flow-design.md index 4e7dda6b..02a86d62 100644 --- a/AUCTION_ORCHESTRATION_FLOW.md +++ b/docs/superpowers/specs/2026-03-19-auction-orchestration-flow-design.md @@ -50,7 +50,7 @@ sequenceDiagram activate TS Client->>TS: POST /auction
AdRequest with adUnits[] Note right of Client: { "adUnits": [{ "code": "header-banner",
"mediaTypes": { "banner": { "sizes": [[728,90]] } } }] } - + TS->>TS: 🔧 Parse AdRequest
🔄 Transform to AuctionRequest
🆔 Generate user IDs
📊 Build context deactivate Client deactivate TS @@ -64,7 +64,7 @@ sequenceDiagram TS->>Orch: orchestrator.run_auction() Orch->>Orch: 🔍 Detect strategy
mediator? parallel_mediation : parallel_only deactivate TS - + Note over Orch: Strategy determined by config:
[auction]
mediator = "adserver_mock" → parallel_mediation
No mediator → parallel_only end @@ -74,27 +74,27 @@ sequenceDiagram activate APS activate Prebid activate Mock - + par Parallel Provider Calls Orch->>APS: POST /e/dtb/bid
APS TAM format Note right of Orch: { "pubId": "5128",
"slots": [{ "slotID": "header-banner",
"sizes": [[728,90]] }] } - + APS->>Mock: APS TAM request Mock-->>APS: APS bid response
(encoded prices, no creative) Note right of Mock: { "contextual": { "slots": [{
"slotID": "header-banner",
"amznbid": "Mi41MA==", // "2.50"
"fif": "1" }] } } - + APS-->>Orch: AuctionResponse
(APS bids) and Orch->>Prebid: POST /openrtb2/auction
OpenRTB 2.x format Note right of Orch: { "id": "request",
"imp": [{ "id": "header-banner",
"banner": { "w": 728, "h": 90 } }] } - + Prebid->>Mock: OpenRTB request Mock-->>Prebid: OpenRTB response
(clear prices, with creative) Note right of Mock: { "seatbid": [{ "seat": "prebid",
"bid": [{ "price": 2.00, "adm": "..." }] }] } - + Prebid-->>Orch: AuctionResponse
(Prebid bids) end - + Note over Orch: 📊 Collected bids from all providers
APS: encoded prices, no creative
Prebid: clear prices, with creative deactivate Mock deactivate APS @@ -108,10 +108,10 @@ sequenceDiagram activate Med Orch->>Med: POST /adserver/mediate
All bids for final selection Note right of Orch: { "id": "auction-123",
"imp": [...],
"ext": { "bidder_responses": [
{ "bidder": "amazon-aps",
"bids": [{ "encoded_price": "Mi41MA==" }] },
{ "bidder": "prebid",
"bids": [{ "price": 2.00 }] }] } } - + Med->>Med: 🔓 Decode APS encoded prices
📏 Apply floor prices
🏆 Select highest CPM per slot Note right of Med: Base64 decode: "Mi41MA==" → "2.50"
Winner: APS at $2.50 vs Prebid at $2.00 - + Med-->>Orch: OpenRTB response with winners Note right of Med: { "seatbid": [{ "seat": "amazon-aps",
"bid": [{ "price": 2.50, "impid": "header-banner" }] }] } deactivate Med @@ -121,7 +121,7 @@ sequenceDiagram Note over Client,Mock: 🏆 Direct Winner Selection Orch->>Orch: 📏 Compare clear prices only
⚠️ Skip APS (encoded prices)
🏆 Select highest CPM Note right of Orch: APS bids skipped (encoded prices)
Winner: Prebid at $2.00 (only clear price) - + Note over Orch: 📝 Results: Limited winner selection
Cannot compare encoded APS prices
Prebid wins by default end end @@ -132,10 +132,10 @@ sequenceDiagram activate TS activate Client Orch->>Orch: 🔄 Transform to OpenRTB response
🖼️ Generate iframe creatives
🔏 Rewrite creative URLs
📊 Add orchestrator metadata - + Orch-->>TS: OpenRTB BidResponse Note right of Orch: { "id": "auction-response",
"seatbid": [{ "seat": "amazon-aps",
"bid": [{ "price": 2.50,
"adm": "