# MNW Server — Todo **Last updated:** 2026-05-31 late evening (post Run #9 — launch-eve pass). ## Status 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. ## Run #9 — fixed this session (2026-05-31) - **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). - **Sec-SERIOUS** `delete_all_sessions_for_user` non-atomic JWT bump → wrapped in `pool.begin()` / `tx.commit()` (`db/sessions.rs:247`). - **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`). - **Pay-SERIOUS** Webhook dual-failure 503 short-circuited on Stripe retry → call `unmark_event_processed` before returning 503 (`routes/stripe/webhook/mod.rs:81`). `cargo check --tests` clean; targeted unit tests (sessions/webhook/two_factor/join_wizard) 33/33 green. Full DB-integration suite needs astra postgres. ## Run #9 — deferred with rationale (Phase 4) - [ ] **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. - [ ] **Sto-HIGH** Migration 129 dead-letter table never written (`cleanup.rs:453`). Operational visibility, not runtime; one-INSERT fix. - [ ] **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. - [ ] **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. - [ ] **Pay-MED** `pricing.rs::parse_dollars_to_cents` strips European decimal comma; `1,23` → 12300¢. - [ ] **Pay-MED** SyncKit app-sub checkout silently defaults `storage_limit_bytes` to 0 if metadata missing. - [ ] **Pay-MED** Guest checkout email sentinel `"unknown@guest"` collision risk. - [ ] **Sto-MED** `is_s3_key_live` 7 EXISTS subqueries on unindexed s3_key columns — sequential scans per retry. Add partial indexes WHERE NOT NULL. - [ ] **Sto-MED** `is_s3_key_live` LIKE suffix `'%' || s3_key` false-positives on neighboring keys → S3 object leaks. Anchor with `/`. - [ ] **UX-MED** `purchase.html:145` `?return_to=` dead-wired; login handler always redirects `/dashboard`. - [ ] **UX-MED** Admin user filter buttons (`admin-users.html:35-44`) use `class="primary"` instead of `btn-primary` — renders unstyled. ## Run #9 — LOW/NOTE (carry forward) - [ ] **UX-LOW** Pagination links in `git/issues.html:72,76` don't URL-encode `search` param. - [ ] **UX-LOW** 5 sites use `.render().unwrap_or_default()` on Askama templates — blank UI on render failure, no log line. - [ ] **UX-LOW** `slugify` (`formatting.rs:85`) produces `"post"` for any non-ASCII title. - [ ] **Sec-MINOR** `csrf.rs:176-185` `validate_token_consuming` doesn't actually consume — rename or rotate. - [ ] **Sec-MINOR** `routes/oauth.rs:101-111` `is_localhost_redirect` allows any port regardless of registered URI. - [ ] **Sec-MINOR** `scanning/archive.rs:124` path-traversal check misses lone `..` segment (no trailing `/`). - [ ] **Perf-LOW** `db/page_views.rs` `pending` HashMap has no max-cardinality cap. - [ ] **Perf-LOW** `build_runner.rs:441` artifact tmpfile leaks if process crashes between SCP and `remove_file`. 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`). ## Open before launch (Monday 2026-06-01) ### Platform-as-product audits (skill-driven, code-review scope; fresh context recommended) - [ ] `/creator-fuzz` — would a working creator trust this with their livelihood? - [ ] `/use-fuzz` — discoverability, learnability, first-five-minutes - [ ] `/business-fuzz` — pricing copy, fee surfacing, refund-policy wording vs actual platform behaviour ### Per-project hygiene (manual, my call when ready) - [ ] README first-screen audit — what is this / who is it for / where to get it / what does it cost. No headliner paragraphs. - [ ] `Cargo.toml` version bump for the launch deploy (pick the number; I do the edit if needed) - [ ] CHANGELOG entry for the launch version ### Monday browser/prod testing (saved for Monday per current direction) - [ ] §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 - [ ] §1.2 First-run creator flow end-to-end in production: signup → Stripe Connect → first item upload - [ ] §1.3 Each seeded creator's `/{handle}` page renders without empty sections; sample item per medium (audio/video/text/download) reachable from `/discover` - [ ] §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 - [ ] §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`) ## Open question for the user (action before Monday) - [ ] **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. ## Deferred with rationale (no action; documented) - [ ] `build_runner.rs:151` serial-target loop. LOW; builds run rarely; refactor touches denominator + error aggregation + log order. Post-launch. - [ ] `scheduler/mod.rs:92-279` advisory-lock per-tier granularity. Multi-replica concern; defer until multi-replica is real. - [ ] Drop unused `completion_effects` table (migration cleanup, schema-only). - [ ] 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. - [ ] `_head_assets.html` apple-touch-icon + manifest link wiring. `static/manifest.json` exists but the `` was reverted; bring back if/when desired. - [ ] Migrate footer's `What's new` and `Shortcuts` `` to `data-*` attributes following the `data-copy-link` pattern. UX MED, not blocking. ## What's done this session (compact summary, full details below) - **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. - **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. - **4 cross-cutting sweeps** — `info@makenot.work` email pin (8 files), localhost/TODO/emoji/secret scans all clean. - **§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). - **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 }}`. - **Exorcise** — 9 AI-tell removals across compare.md, content-scanning.md, appeals.md, faq.md. - **Nitpick** — 2 polish edits (dead `let _ = scan_status` removed, unused tuple-name destructure tidied). - **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. --- ## Ultra Fuzz 2026-05-31 (Run #8 — final re-grade) ### Above-MED items to address before launch (or defer with rationale) - [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). ### New MED-tier findings (all closed 2026-05-31) - [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. - [x] **Payments MED — Cart-all chain-break on all-free first seller.** Fixed. `process_seller_checkout` signature changed `Result` → `Result>`; 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. - [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. - [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 `Copy link` (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`. - [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). All 5 MEDs landed. `cargo test --lib` 1654 / 0. ### Verified closed this run - [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. - [x] **Storage S1** — `media_confirm` three-write atomicity (media.rs:241-293). Single tx wraps storage credit + pending_uploads clear + media_files INSERT. - [x] DB helpers genericized to `impl PgExecutor<'e>` — all 12 callers (including `synckit/blobs.rs:157`) verified backwards-compatible. ### Storage A- standing — remaining MED/LOW (Phase 4 polish or defer) Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A- blockers. --- ## Audit backlog sweep 2026-05-31 (post-Run #8, 7 waves) Sorted by file locality and difficulty. Tests: 1655 / 0 throughout. ### Wave 1 — auth/security cluster (8 tiny) - [x] `synckit_auth.rs:147` `<` → `<=` closes 1-second JWT-revocation collision window. - [x] `routes/auth.rs:128, 137` malformed-email + invalid-username branches now run DUMMY_HASH equalizer — closes timing oracle. - [x] `routes/auth.rs:331-336` `validate_username` length switched from `len()` bytes to `chars().count()` — multi-byte usernames treated correctly. - [x] `git_ssh.rs:162` `parse_repo_path` rejects lone-dot segments. - [x] `routes/oauth.rs:206` `validate_token` → `validate_token_consuming` (sealed witness type). - [x] `routes/oauth.rs:213-222` OAuth `state` ≤ 1024 bytes + `code_challenge` ≤ 44 chars. - [x] `helpers.rs::ip_advisory_lock_key` `DefaultHasher` → SHA-256 (stable across Rust versions). - [x] `helpers.rs::extract_client_ip` one-shot WARN after 100 cumulative missing `cf-connecting-ip` requests. ### Wave 2 — scanning (3) - [x] `scanning/clamav.rs::ping` + `ScanPipeline::assert_live` at startup; refuses to boot if scanning configured but no AV layer live. - [x] `scanning/clamav.rs` 16 KB INSTREAM truncation → `LayerVerdict::Fail` (was Error → FailOpen → Pass). - [x] `scanning/worker.rs:251` inline media UPDATE swapped to `db::scanning::update_media_file_scan_status` helper. ### Wave 3 — DB layer polish (4) - [x] `db/pending_uploads.rs::remove_pending_upload` signature now requires `user_id`; 12 callers updated. - [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`. - [x] `db/projects.rs::update_project_image_url` returns `Result`; `images.rs::project_image_confirm` three-arm match fires rollback + orphan-queue on `Ok(false)`. - [x] `db/items/media.rs::update_item_cover` same shape; same caller treatment in `images.rs::item_image_confirm`. ### Wave 4 — storage handlers + admin rescan seal + downloads (5) - [x] **Migration 133** `items_duration_seconds_nonnegative.sql` CHECK on `duration_seconds` + `video_duration_seconds`. - [x] `routes/storage/downloads.rs:120` defensive clamp: `duration.max(0) as u64 → saturating_mul(2) → clamp(3600, 86_400)`. - [x] `routes/storage/mod.rs::commit_rescan` new sibling to `commit_upload` for admin-rescan paths. - [x] `routes/admin/uploads.rs::rescan_{version,item}_inner` migrated to `commit_rescan`; chronic-disease seal now covers admin paths. - [x] `routes/pages/dashboard/wizards/item/save.rs:95` wizard `update_item_cover_image_url` call dropped (confirm authoritative, hidden-field desync risk closed). - [x] `routes/storage/mod.rs` `enqueue_s3_orphan` doc rewritten to match reality (post-credit failures only). ### Wave 5 — UX polish (2) - [x] `pricing.rs::parse_dollars_to_cents` strips `$`, `,`, whitespace; new `strips_clipboard_decoration` test. - [x] `routes/admin/users.rs:37, 77` page `clamp(1, 1_000_000_000)` to prevent OFFSET overflow → sqlx 500. ### Wave 6 — Performance (3 of 5; 2 deferred) - [x] `metrics::idempotency_middleware` in-memory negative cache (OnceLock, 60s TTL, periodic GC). Skips per-POST `get_cached_response` SELECT for keys recently confirmed not-cached. - [x] `monitor.rs` `record_storage_fill_stats` gated by 5-min TTL — 60× reduction at 10k+ creators. - [x] `scheduler/cleanup.rs::cleanup_sandbox_accounts` serial loop → JoinSet `CLEANUP_PARALLELISM=4`. - [ ] **DEFERRED** `build_runner.rs:151` serial-target loop. LOW; refactor touches denominator + error agg + log order. Post-launch. - [ ] **DEFERRED** `scheduler/mod.rs:92-279` advisory-lock granularity. Multi-replica concern; defer until multi-replica is real. ### Wave 7 — Payments LOW (2) - [x] `routes/stripe/webhook/mod.rs:73-87` `insert_failed_event` failure → 503 (Stripe redelivers) instead of dropping event with 200. - [x] `routes/stripe/checkout/cart.rs:73-82, 535-543` `remove_from_cart` already-owned cleanup now logs WARN on Err. --- ## Storage code-fuzz 2026-05-31 (post-Run #7) Targeted Storage-axis fuzz to verify A- before triggering full Run #8. ### Above-MED fixes that landed - [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. - [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. - [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). ### Remaining MED/LOW (below A- bar; defer or Phase 4 polish) - [ ] 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. - [ ] 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)`). - [ ] 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, ...)`. - [ ] Storage MED — `enqueue_s3_orphan` single-policy doc overstates discipline; either tighten doc or migrate remaining direct `delete_object` cleanup sites. - [ ] Storage MED — `is_s3_key_live` doesn't enumerate project image URLs (no current bug; surface fragile). - [ ] Storage LOW — `scanning/worker.rs:251` inline UPDATE bypasses `db::scanning::update_media_file_scan_status` helper. - [ ] Storage LOW — wizard `save.rs:95` updates only `cover_image_url` (not s3_key/size). - [ ] Storage LOW — `pending_uploads::remove_pending_upload` deletes by s3_key alone (signature broader than needed). --- ## Ultra Fuzz 2026-05-31 (Runs #6, #7 + S1) ### Structural / chronic-disease fixes that landed - [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. - [x] `crate::pricing::parse_dollars_to_cents` + `validate_dollars_f64` shared helpers; 5 callsites converted (item save, project wizard ×2, bulk price, projects API). - [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. ### Bug-level fixes that landed - [x] Storage CRIT Run #6 — `enqueue_s3_orphan` wired at `uploads.rs:325` post-commit old-key delete. - [x] Storage HIGH Run #6 — `images.rs::project_image_confirm` idempotency check added. - [x] Storage HIGH Run #6 — `images.rs::item_image_confirm` scan-ordering fixed via commit_upload. - [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). - [x] Storage HIGH Run #7 — `update_project_image_url` + `update_item_cover` failures now refund storage + queue new key for orphan deletion. - [x] Storage MED Run #6 — `media_delete` enqueue moved after `tx.commit()`. - [x] UX HIGH Run #6 — `routes/api/projects.rs` float-parse via `validate_dollars_f64`. - [x] UX HIGH Run #6 — project wizard tier-row loop bubbles errors instead of silently `continue`-ing. - [x] UX HIGH Run #7 — `pricing_model` silent fallback to Free fixed; missing/malformed now rejects. - [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. - [x] Payments S Run #6 — `process_seller_checkout` uses bulk `purchased_subset`. - [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. - [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). - [x] Payments H Run #6 — free PWYW project checkout clears contact revocation when share_contact=true. - [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). ### Deferred (with rationale) - [x] **Payments H2 (Run #7) — `claim_free_project` race.** Fixed 2026-05-31. `db::transactions::claim_free_project` now returns `Result` (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). - [ ] Drop unused `completion_effects` table — schema-only cleanup; harmless empty table. ### Notes on remaining MED/LOW (per Run #7 axis reports) - 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. - Storage MED — `update_item_cover` / `update_project_image_url` don't check `rows_affected()`; an ownership-filter mismatch returns Ok(0 rows) silently. - Storage MED — worker inline media UPDATE at `scanning/worker.rs:251` should use the new `db::scanning::update_media_file_scan_status` helper. - Storage LOW — internal CLI confirm drops returned `FileScanStatus` (no `pending_review` surfacing). - Storage LOW — `main.rs:334` comment references now-private `enqueue_scan_for`. - UX MED — `parse_dollars_to_cents` rejects `"$5"` and `"1,000"` literally; could strip `$`/`,` for clipboard-paste UX. - UX MED — project wizard skips `validate_tier_price` ($1–$10k); API path enforces it. - UX LOW — `BundleItemIds.filter_map` silently drops malformed UUIDs. - Payments M1 — `compute_splits` should `.max(0)` per-member for defense vs legacy negative `split_percent` rows. - Payments NIT — extract `require_stripe_ready` helper; six near-identical 5-line blocks across checkout files. --- ## Ultra Fuzz 2026-05-30 (Run #5) ## Ultra Fuzz 2026-05-30 (Run #5) 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. ### Phase 1 — CRITICAL (fix today) - [ ] **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. - [ ] **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. - [ ] **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". ### Phase 2 — SERIOUS / HIGH (fix this weekend) - [ ] **Payments SERIOUS — NULL `item_id` refund decode bomb** — `db/transactions.rs:699-716`. Return `Vec<(TransactionId, Option)>`; skip `decrement_sales_count`/`revoke_keys_by_transaction` when None. Fixture test against a project-level transaction. - [ ] **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%. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **UX HIGH — Username live-check fails open on DB error** — `routes/auth.rs:356-361`. Propagate error or treat as "unavailable, try again". - [ ] **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. - [ ] **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. - [ ] **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. ### Phase 3 — MED (fix before Run #6 if cheap) - [ ] Storage: advisory-lock leak in `check_sandbox_cap` (`db/mod.rs:92-128`) → `pg_advisory_xact_lock` or RAII guard. - [ ] Storage: `is_s3_key_live` missing tables (`db/pending_s3_deletions.rs:67-82`). - [ ] Storage: `delete_version` owner SELECT outside tx + post-commit S3 enqueue (`db/versions.rs:267-315`). - [ ] Security: ClamAV `FailOpen` startup assertion (`scanning/clamav.rs:19` + `scanning/mod.rs:151-164`) — refuse boot if scan configured but no AV layer live. - [ ] Security: `helpers.rs:44-50` `DefaultHasher` → stable hasher (sha2 first 8 bytes or `xxh3` constant seed). - [ ] Security: OAuth `state` size cap (`routes/oauth.rs:379-386`) — reject `> 1024`; cap `code_challenge` at 44 chars. - [ ] Security: `extract_client_ip` non-Cloudflare fallback warning (`helpers.rs:33-40`). - [ ] UX: pagination offset overflow (`routes/pages/public/discover.rs:85-87`, `routes/admin/users.rs:37-39`). - [ ] UX: forms silently render without `_csrf` when handler forgets to populate token — make `csrf_token` non-optional in form-bearing templates. - [ ] UX: `validate_username` byte-length vs `chars().count()` (`routes/auth.rs:322`). - [ ] Perf: scheduler advisory-lock connection pinned across S3 (`scheduler/mod.rs:92-279`) → dedicated `max_connections(1)` pool. - [ ] Perf: cleanup S3 deletes serialized inside scheduler tick (`scheduler/cleanup.rs:77-100`) → `for_each_concurrent(8, ...)`. ### Phase 4 — Polish (after Run #6 confirms ≥ A-) - [ ] Payments: `has_active_subscription_to_item` period-end clause mirroring (`db/subscriptions.rs:464-470`). - [ ] Payments: `get_active_creator_tier` + `sync_user_creator_tier` period-end defense (`db/creator_tiers.rs:91-103, 181-194`). - [ ] Payments: `release_use_count` race messaging (`db/promo_codes.rs:184-200`). - [ ] Payments: License key `activation_count` recount on revoke (`db/license_keys.rs:343-382`). - [ ] Payments: Subscription minimum-charge check (`payments/checkout.rs:283-317`). - [ ] Payments: Webhook v1/v2 unmark-on-failure parity (`routes/stripe/webhook/mod.rs:48-86`). - [ ] Storage: `media_files.list_folders` scan filter (`db/media_files.rs:73-82`). - [ ] Storage: `pending_uploads.record_pending_upload` silent user-mismatch (`db/pending_uploads.rs:23-33`). - [ ] Storage: `append_log_bounded` non-atomic size cap (`build_runner.rs:516-534`). - [ ] Storage: `downloads.rs:119-122` presigned-URL expiry — cap `duration_seconds` + DB CHECK ≥ 0. - [ ] Security: `validate_token_consuming` for OAuth POST (`routes/oauth.rs:206`). - [ ] Security: `parse_repo_path` rejects lone-dot entries (`git_ssh.rs:162`). - [ ] Security: ClamAV INSTREAM 16K cap → fail-closed on truncation (`scanning/clamav.rs:101-108`). - [ ] UX: validation error messages stop reflecting user input (`wizards/item/mod.rs:176-179`). - [ ] UX: CSRF body extraction stops using `from_utf8_lossy` (`csrf.rs:528-543`). - [ ] Perf: scan-pipeline 400 MiB worst-case capacity note (`constants.rs:156-157`). - [ ] Perf: announcement fan-out persistence + resume (`scheduler/announcements.rs:59-89, 147-177`). - [ ] Perf: build log per-line DB roundtrip (`build_runner.rs:516-534`). ### Phase 5 — Chronic - [ ] **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. --- ## Ultra Fuzz 2026-05-26 (Run #4) 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-). ### Phase 1 — clear HIGH/CRITICAL caps (must do before launch) - [x] **Creator-tier forms missing CSRF token (UX CRITICAL)** — `templates/partials/tabs/user_creator.html:80,85`. Add `{% if let Some(token) = csrf_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. - [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. - [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. - [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. - [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` before the release loop, or have `cleanup_stale_pending` return `DISTINCT promo_code_id` from the DELETE. - [x] **Scanner streams instead of `Vec` (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`) - [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`) - [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`) - [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`) ### Phase 2 — close axis-dragging SERIOUS items - [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"`). - [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. - [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. - [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. - [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`. - [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. - [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. ### Phase 3 — resilience & infra hardening - [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. - [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. - [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` with a DB-backed cache + TTL so multi-replica deploys don't diverge on app inventory. - [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`. - [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". ### Phase 4 — polish - [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`. - [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. - [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. - [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. - [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. - [x] **Download HTML sniff strengthening (Security LOW)** — `scanning/content_type.rs:119-146`. For Download, add a lightweight string sniff for `