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": "