Skip to main content

max / makenotwork

54.5 KB · 472 lines History Blame Raw
1 # MNW Server — Todo
2
3 **Last updated:** 2026-05-31 late evening (post Run #9 — launch-eve pass).
4
5 ## Status
6
7 All 5 axes at A- after Run #9 fixes. **0 CRITICAL open · 1 SERIOUS open (deferred) · 3 HIGH open (deferred) · 7 MED open (deferred).** Launchplan §1.5 A- bar holds. See `docs/audit_review.md` Run #9 section for full triage.
8
9 ## Run #9 — fixed this session (2026-05-31)
10
11 - **UX-CRITICAL** Signup TOCTOU 23505 → 500 + form loss. `join_wizard.rs`: catch 23505 with constraint-name routing, surface as `return_error`. Follow-up: preserve typed form fields on error swap (Phase 4).
12 - **Sec-SERIOUS** `delete_all_sessions_for_user` non-atomic JWT bump → wrapped in `pool.begin()` / `tx.commit()` (`db/sessions.rs:247`).
13 - **Sec-SERIOUS** 2FA login-email IP spoofable via bare `x-forwarded-for` → swapped to `crate::helpers::extract_client_ip` (`routes/pages/public/two_factor.rs:308`).
14 - **Pay-SERIOUS** Webhook dual-failure 503 short-circuited on Stripe retry → call `unmark_event_processed` before returning 503 (`routes/stripe/webhook/mod.rs:81`).
15
16 `cargo check --tests` clean; targeted unit tests (sessions/webhook/two_factor/join_wizard) 33/33 green. Full DB-integration suite needs astra postgres.
17
18 ## Run #9 — deferred with rationale (Phase 4)
19
20 - [ ] **Pay-SERIOUS** Subscription webhook out-of-order events resurrect `active`. Needs `created`-timestamp re-extraction from `UntypedEvent` + `WHERE last_event_at <= $created` guards across Fan+/creator-tier/synckit subscription writes. Cross-cutting; worst case is minutes-window of restored access until next webhook.
21 - [ ] **Sto-HIGH** Migration 129 dead-letter table never written (`cleanup.rs:453`). Operational visibility, not runtime; one-INSERT fix.
22 - [ ] **Perf-HIGH** Per-request `reqwest::Client::new()` in 5 hot paths (dashboard/main, public/landing, api/internal/cli_features, api/domains, auth.rs). Hoist to OnceLock or AppState pooled client.
23 - [ ] **Perf-HIGH** Unbounded `tokio::spawn` in `cleanup.rs:215-220` `spawn_expired_account_cleanups`. Lift existing `CLEANUP_PARALLELISM=4` JoinSet pattern from `cleanup_sandbox_accounts` 100 lines above.
24 - [ ] **Pay-MED** `pricing.rs::parse_dollars_to_cents` strips European decimal comma; `1,23` → 12300¢.
25 - [ ] **Pay-MED** SyncKit app-sub checkout silently defaults `storage_limit_bytes` to 0 if metadata missing.
26 - [ ] **Pay-MED** Guest checkout email sentinel `"unknown@guest"` collision risk.
27 - [ ] **Sto-MED** `is_s3_key_live` 7 EXISTS subqueries on unindexed s3_key columns — sequential scans per retry. Add partial indexes WHERE NOT NULL.
28 - [ ] **Sto-MED** `is_s3_key_live` LIKE suffix `'%' || s3_key` false-positives on neighboring keys → S3 object leaks. Anchor with `/`.
29 - [ ] **UX-MED** `purchase.html:145` `?return_to=` dead-wired; login handler always redirects `/dashboard`.
30 - [ ] **UX-MED** Admin user filter buttons (`admin-users.html:35-44`) use `class="primary"` instead of `btn-primary` — renders unstyled.
31
32 ## Run #9 — LOW/NOTE (carry forward)
33
34 - [ ] **UX-LOW** Pagination links in `git/issues.html:72,76` don't URL-encode `search` param.
35 - [ ] **UX-LOW** 5 sites use `.render().unwrap_or_default()` on Askama templates — blank UI on render failure, no log line.
36 - [ ] **UX-LOW** `slugify` (`formatting.rs:85`) produces `"post"` for any non-ASCII title.
37 - [ ] **Sec-MINOR** `csrf.rs:176-185` `validate_token_consuming` doesn't actually consume — rename or rotate.
38 - [ ] **Sec-MINOR** `routes/oauth.rs:101-111` `is_localhost_redirect` allows any port regardless of registered URI.
39 - [ ] **Sec-MINOR** `scanning/archive.rs:124` path-traversal check misses lone `..` segment (no trailing `/`).
40 - [ ] **Perf-LOW** `db/page_views.rs` `pending` HashMap has no max-cardinality cap.
41 - [ ] **Perf-LOW** `build_runner.rs:441` artifact tmpfile leaks if process crashes between SCP and `remove_file`.
42
43 Live state: working tree has 104+ Run #8 files plus 4 Run #9 files (`join_wizard.rs`, `sessions.rs`, `two_factor.rs`, `webhook/mod.rs`, `docs/audit_review.md`, `todo.md`).
44
45 ## Open before launch (Monday 2026-06-01)
46
47 ### Platform-as-product audits (skill-driven, code-review scope; fresh context recommended)
48 - [ ] `/creator-fuzz` — would a working creator trust this with their livelihood?
49 - [ ] `/use-fuzz` — discoverability, learnability, first-five-minutes
50 - [ ] `/business-fuzz` — pricing copy, fee surfacing, refund-policy wording vs actual platform behaviour
51
52 ### Per-project hygiene (manual, my call when ready)
53 - [ ] README first-screen audit — what is this / who is it for / where to get it / what does it cost. No headliner paragraphs.
54 - [ ] `Cargo.toml` version bump for the launch deploy (pick the number; I do the edit if needed)
55 - [ ] CHANGELOG entry for the launch version
56
57 ### Monday browser/prod testing (saved for Monday per current direction)
58 - [ ] §1.1 Walk every public page: footer present, OG/Twitter meta render correctly in Facebook + Twitter debuggers, error pages render via forced 404/403/500
59 - [ ] §1.2 First-run creator flow end-to-end in production: signup → Stripe Connect → first item upload
60 - [ ] §1.3 Each seeded creator's `/{handle}` page renders without empty sections; sample item per medium (audio/video/text/download) reachable from `/discover`
61 - [ ] §1.4 Production deploy of post-fuzz build + version recorded via `record_deploy`; scheduled jobs running on prod (cleanup, scan jobs retention, build reaper, broadcast fan-out); Stripe webhook reachable from dashboard ping; backup snapshot taken pre-launch + restoration path documented in `_private/docs/mnw/server-docs/`; `/health` green
62 - [ ] §5 launch-day sequence: final deploy, smoke-test logged-out from non-dev machine, update bios/link-in-bio/handles, confirm `maxj.phd` resolves, tag launch commit (`git tag launch-2026-06-01`)
63
64 ## Open question for the user (action before Monday)
65
66 - [ ] **Confirm all role-based email addresses route to real mailboxes**: `info@`, `security@`, `dmca@`, `privacy@`, `dpo@`, `legal@`, `billing@`, `policy@`, `reports@`, `community@`, `appeals@`, `press@`, `noreply@`. Legal pages (terms, privacy, copyright, appeals) and several role-routed flows reference them. If any are aspirational, that's a launch risk for the legal pages and an inbound-mail blackhole. Verify with Postmark/forwarding setup.
67
68 ## Deferred with rationale (no action; documented)
69
70 - [ ] `build_runner.rs:151` serial-target loop. LOW; builds run rarely; refactor touches denominator + error aggregation + log order. Post-launch.
71 - [ ] `scheduler/mod.rs:92-279` advisory-lock per-tier granularity. Multi-replica concern; defer until multi-replica is real.
72 - [ ] Drop unused `completion_effects` table (migration cleanup, schema-only).
73 - [ ] Templatize founder + standard annual prices in `tiers.md` and `pricing.md` (e.g. `$86/yr`, `$130/yr`, `$194/yr`, `$324/yr`; standard `$173/$259/$389/$648`). docengine substitutions don't support arithmetic; would require adding derived `tiers.founding.basic_annual` etc keys in `shared/docengine/src/assumptions.rs`. Not blocking.
74 - [ ] `_head_assets.html` apple-touch-icon + manifest link wiring. `static/manifest.json` exists but the `<link rel="manifest">` was reverted; bring back if/when desired.
75 - [ ] Migrate footer's `What's new` and `Shortcuts` `<a href="#" onclick="...">` to `data-*` attributes following the `data-copy-link` pattern. UX MED, not blocking.
76
77 ## What's done this session (compact summary, full details below)
78
79 - **Ultra Fuzz Run #8** — all 5 axes A-. SERIOUS webhook unbounded spawn closed via new `src/background.rs` (bounded mpsc + semaphore-bounded concurrent execution). `spawn_email!` macro migrated; 17 callers + 5 manual webhook spawns + 5 same-disease per-request email spawns now route through bg queue. Run #8 5 new MEDs all closed (cart `min_price_cents`, cart-all chain-break, item-wizard `pricing_model`, inline-JS templates, cart free-claim N+1). Previously-deferred Payments H2 `claim_free_project` race closed.
80 - **7-wave backlog sweep** — 24 of 26 carried items across auth/security/scanning/db/storage/UX/perf/payments. New schema migration `133_items_duration_seconds_nonnegative.sql`. New `commit_rescan` helper extends chronic-disease seal to admin paths. Two LOW items deferred above.
81 - **4 cross-cutting sweeps**`info@makenot.work` email pin (8 files), localhost/TODO/emoji/secret scans all clean.
82 - **§1.1 public-surface code work** — OG + Twitter card meta in `base.html` (per-page overridable blocks), `static/manifest.json` created with brand colours, `error.html` drops broken back button + adds contact link, `Contact` link added to footer (mailto:info@), new `routes/pages/public/sitemap.rs` (with in-memory 10-min cache + LIKE-wildcard escape from the security review).
83 - **Doc-fuzz**`content-scanning.md` restructured (Malware checks + Authenticity checks sections, added URLhaus/MetaDefender/signing layers), `policy.html` See-also block linking 6 legal pages, `tiers.md` prose prices templatized via `{{ tiers.standard.* | int }}`.
84 - **Exorcise** — 9 AI-tell removals across compare.md, content-scanning.md, appeals.md, faq.md.
85 - **Nitpick** — 2 polish edits (dead `let _ = scan_status` removed, unused tuple-name destructure tidied).
86 - **Security review** — 2 MEDs fixed inline: sitemap.xml in-memory cache to absorb crawler/attacker hammering; LIKE-wildcard escape on `is_s3_key_live` to prevent `_` in s3_keys from false-positive matching.
87
88 ---
89
90 ## Ultra Fuzz 2026-05-31 (Run #8 — final re-grade)
91
92 ### Above-MED items to address before launch (or defer with rationale)
93
94 - [x] **Perf SERIOUS — Webhook hot-path unbounded `tokio::spawn`.** Fixed 2026-05-31. New `src/background.rs` with `BackgroundTx` + bounded mpsc (1024) + semaphore-bounded concurrency (8 workers vs 25 pool conns). `spawn_email!` macro refactored to use the bg queue (covers 17 callers). 5 manual webhook spawns migrated (`checkout_helpers.rs:58, 96, 124, 290` + `checkout.rs:618`). Same-disease per-request email spawns also migrated: postmark issue replies (×2), guest-claim email, join-wizard signup (×2). `cargo test --lib` 1654 / 0. Deferred (different shapes): import pipeline (long-running), MT community create (HTTP not pool), departure/status broadcast (broadcast-class), idempotency store (trivial).
95
96 ### New MED-tier findings (all closed 2026-05-31)
97
98 - [x] **Payments MED — Cart `min_price_cents` bypass.** Fixed. Both cart paths (`process_seller_checkout` and `create_cart_checkout`) now check `pc.min_price_cents` for non-platform Discount codes before applying. Skips the ineligible item (others may still qualify) rather than rejecting the whole cart — matches the existing scope-skip pattern.
99 - [x] **Payments MED — Cart-all chain-break on all-free first seller.** Fixed. `process_seller_checkout` signature changed `Result<String>``Result<Option<String>>`; all-free path now returns `Ok(None)` instead of `Err(BadRequest)`. New `drain_to_paid` helper loops through the queued sellers until a paid one is reached (returns URL) or queue exhausted (returns `Ok(None)` → library redirect). Both callers (`create_cart_checkout_all` and `checkout_success`) updated.
100 - [x] **UX MED — Item wizard `pricing_model` silent fallback.** Fixed. `save_pricing` now rejects missing pricing_model with `AppError::validation("Select a pricing model")` and rejects unknown values with `format!("Unknown pricing model: {other}")`. Same shape as project wizard Run #6 fix.
101 - [x] **UX MED — Inline-JS template duplication.** Fixed. Added delegated `data-copy-link` handler to `static/mnw.js` with proper `.catch()` (falls back to `window.prompt` in non-secure contexts). 8 templates migrated from `onclick="navigator.clipboard.writeText(...).then(...)"` to `<a href="..." data-copy-link>Copy link</a>` (audio_player, blog_post, collection, item, project, text_reader, user, video_player). `href` is the real URL so middle-click / no-JS / share menus still work. Cache-bust bumped to `v=0531`.
102 - [x] **Perf MED — Cart free-claim N+1.** Fixed. Extended `CartItem` with `enable_license_keys` + `default_max_activations` (both cart queries pull them through). Three free-claim loops (single-seller paid path, discount-zeroed promo path, chain-flow path) drop the per-item `get_item_by_id` (saves N roundtrips) and replace per-item `remove_from_cart` DELETE with a single bulk `remove_from_cart_bulk(..., ANY($2))` at the end of each loop. Per-item tx for `claim_free_item` stays (per-item claim-vs-already-purchased return value). Roundtrips per free item: was ~5-7 → now ~3-4; bulk delete = +1 roundtrip total per loop (was N).
103
104 All 5 MEDs landed. `cargo test --lib` 1654 / 0.
105
106 ### Verified closed this run
107
108 - [x] **Storage H1**`confirm_upload` silent zero-rows + side-effects-already-fired (uploads.rs:295-337). Three-arm match, zero-rows arm rolls back storage + enqueue_s3_orphan.
109 - [x] **Storage S1**`media_confirm` three-write atomicity (media.rs:241-293). Single tx wraps storage credit + pending_uploads clear + media_files INSERT.
110 - [x] DB helpers genericized to `impl PgExecutor<'e>` — all 12 callers (including `synckit/blobs.rs:157`) verified backwards-compatible.
111
112 ### Storage A- standing — remaining MED/LOW (Phase 4 polish or defer)
113 Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A- blockers.
114
115 ---
116
117 ## Audit backlog sweep 2026-05-31 (post-Run #8, 7 waves)
118
119 Sorted by file locality and difficulty. Tests: 1655 / 0 throughout.
120
121 ### Wave 1 — auth/security cluster (8 tiny)
122 - [x] `synckit_auth.rs:147` `<``<=` closes 1-second JWT-revocation collision window.
123 - [x] `routes/auth.rs:128, 137` malformed-email + invalid-username branches now run DUMMY_HASH equalizer — closes timing oracle.
124 - [x] `routes/auth.rs:331-336` `validate_username` length switched from `len()` bytes to `chars().count()` — multi-byte usernames treated correctly.
125 - [x] `git_ssh.rs:162` `parse_repo_path` rejects lone-dot segments.
126 - [x] `routes/oauth.rs:206` `validate_token``validate_token_consuming` (sealed witness type).
127 - [x] `routes/oauth.rs:213-222` OAuth `state` ≤ 1024 bytes + `code_challenge` ≤ 44 chars.
128 - [x] `helpers.rs::ip_advisory_lock_key` `DefaultHasher` → SHA-256 (stable across Rust versions).
129 - [x] `helpers.rs::extract_client_ip` one-shot WARN after 100 cumulative missing `cf-connecting-ip` requests.
130
131 ### Wave 2 — scanning (3)
132 - [x] `scanning/clamav.rs::ping` + `ScanPipeline::assert_live` at startup; refuses to boot if scanning configured but no AV layer live.
133 - [x] `scanning/clamav.rs` 16 KB INSTREAM truncation → `LayerVerdict::Fail` (was Error → FailOpen → Pass).
134 - [x] `scanning/worker.rs:251` inline media UPDATE swapped to `db::scanning::update_media_file_scan_status` helper.
135
136 ### Wave 3 — DB layer polish (4)
137 - [x] `db/pending_uploads.rs::remove_pending_upload` signature now requires `user_id`; 12 callers updated.
138 - [x] `db/pending_s3_deletions.rs::is_s3_key_live` now covers `projects.cover_image_url` and `items.cover_image_url` via `LIKE %s3_key`.
139 - [x] `db/projects.rs::update_project_image_url` returns `Result<bool>`; `images.rs::project_image_confirm` three-arm match fires rollback + orphan-queue on `Ok(false)`.
140 - [x] `db/items/media.rs::update_item_cover` same shape; same caller treatment in `images.rs::item_image_confirm`.
141
142 ### Wave 4 — storage handlers + admin rescan seal + downloads (5)
143 - [x] **Migration 133** `items_duration_seconds_nonnegative.sql` CHECK on `duration_seconds` + `video_duration_seconds`.
144 - [x] `routes/storage/downloads.rs:120` defensive clamp: `duration.max(0) as u64 → saturating_mul(2) → clamp(3600, 86_400)`.
145 - [x] `routes/storage/mod.rs::commit_rescan` new sibling to `commit_upload` for admin-rescan paths.
146 - [x] `routes/admin/uploads.rs::rescan_{version,item}_inner` migrated to `commit_rescan`; chronic-disease seal now covers admin paths.
147 - [x] `routes/pages/dashboard/wizards/item/save.rs:95` wizard `update_item_cover_image_url` call dropped (confirm authoritative, hidden-field desync risk closed).
148 - [x] `routes/storage/mod.rs` `enqueue_s3_orphan` doc rewritten to match reality (post-credit failures only).
149
150 ### Wave 5 — UX polish (2)
151 - [x] `pricing.rs::parse_dollars_to_cents` strips `$`, `,`, whitespace; new `strips_clipboard_decoration` test.
152 - [x] `routes/admin/users.rs:37, 77` page `clamp(1, 1_000_000_000)` to prevent OFFSET overflow → sqlx 500.
153
154 ### Wave 6 — Performance (3 of 5; 2 deferred)
155 - [x] `metrics::idempotency_middleware` in-memory negative cache (OnceLock<DashMap>, 60s TTL, periodic GC). Skips per-POST `get_cached_response` SELECT for keys recently confirmed not-cached.
156 - [x] `monitor.rs` `record_storage_fill_stats` gated by 5-min TTL — 60× reduction at 10k+ creators.
157 - [x] `scheduler/cleanup.rs::cleanup_sandbox_accounts` serial loop → JoinSet `CLEANUP_PARALLELISM=4`.
158 - [ ] **DEFERRED** `build_runner.rs:151` serial-target loop. LOW; refactor touches denominator + error agg + log order. Post-launch.
159 - [ ] **DEFERRED** `scheduler/mod.rs:92-279` advisory-lock granularity. Multi-replica concern; defer until multi-replica is real.
160
161 ### Wave 7 — Payments LOW (2)
162 - [x] `routes/stripe/webhook/mod.rs:73-87` `insert_failed_event` failure → 503 (Stripe redelivers) instead of dropping event with 200.
163 - [x] `routes/stripe/checkout/cart.rs:73-82, 535-543` `remove_from_cart` already-owned cleanup now logs WARN on Err.
164
165 ---
166
167 ## Storage code-fuzz 2026-05-31 (post-Run #7)
168
169 Targeted Storage-axis fuzz to verify A- before triggering full Run #8.
170
171 ### Above-MED fixes that landed
172 - [x] **Storage HIGH — `confirm_upload` silent zero-rows + side-effects-already-fired.** `routes/storage/uploads.rs:295-336`. Three-arm match on UPDATE result; zero-rows case rolls storage back, routes new S3 key through `enqueue_s3_orphan`, returns BadRequest. Same shape as Run #7 HIGH-2, one step further along the same handler family.
173 - [x] **Storage SERIOUS — `media_confirm` three-write atomicity (Run #5 plan #12 reopened).** `routes/storage/media.rs:235-294`. Three writes (storage credit + pending_uploads clear + media_files INSERT) now in a single tx; tx drop rolls all three back on interruption. Only S3 object needs explicit cleanup. 23505 duplicate-filename detection moved outside the tx — same SQLSTATE check, runs after rollback.
174 - [x] DB-layer support: `creator_tiers::try_increment_storage_on(&mut PgConnection)` new tx-friendly variant; `pending_uploads::remove_pending_upload` and `media_files::create` signatures genericized to `impl PgExecutor<'e>` (backwards compatible — all existing `&PgPool` call sites still compile).
175
176 ### Remaining MED/LOW (below A- bar; defer or Phase 4 polish)
177 - [ ] Storage MED — `update_project_image_url` / `update_item_cover` ignore `rows_affected()`. Same shape as H1 but only follow-on side-effect is `bump_cache_generation`, so blast radius is small.
178 - [ ] Storage MED — `downloads.rs:120` `((duration as u64) * 2).max(3600)` with no DB CHECK on `duration_seconds`. Add `CHECK (duration_seconds >= 0)` migration + cap in code (`duration.max(0).saturating_mul(2).clamp(3600, 86400)`).
179 - [ ] Storage MED — Admin rescan (`routes/admin/uploads.rs:347, 390`) bypasses `commit_upload` seal via direct `db::scan_jobs::enqueue`. Demote to `pub(crate)` and expose `commit_rescan(target, ...)`.
180 - [ ] Storage MED — `enqueue_s3_orphan` single-policy doc overstates discipline; either tighten doc or migrate remaining direct `delete_object` cleanup sites.
181 - [ ] Storage MED — `is_s3_key_live` doesn't enumerate project image URLs (no current bug; surface fragile).
182 - [ ] Storage LOW — `scanning/worker.rs:251` inline UPDATE bypasses `db::scanning::update_media_file_scan_status` helper.
183 - [ ] Storage LOW — wizard `save.rs:95` updates only `cover_image_url` (not s3_key/size).
184 - [ ] Storage LOW — `pending_uploads::remove_pending_upload` deletes by s3_key alone (signature broader than needed).
185
186 ---
187
188 ## Ultra Fuzz 2026-05-31 (Runs #6, #7 + S1)
189
190 ### Structural / chronic-disease fixes that landed
191 - [x] `routes/storage/mod.rs::commit_upload(target, ...)` + `CommitTarget` enum; `enqueue_scan_for` demoted to module-private. All 7 confirm handlers (uploads, versions, project_image, item_image, media, internal/uploads, content_insertions) converted.
192 - [x] `crate::pricing::parse_dollars_to_cents` + `validate_dollars_f64` shared helpers; 5 callsites converted (item save, project wizard ×2, bulk price, projects API).
193 - [x] Dead `completion_effects` outbox deleted (`db/completion_effects.rs`, `scheduler/completion_effects.rs`, `routes/stripe/webhook/effects.rs` were orphaned files, no module declarations). Migrations 124/125 left in place — empty table, harmless. Drop-table migration is a future cleanup.
194
195 ### Bug-level fixes that landed
196 - [x] Storage CRIT Run #6 — `enqueue_s3_orphan` wired at `uploads.rs:325` post-commit old-key delete.
197 - [x] Storage HIGH Run #6 — `images.rs::project_image_confirm` idempotency check added.
198 - [x] Storage HIGH Run #6 — `images.rs::item_image_confirm` scan-ordering fixed via commit_upload.
199 - [x] Storage HIGH Run #7 — 4 idempotent-early-return sites now call `remove_pending_upload` before returning (uploads.rs, versions.rs, images.rs project + item).
200 - [x] Storage HIGH Run #7 — `update_project_image_url` + `update_item_cover` failures now refund storage + queue new key for orphan deletion.
201 - [x] Storage MED Run #6 — `media_delete` enqueue moved after `tx.commit()`.
202 - [x] UX HIGH Run #6 — `routes/api/projects.rs` float-parse via `validate_dollars_f64`.
203 - [x] UX HIGH Run #6 — project wizard tier-row loop bubbles errors instead of silently `continue`-ing.
204 - [x] UX HIGH Run #7 — `pricing_model` silent fallback to Free fixed; missing/malformed now rejects.
205 - [x] Payments S Run #6 — promo `try_increment_use_count` moved after Stripe-readiness checks in item.rs, cart.rs::create_cart_checkout, and cart.rs::process_seller_checkout.
206 - [x] Payments S Run #6 — `process_seller_checkout` uses bulk `purchased_subset`.
207 - [x] Payments H Run #6 — `project_members::add_project_member` + `update_member_split` reject split_percent outside [0,100]; upsert subtracts existing row before cap check.
208 - [x] Payments H Run #6 — `CodePurpose::Discount` with NULL discount_type/value rejected at validation (item.rs, cart.rs:184, cart.rs::process_seller_checkout — third copy fixed in Run #7).
209 - [x] Payments H Run #6 — free PWYW project checkout clears contact revocation when share_contact=true.
210 - [x] Payments SERIOUS Run #7 — cart 23505 swallow → buyer charged for unfulfilled items. New `db::transactions::pending_subset` bulk pre-check; both cart paths pre-check before Stripe session; remaining 23505 catch is now hard-error (release promo + abort).
211
212 ### Deferred (with rationale)
213 - [x] **Payments H2 (Run #7) — `claim_free_project` race.** Fixed 2026-05-31. `db::transactions::claim_free_project` now returns `Result<bool>` (mirrors `claim_free_item`). Caller in `routes/stripe/checkout/project.rs` gates `clear_contact_revocation` on the `claimed` winner — the loser of a concurrent-claim race no longer fires the side-effect. Same shape ready for any future side-effects added below the claim (sale-notification email, split recording, etc).
214 - [ ] Drop unused `completion_effects` table — schema-only cleanup; harmless empty table.
215
216 ### Notes on remaining MED/LOW (per Run #7 axis reports)
217 - Storage MED — admin rescan handlers (`routes/admin/uploads.rs:347, 390`) still call `enqueue_scan_for` indirectly via lower-level primitives; functional today but bypasses the chronic-disease seal.
218 - Storage MED — `update_item_cover` / `update_project_image_url` don't check `rows_affected()`; an ownership-filter mismatch returns Ok(0 rows) silently.
219 - Storage MED — worker inline media UPDATE at `scanning/worker.rs:251` should use the new `db::scanning::update_media_file_scan_status` helper.
220 - Storage LOW — internal CLI confirm drops returned `FileScanStatus` (no `pending_review` surfacing).
221 - Storage LOW — `main.rs:334` comment references now-private `enqueue_scan_for`.
222 - UX MED — `parse_dollars_to_cents` rejects `"$5"` and `"1,000"` literally; could strip `$`/`,` for clipboard-paste UX.
223 - UX MED — project wizard skips `validate_tier_price` ($1–$10k); API path enforces it.
224 - UX LOW — `BundleItemIds.filter_map` silently drops malformed UUIDs.
225 - Payments M1 — `compute_splits` should `.max(0)` per-member for defense vs legacy negative `split_percent` rows.
226 - Payments NIT — extract `require_stripe_ready` helper; six near-identical 5-line blocks across checkout files.
227
228 ---
229
230 ## Ultra Fuzz 2026-05-30 (Run #5)
231
232 ## Ultra Fuzz 2026-05-30 (Run #5)
233
234 Full report: `docs/audit_review.md`. 3 CRITICAL, 14 HIGH/SERIOUS. Two-axis regressions (Payments B, Storage B-) are coverage expansion into previously-unaudited paths plus one chronic recurrence; Security improved to A-; all 27 Run #4 plan items verified closed.
235
236 ### Phase 1 — CRITICAL (fix today)
237
238 - [ ] **Storage CRIT — `uploads.rs` file-type gate ordering**`routes/storage/uploads.rs:204-237`. Move the match-arm rejection of `Download`/`Insertion`/`MediaImage`/`MediaVideo` BEFORE `enqueue_scan_for` and `update_item_scan_status`. Then make `enqueue_scan_for` + `update_*_scan_status` `pub(crate)` and expose a `commit_upload(file_type, item_id, s3_key)` higher-level op used by all three handlers (uploads / versions / images). Closes Phase 5 chronic invariant-in-prose finding.
239 - [ ] **UX CRIT — Field-aware validation reaches the UI**`error.rs:216-264` + `templates/error.html`. Either add `fields: Vec<(String, String)>` to `ErrorTemplate` + per-input markup in templates, OR delete the `validation_fields*` API and migrate callers to `validation(summary)`. Audit `validation_fields` callsites and pick a path.
240 - [ ] **Perf CRIT — `build_runner.rs` partial-failure denominator**`build_runner.rs:175-180`. Track `failed_count`; report `succeeded/(succeeded+failed)`. Add a test with 3 targets / 2 failures asserting "1/3".
241
242 ### Phase 2 — SERIOUS / HIGH (fix this weekend)
243
244 - [ ] **Payments SERIOUS — NULL `item_id` refund decode bomb**`db/transactions.rs:699-716`. Return `Vec<(TransactionId, Option<ItemId>)>`; skip `decrement_sales_count`/`revoke_keys_by_transaction` when None. Fixture test against a project-level transaction.
245 - [ ] **Payments SERIOUS — `compute_splits` over-credit on members > 100%**`routes/stripe/webhook/checkout_helpers.rs:240-269`. Reject `total_split_pct > 100` at the project_members write site (DB CHECK + validation). Defensively scale or clamp each split. Add test at 60%+60%.
246 - [ ] **Payments SERIOUS — Tip `project_id` not validated vs recipient**`routes/stripe/checkout/tips.rs:104-106`. After form accept, assert `project.user_id == recipient_id`; 400 otherwise.
247 - [ ] **Payments SERIOUS — Cart bypasses item `listed` gate**`db/cart.rs:94-123` + `get_cart_items` + `get_cart_items_for_seller`. Add `AND i.listed = true` to all three. Add per-seller checkout path check. Regression test: toggle unlisted item into cart → rejection.
248 - [ ] **Payments SERIOUS — Unknown subscription status retry storm**`routes/stripe/webhook/subscriptions.rs:117-121`. Replace `?` with a match: known statuses dispatch; unknown statuses `tracing::warn!` and return 200 OK so Stripe stops retrying.
249 - [ ] **Payments SERIOUS — `is_full_refund` zero-amount**`payments/webhooks.rs:294-308`. Predicate becomes `amount > 0 && amount_refunded >= amount`. Invert the test at line 517-525.
250 - [ ] **Storage HIGH — `versions.rs` enqueue-before-idempotency**`routes/storage/versions.rs:159-174`. Move `version.s3_key == req.s3_key` idempotency check before `enqueue_scan_for`. Apply Phase 1 `commit_upload` helper.
251 - [ ] **Storage HIGH — `project_image_confirm` probe-failure + no rollback**`routes/storage/images.rs:179-208`. On `Err`/`Ok(None)` from `s3.object_size`, fall back to recorded size. Move `enqueue_deletions` AFTER `update_project_image_url` success, or wrap in a tx.
252 - [ ] **Storage HIGH — `media_confirm` non-atomic three-write**`routes/storage/media.rs:236-293`. Wrap `try_increment_storage``remove_pending_upload``media_files::create` in a transaction. Refund storage credit on any failure.
253 - [ ] **UX HIGH — Negative/zero prices via `PriceCents::from_db`**`routes/pages/dashboard/wizards/item/save.rs:183-185, 214-227`. Use `PriceCents::new(price_cents)?` unconditionally; drop `> 0` guard. Add `min <= suggested` check on PWYW.
254 - [ ] **UX HIGH — f64 price parsing accepts NaN/saturates** — same file + `routes/api/items/bulk.rs:136-139` + `routes/pages/dashboard/wizards/project.rs:264-298`. Parse as decimal cents (`rust_decimal::Decimal::from_str_exact`); reject NaN/Inf/out-of-range before cast.
255 - [ ] **UX HIGH — Username live-check fails open on DB error**`routes/auth.rs:356-361`. Propagate error or treat as "unavailable, try again".
256 - [ ] **Perf HIGH — Cart checkout 80 sequential roundtrips**`routes/stripe/checkout/cart.rs:68-248`. Bulk-load `has_purchased_item` with `WHERE item_id = ANY($1)`. Batch `get_item_by_id`. Claim free items in one tx with batched inserts. Target ≤ 5 roundtrips for any cart size.
257 - [ ] **Perf HIGH — `record_view` unbounded spawn per request**`db/page_views.rs:18-32`. Replace per-request spawn with `mpsc` channel + single background drainer flushing every 250ms via bulk UPSERT.
258 - [ ] **Perf HIGH — `check_sales_count_drift` full-table aggregate**`scheduler/integrity.rs:53-73`. Add `WHERE i.sales_count > 0 OR EXISTS(SELECT 1 FROM transactions WHERE item_id = i.id LIMIT 1)` short-term; long-term trigger-maintained counts.
259
260 ### Phase 3 — MED (fix before Run #6 if cheap)
261
262 - [ ] Storage: advisory-lock leak in `check_sandbox_cap` (`db/mod.rs:92-128`) → `pg_advisory_xact_lock` or RAII guard.
263 - [ ] Storage: `is_s3_key_live` missing tables (`db/pending_s3_deletions.rs:67-82`).
264 - [ ] Storage: `delete_version` owner SELECT outside tx + post-commit S3 enqueue (`db/versions.rs:267-315`).
265 - [ ] Security: ClamAV `FailOpen` startup assertion (`scanning/clamav.rs:19` + `scanning/mod.rs:151-164`) — refuse boot if scan configured but no AV layer live.
266 - [ ] Security: `helpers.rs:44-50` `DefaultHasher` → stable hasher (sha2 first 8 bytes or `xxh3` constant seed).
267 - [ ] Security: OAuth `state` size cap (`routes/oauth.rs:379-386`) — reject `> 1024`; cap `code_challenge` at 44 chars.
268 - [ ] Security: `extract_client_ip` non-Cloudflare fallback warning (`helpers.rs:33-40`).
269 - [ ] UX: pagination offset overflow (`routes/pages/public/discover.rs:85-87`, `routes/admin/users.rs:37-39`).
270 - [ ] UX: forms silently render without `_csrf` when handler forgets to populate token — make `csrf_token` non-optional in form-bearing templates.
271 - [ ] UX: `validate_username` byte-length vs `chars().count()` (`routes/auth.rs:322`).
272 - [ ] Perf: scheduler advisory-lock connection pinned across S3 (`scheduler/mod.rs:92-279`) → dedicated `max_connections(1)` pool.
273 - [ ] Perf: cleanup S3 deletes serialized inside scheduler tick (`scheduler/cleanup.rs:77-100`) → `for_each_concurrent(8, ...)`.
274
275 ### Phase 4 — Polish (after Run #6 confirms ≥ A-)
276
277 - [ ] Payments: `has_active_subscription_to_item` period-end clause mirroring (`db/subscriptions.rs:464-470`).
278 - [ ] Payments: `get_active_creator_tier` + `sync_user_creator_tier` period-end defense (`db/creator_tiers.rs:91-103, 181-194`).
279 - [ ] Payments: `release_use_count` race messaging (`db/promo_codes.rs:184-200`).
280 - [ ] Payments: License key `activation_count` recount on revoke (`db/license_keys.rs:343-382`).
281 - [ ] Payments: Subscription minimum-charge check (`payments/checkout.rs:283-317`).
282 - [ ] Payments: Webhook v1/v2 unmark-on-failure parity (`routes/stripe/webhook/mod.rs:48-86`).
283 - [ ] Storage: `media_files.list_folders` scan filter (`db/media_files.rs:73-82`).
284 - [ ] Storage: `pending_uploads.record_pending_upload` silent user-mismatch (`db/pending_uploads.rs:23-33`).
285 - [ ] Storage: `append_log_bounded` non-atomic size cap (`build_runner.rs:516-534`).
286 - [ ] Storage: `downloads.rs:119-122` presigned-URL expiry — cap `duration_seconds` + DB CHECK ≥ 0.
287 - [ ] Security: `validate_token_consuming` for OAuth POST (`routes/oauth.rs:206`).
288 - [ ] Security: `parse_repo_path` rejects lone-dot entries (`git_ssh.rs:162`).
289 - [ ] Security: ClamAV INSTREAM 16K cap → fail-closed on truncation (`scanning/clamav.rs:101-108`).
290 - [ ] UX: validation error messages stop reflecting user input (`wizards/item/mod.rs:176-179`).
291 - [ ] UX: CSRF body extraction stops using `from_utf8_lossy` (`csrf.rs:528-543`).
292 - [ ] Perf: scan-pipeline 400 MiB worst-case capacity note (`constants.rs:156-157`).
293 - [ ] Perf: announcement fan-out persistence + resume (`scheduler/announcements.rs:59-89, 147-177`).
294 - [ ] Perf: build log per-line DB roundtrip (`build_runner.rs:516-534`).
295
296 ### Phase 5 — Chronic
297
298 - [ ] **Invariant-in-prose, FOURTH consecutive run.** Phase 1 #1 (constructive `commit_upload` helper sealing the lower-level scan/credit/status ops) is the only acceptable resolution. After it lands, audit `compute_splits` (Payments) and `ErrorTemplate` (UX) for the same shape and apply the same treatment.
299
300 ---
301
302 ## Ultra Fuzz 2026-05-26 (Run #4)
303
304 Full report: `docs/audit_review.md`. Plan target: lift every axis back to A- or higher (Payments A · Storage A · UX A- · Security A- · Performance A-).
305
306 ### Phase 1 — clear HIGH/CRITICAL caps (must do before launch)
307
308 - [x] **Creator-tier forms missing CSRF token (UX CRITICAL)**`templates/partials/tabs/user_creator.html:80,85`. Add `{% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}` to both forms. Verify the page handler populates `csrf_token` in the template context. Path `/stripe/creator-tier` is not in any exempt prefix, so without the token every authenticated click returns 403.
309 - [x] **`git_ssh.rs` repo-name validation gap (Security HIGH)**`git_ssh.rs:90-102` + `db/git_repos.rs:21-35`. Call `validate_git_repo_name(repo_name).map_err(|_| anyhow!("repository not found"))?` immediately after `parse_repo_path` succeeds in the dispatch path; add the same in `db::git_repos::create_repo`. Without this, the raw name flows into `format!("{op} '/{owner}/{repo_name}.git'")` fed to `git-shell -c`. `cmd_ssh_repo_delete` already validates at line 354 — backport.
310 - [x] **Login lockout `just_locked` per-attempt email flood (Security HIGH)**`db/auth.rs:30-54`. Change the `RETURNING` clause from `(failed_login_attempts >= $2) AS just_locked` to `(failed_login_attempts = $2) AS just_locked` (exact equality, fires only on the crossing attempt). Follow-up: idempotency key on the lockout-email outbox so a regression cannot inbox-flood a known email.
311 - [x] **`cancel_pending_item_checkout` CSRF gap (UX HIGH)**`routes/stripe/checkout/item.rs:390-407`. Either narrow the `/stripe/checkout` CSRF exempt prefix so this path isn't in it, or add explicit `csrf::validate_token` like `create_tip_checkout` does in `stripe/checkout/tips.rs:49-50`. Document the rule at the exempt-prefix definition site so future "I'll just add a quick mutation handler" landings fail review.
312 - [x] **Promo `use_count` over-release on cart cleanup (Payments SERIOUS)**`scheduler/cleanup.rs:223` + `db/transactions.rs:1011-1029`. Cart checkouts produce N pending-tx rows carrying the same `promo_code_id`; the loop calls `release_use_count` N times for a single reservation. Either dedupe with `HashSet<PromoCodeId>` before the release loop, or have `cleanup_stale_pending` return `DISTINCT promo_code_id` from the DELETE.
313 - [x] **Scanner streams instead of `Vec<u8>` (Storage SERIOUS)**`storage.rs:179,404,629` + `scanning/worker.rs:175`. Add `download_stream`/`get_object_stream` to the S3 trait returning a `ByteStream`; pipe through ClamAV instead of buffering. A 20 GB media scan currently OOMs the worker; concurrent scans amplify. (plan: `../../_private/docs/mnw/server-docs/plans/scanner-streaming.md`)
314 - [x] **`scan_jobs` retention (Perf CRITICAL)**`db/scan_jobs.rs`. Add a scheduler tier (hourly) that deletes rows older than 30 days where status ∈ {Clean, Quarantined, Failed}. Keep Pending/Running. Default retention constant in `constants.rs`. (plan: `../../_private/docs/mnw/server-docs/plans/scan-jobs-retention.md`)
315 - [x] **Scanner DB-pool permit across S3 download (Perf CRITICAL)**`scanning/worker.rs`. Drop the pool permit before `download_object` (or `download_stream` after the SERIOUS fix above); reacquire after bytes are local. Under any scan backlog the pool starves request traffic today. (plan: `../../_private/docs/mnw/server-docs/plans/scanner-pool-permit.md`)
316 - [x] **`broadcast.rs` unbounded sequential loop (Perf HIGH)**`routes/api/users/broadcast.rs`. Wrap recipient fan-out in `tokio::spawn` with a bounded `JoinSet` (cap 16) over chunks; preserve the 100ms per-recipient SMTP shape. (plan: `../../_private/docs/mnw/server-docs/plans/broadcast-bounded-fanout.md`)
317
318 ### Phase 2 — close axis-dragging SERIOUS items
319
320 - [x] **Cart template price math (UX MED)**`templates/pages/cart.html:71-76,95,102`. Move `effective_price_cents() / 100` etc. out of templates; pass pre-formatted strings from the handler, or expose `format_price` as an Askama filter. Unify with `format_revenue` so thousands separators match across dashboards (`format_revenue(1_000_000)` returns `"$10000.00"`).
321 - [x] **`media.rs` delete-then-reupload race (Storage MED)**`routes/storage/media.rs:418-456`. Worker must re-check `SELECT 1 FROM media_files/items/versions WHERE s3_key = $1` before each S3 delete, OR queue inserts carry the entity-ID and worker confirms absence. Today a delete-then-reupload within worker latency deletes the fresh file.
322 - [x] **`pending_uploads` reaper-bump owner pinning (Storage MED)**`db/pending_uploads.rs:26-34`. ON CONFLICT only refresh `created_at` when `user_id` matches; otherwise treat as a fresh row. Closes the slow-leak where re-presigning every minute keeps an orphan S3 object alive indefinitely.
323 - [x] **TOTP step-replay across re-enable (Security MED)**`db/totp.rs:60-81`. `disable_totp` adds `totp_last_used_step = NULL`. `set_totp_secret` resets to 0 / NULL. Closes the false-reject DoS when a user disables and re-enables TOTP.
324 - [x] **`delete_other_sessions` cache eviction (Security MED)**`db/sessions.rs:178-192`. Change return to `RETURNING id`; callers iterate and call `state.session_cache.remove(&id)`. Today "log out other sessions" leaves cached `AuthUser` extractor entries valid up to `SESSION_TOUCH_CACHE_SECS`.
325 - [x] **CSRF on `/login` (Security MED)**`csrf.rs:166-181`. Move `/login` out of `exempt_prefixes`; have `login_handler` call `csrf::validate_token` like `authorize_post` does. The login template already renders the token; only middleware bypass is keeping it from doing work. Defense-in-depth for SameSite-Lax-only login-CSRF.
326 - [x] **`is_registered_redirect_uri` `fetch_one` brittleness (Security MED)**`db/oauth.rs:84-97`. Switch to `fetch_optional`, return `Ok(false)` on `None`. Document trailing-slash matching policy explicitly to close the developer-onboarding foot-gun.
327
328 ### Phase 3 — resilience & infra hardening
329
330 - [x] **Multi-replica `claim_pending_build` race (Storage MED)**`db/builds.rs:262-281`. Add `CREATE UNIQUE INDEX … ON ota_builds(status) WHERE status='running'` partial unique index, OR wrap with `pg_advisory_xact_lock`. Today's single-replica prod is safe; the bug fires the moment a second builder joins.
331 - [x] **Build status reaper race (Storage MED)**`db/builds.rs:216-252,287`. Add `WHERE status='running'` to the success UPDATE and check `rows_affected`. Avoids the case where the stale-build reaper marks a successful build failed at the exact second it completes.
332 - [x] ~~**`KNOWN_SYNC_APPS` deletion path (Perf surprise)**~~ — registry removed; no `KNOWN_SYNC_APPS` in `rate_limit.rs` (only JWT extractors). Item moot. — `rate_limit.rs:65-95`. Add `unregister_known_sync_app(app_id)` called from sync-app delete. Long-term: replace `OnceLock<DashSet>` with a DB-backed cache + TTL so multi-replica deploys don't diverge on app inventory.
333 - [x] **`extract_s3_key_from_url` host pinning (Storage SERIOUS)**`storage.rs:582-593`. Require the `https://` host portion to be the configured S3 endpoint host or a known CDN domain before extracting the key. Today path-style branch returns `Some("foo")` for `https://attacker.example/my-bucket/foo`.
334 - [x] **TOTP `pending_2fa_*` tracking row (Security surprise)**`routes/auth.rs:196-202`. Insert a temporary tracking row scoped via `kind='pending_2fa'` (or separate table) at the moment 2FA-pending begins, so `delete_all_sessions_for_user` can sweep a phisher mid-2FA-prompt. Today that intermediate authenticated state is invisible to "log out everywhere".
335
336 ### Phase 4 — polish
337
338 - [x] **`MaybeUserUnverified` rename or short-circuit (Security LOW)**`auth.rs:229-249`. Either rename to make the danger boundary impossible to forget (e.g. `SessionUserStaleAllowed`), or short-circuit to `None` when the session lacks `SESSION_TRACKING_KEY` so legacy sessions don't quietly survive `/logout-everywhere`.
339 - [x] **`sum_file_sizes_for_item` clamp direction (Storage LOW)**`db/versions.rs:320-332`. Replace `COALESCE(LEAST(SUM, i64::MAX)::BIGINT, 0)` with `COALESCE(GREATEST(0, LEAST(SUM, i64::MAX))::BIGINT, 0)`. Today the protection is one-sided; negative values flow through.
340 - [x] **License-key 23505 collision retry (Payments LOW)**`crypto.rs:30-40` + `db/transactions.rs:411-422`. Retry once on UNIQUE violation in `create_license_key` and the promo-claim arm so an unlucky generator collision doesn't surface as a 500.
341 - [x] **Stripe v1 multi-signature rotation (Payments LOW)**`payments/webhooks.rs:350-389`. Collect all `v1=` values from the header; accept on any match against any configured secret. Today only the last `v1=` value is tried, which breaks Stripe's documented secret-rotation procedure.
342 - [x] **`has_active_subscription_to_project` period-end check (Payments LOW)**`db/subscriptions.rs:394-408`. Add `AND (current_period_end IS NULL OR current_period_end > NOW())`. Defense-in-depth for a missed/delayed `customer.subscription.deleted` webhook.
343 - [x] **Download HTML sniff strengthening (Security LOW)**`scanning/content_type.rs:119-146`. For Download, add a lightweight string sniff for `<!--`, `<script`, `<svg`, `<?xml`, BOM-stripped `<html` after `infer` returns None. Pair with `Content-Disposition: attachment` on all served downloads so the browser never renders inline regardless of scan verdict.
344 - [x] **Profile-link `rel="ugc nofollow"` (UX LOW)**`templates/pages/user.html:84`. Add `rel="ugc nofollow"` to user-supplied profile links.
345 - [x] **`format_revenue` thousands separator (UX LOW)**`formatting.rs:43-51`. Make `format_revenue` use thousands separators consistent with `format_price`. Dashboards mixing both currently render "$1,234" and "$10000.00" side by side.
346 - [x] **Third-party credits / attributions page (UX, brand)** — Build a public page that credits every library, vendor, and dependency the platform leans on (Rust crates from server + multithreaded + pom + mnw-cli + shared/, JS libs, fonts, SDKs, payment / object-storage / email vendors). Pull crate names + licenses out of `cargo metadata`, group by tier (runtime infra, scanning, payments, etc.), and write a short human-readable paragraph for the big ones (tokio, axum, sqlx, aws-sdk-s3, async-stripe, yara-x, fs2, etc.). Lives somewhere discoverable from the footer or the `/about` tree. Worth real effort — this is one of the visible ways MNW shows respect for the OSS ecosystem it stands on.
347
348 ### Phase 5 — chronic
349
350 - [~] **Invariant-in-prose / policy-not-in-types — third consecutive run (CHRONIC)** — scan_status-ordering half closed 2026-05-26 (see Phase 1 entry for `images.rs::item_image_confirm`). The constructive-impossibility shape from the chronic-remediation rubric: `commit_*_upload` is the only handler-reachable path that writes both row + scan_status; the lower-level scan_status writes were renamed `set_*_scan_status_standalone` and documented as worker- and admin-override-only. Compiler-driven migration found one additional handler with the same bug (CLI internal upload) — that's the test the rubric wants: structural change exposes drift, not human review. Remaining: `/stripe/*` CSRF policy patchwork — same disease, different organ. Track as Landing 2 below.
351
352 - [x] **Per-route CSRF posture (CHRONIC, second half) — Landing 2** — landed 2026-05-26. `csrf.rs::exempt_prefixes` allowlist replaced with per-route helpers + a `CsrfRouter<S>` newtype that only accepts `PostureMethodRouter<S>` values produced by the `{post,put,patch,delete}_csrf*` helpers. A bare `Router::route(path, post(handler))` no longer compiles inside any of the 14 mutation-bearing route files — the structural guarantee replaces the originally-planned runtime trip wire, which was impossible because per-route layers run *inside* global ones (the global writer would have run after the global reader). Compiler-driven migration surfaced one real regression (Manual-posture tip handler was form-only; old middleware was header-then-form) — fixed by routing the Manual handler through `extract_token_from_request` to match the standard precedence.
353
354 Sub-steps:
355 - [x] L2.1 Helpers + sealed marker. `CsrfPosture::{Auto, Manual(&'static str), Skip(&'static str)}`, `CsrfManuallyValidated` ZST with sealed constructor, `validate_token_consuming` as the only producer.
356 - [x] L2.2 Per-route layers replace global middleware. The Auto helper attaches a per-route `from_fn` that runs `validate_auto`; Skip/Manual attach nothing at runtime (the posture lives in the helper signature for source-level grep, not in request extensions).
357 - [x] L2.3 Inventory: 290 mutation routes across 14 files (208 POST · 36 PUT · 2 PATCH · 45 DELETE).
358 - [x] L2.4–L2.6 Migrate all routes. Skip for webhooks / pre-auth / HMAC-internal / bearer-sync / guest-checkout; Manual for the tip handler; Auto for the rest. Reason strings are at the call site (`STRIPE_SESSION_SKIP`, `SYNCKIT_API_KEY_SKIP`, etc.).
359 - [x] L2.7 Allowlist + `csrf_middleware` deleted from `csrf.rs` and `lib.rs`.
360 - [x] L2.8 Structural enforcement via `CsrfRouter<S>` + `PostureMethodRouter<S>`. `route_get` for read-only methods; `merge`, `nest`, `layer`, `route_layer` mirror axum's `Router`. `finalize()` is the single escape hatch, called once in `build_app`.
361
362 Follow-ups:
363 - [ ] **Manual-posture runtime assertion (dev builds).** Today `*_csrf_manual` requires no compile-time proof that the handler called `validate_token_consuming`. Only the tip handler is Manual, and `_validated` is bound only as documentation. In dev/test builds, set a flag in `validate_token_consuming` and debug-assert it after the handler runs; mismatched routes panic loudly in CI without affecting prod. Not blocking — only matters if Manual grows beyond one route.
364 - [ ] **Phase 1 entries still open:** `cancel_pending_item_checkout` Skip reason is `"Phase 1 todo: tighten to post_csrf"` (grep "Phase 1 todo" to find). `/login` and `creator-tier` template tightening tracked separately above.
365
366 - [x] **Integration test drift — 42 pre-existing failures (snapshot 2026-05-26 post-Landing 2).** Cleared 2026-05-27. Three landings (harness helpers, KeyCode validator + fixtures, password_reset body asserts) + per-test cleanup. Real code fixes that fell out: `validate_key_code` accepts 5 or 6 words (generator was emitting 6 but validator pinned at 5); `update_build_status` source-status gate now `IN ('pending','running')` (Phase 3 gate inadvertently blocked cancel of pending builds); `validate_auto` short-circuits safe methods (GET on multi-method `with_csrf` routes was 403); `media_confirm` matches `AppError::Database(sqlx::Error::Database)` for 23505 instead of substring on the wrapper Display. All 803 integration tests now pass. Net delta from the CSRF migration is 0 (tip-handler regression was found and fixed; the remaining 42 match the pre-migration baseline modulo flakes). These are not CSRF-related and need triage in their own landings. Re-snapshot before each push.
367 - [ ] workflows::admin::admin_approve_item_upload
368 - [ ] workflows::admin::admin_approve_version_upload
369 - [ ] workflows::admin::admin_reject_item_upload
370 - [ ] workflows::admin::admin_reject_version_upload
371 - [ ] workflows::adversarial_auth::suspended_user_login_ok_writes_blocked
372 - [ ] workflows::adversarial_business::exhausted_promo_code_rejected
373 - [ ] workflows::auth::lockout_after_failed_attempts
374 - [ ] workflows::auth::nonexistent_user_rejected
375 - [ ] workflows::auth::password_change_flow
376 - [ ] workflows::auth::wrong_password_rejected
377 - [ ] workflows::creator_media::scan_real_flac_passes
378 - [ ] workflows::creator_media::scan_real_mp3_passes
379 - [ ] workflows::creator_media::scan_real_wav_passes
380 - [ ] workflows::fingerprinting::license_deactivate_frees_slot
381 - [ ] workflows::fingerprinting::license_verify_lifecycle
382 - [ ] workflows::fingerprinting::license_verify_requires_verification_enabled
383 - [ ] workflows::fingerprinting::license_verify_revoked_key
384 - [ ] workflows::license_keys::license_key_lifecycle
385 - [ ] workflows::license_keys::max_activations_enforced
386 - [ ] workflows::license_keys::revoke_key_then_validate_fails
387 - [ ] workflows::license_keys::v1_license_endpoints
388 - [ ] workflows::lifecycle::sandbox_lifecycle_create_use_expire_cleanup
389 - [ ] workflows::media_library::filename_collision_rejected
390 - [ ] workflows::password_reset::password_reset_expired_link
391 - [ ] workflows::password_reset::password_reset_passwords_must_match
392 - [ ] workflows::password_reset::password_reset_tampered_signature
393 - [ ] workflows::promo_codes_free_access::free_access_code_claim_already_owned
394 - [ ] workflows::promo_codes_free_access::free_access_code_claim_by_buyer
395 - [ ] workflows::promo_codes_free_access::free_access_code_generate_list_delete
396 - [ ] workflows::sandbox::create_sandbox_account
397 - [ ] workflows::sandbox::sandbox_blocks_restricted_endpoints
398 - [ ] workflows::sandbox::sandbox_blog_no_email
399 - [ ] workflows::sandbox::sandbox_content_not_visible_on_item_page
400 - [ ] workflows::sandbox::sandbox_rss_returns_404
401 - [ ] workflows::scanning::admin_approve_held_upload
402 - [ ] workflows::scanning::confirm_upload_bad_magic_quarantined
403 - [ ] workflows::scanning::confirm_upload_clean_file_passes
404 - [ ] workflows::scanning::trusted_creator_upload_auto_publishes
405 - [ ] workflows::scanning::untrusted_creator_upload_held_for_review
406 - [ ] workflows::subscriptions::sandbox_tier_uses_fake_stripe_ids
407 - [ ] workflows::suspension::suspended_user_claim_promo_code_blocked
408 - [ ] workflows::wizards::wizard_back_navigation
409
410 ### Notes & non-actions
411
412 - Status-notification fan-out cooldown across overlapping tasks (`monitor.rs:213-237`) — single-replica today; harmless. Reconsider when adding a second instance.
413 - `record_storage_fill_stats` JOIN (`metrics.rs:181-218`) — 5min cadence is acceptable at 100k users; revisit at 1M.
414 - `metadefender` could run concurrently with MalwareBazaar in suspicion path (`scanning/mod.rs:377-398`) — micro-optimization, deferred.
415 - `populate_known_sync_apps` startup-only (`rate_limit.rs:65-85`) — paired with the deletion-path item above; together they're a single fix.
416
417 ---
418
419 ## Ultra Fuzz 2026-05-26 (Run #3)
420
421 Full report: `docs/audit_review.md`. Plan target: lift every axis to A- or higher (Payments A · Storage A- · UX A · Security A- · Performance A-).
422
423 ### Notes & non-actions
424
425 - Backup-code fast-path malformed-hash trap (`db/totp.rs:155-189`) — log + alert + fall through to legacy path; small, file as polish.
426 - `session_cache` TTL window vs admin revoke (`auth.rs:154-191`) — documented as intentional; consider exposing a broadcast invalidate op if operator demand emerges.
427 - `monitor.rs` `pg_stat_activity` cadence already covered by Phase 2 split.
428
429 ---
430
431 ## Ultra Fuzz 2026-05-25 (Run #2)
432
433 Full report: `docs/audit_review.md`.
434
435 ### Outbox follow-ups — convert remaining webhook handlers
436
437 All five remaining handlers converted to outbox 2026-05-25; migration 125 added `fan_plus_subscription_id` and `creator_subscription_id` parents so each subscription type has its own idempotency anchor.
438
439 ### Current phase — serious / high
440
441 - [ ] **Login template field-aware errors** — deferred 2026-05-26. Re-scoped: error-construction infra (`AppError::validation_fields`) is in place in `join_wizard::step_account_create`, but neither signup nor login renders per-field highlights yet. Real work is a new HTMX partial with OOB swaps per input + per-field error containers on both templates. Login itself has only one safe per-field message by design (creds are intentionally generic to avoid enumeration); the value is mostly on the signup side.
442 - [~] **Scanning peak memory**`scanning/mod.rs:174` already uses `std::sync::Arc::<[u8]>::from(data)` which dispatches through `Vec::into_boxed_slice` and reuses the allocation; SHA-256 streams via `Sha256::update` over the same Arc-shared buffer. No change needed.
443 - [~] **`check_sales_count_drift` full GROUP BY** — the SQL already filters via `HAVING i.sales_count != COUNT(t.id)` (the real bound). The trailing `LIMIT 50` is a per-tick cap on how many drifts to surface, not a cosmetic post-group filter. No action.
444
445 ### Current phase — medium / minor
446
447 - [~] **`pg_stat_activity` baseline load**`monitor.rs:290-294` doc explicitly justifies the 30 s cadence for operator-dashboard refresh; no change.
448
449 ### Deferred — architectural
450
451 - [ ] **Cloudflare-only origin: migrate custom domains to CF for SaaS, then firewall 80/443.** Re-scoped 2026-05-26. The original sketch (firewall the origin to CF IP ranges) conflicts with the shipped custom-domain feature (`api/domains.rs` + Caddy `on_demand_tls`), which expects creators' A-records to hit the origin directly. The two threats the firewall was meant to close are already mitigated at layer 7 — `CloudflareIpKeyExtractor` peer-IP fallback (landed 2026-05-26) closes the CF-Connecting-IP spoofing surface; Caddy `client_auth require_and_verify` closes the WAF-bypass surface for `makenot.work`. The proper sequencing is now (1) upgrade to CF Business for CF-for-SaaS, (2) reconfigure CF dashboard with a fallback origin, (3) update `api/domains.rs` onboarding to CNAME instead of A-record, (4) migrate the 1 live custom-domain creator, (5) drop `on_demand_tls` from `Caddyfile`, (6) apply the firewall ACL. Full sequence + ACL sketch + gotchas live in `_meta/docs/incident_response.md` § "Pending Hardening: Cloudflare-only origin firewall". Blocked on the CF plan upgrade + 1 customer email, neither of which happens in-session.
452
453 ---
454
455 ## Ultra Fuzz 2026-05-24 (Run #1)
456
457 Full report: `docs/audit_review.md`.
458
459 ### Current phase — medium
460
461 - [~] **Status notification parallel fan-out** — kept sequential with 100 ms shaper; that pacing is intentional (SMTP rate-limit shape). No change.
462
463 ### Deferred — architectural
464
465 All four Run #1 deferred items closed by Run #2 sweeps; pointers below.
466
467 ---
468
469 ## PoM contract guard (landed 2026-05-25)
470
471 Schema-drift guard test wired against `shared/pom-contract/`: `src/routes/pages/public/health/mod.rs::tests::pom_hetzner_health_expectations_resolve`. `health_json` body builder extracted as pure `health_json_body(overall, db_ok)` for the test. Catches the v0.5.16-class drift where a field is removed from `/api/health` without updating PoM's expectations. See `MNW/CLAUDE.md` § PoM Health Contract.
472