| 91 |
91 |
|
|
| 92 |
92 |
|
### Above-MED items to address before launch (or defer with rationale)
|
| 93 |
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 |
94 |
|
### New MED-tier findings (all closed 2026-05-31)
|
| 97 |
95 |
|
|
| 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 |
96 |
|
All 5 MEDs landed. `cargo test --lib` 1654 / 0.
|
| 105 |
97 |
|
|
| 106 |
98 |
|
### Verified closed this run
|
| 107 |
99 |
|
|
| 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 |
100 |
|
### Storage A- standing — remaining MED/LOW (Phase 4 polish or defer)
|
| 113 |
101 |
|
Carried from Storage code-fuzz 2026-05-31 — see below. All still MED, none A- blockers.
|
| 114 |
102 |
|
|
| 119 |
107 |
|
Sorted by file locality and difficulty. Tests: 1655 / 0 throughout.
|
| 120 |
108 |
|
|
| 121 |
109 |
|
### 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 |
110 |
|
### 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 |
111 |
|
### 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 |
112 |
|
### 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 |
113 |
|
### 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 |
114 |
|
### 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 |
115 |
|
- [ ] **DEFERRED** `build_runner.rs:151` serial-target loop. LOW; refactor touches denominator + error agg + log order. Post-launch.
|
| 159 |
116 |
|
- [ ] **DEFERRED** `scheduler/mod.rs:92-279` advisory-lock granularity. Multi-replica concern; defer until multi-replica is real.
|
| 160 |
117 |
|
|
| 161 |
118 |
|
### 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 |
119 |
|
---
|
| 166 |
120 |
|
|
| 167 |
121 |
|
## Storage code-fuzz 2026-05-31 (post-Run #7)
|
| 169 |
123 |
|
Targeted Storage-axis fuzz to verify A- before triggering full Run #8.
|
| 170 |
124 |
|
|
| 171 |
125 |
|
### 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 |
126 |
|
### Remaining MED/LOW (below A- bar; defer or Phase 4 polish)
|
| 177 |
127 |
|
- [ ] 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 |
128 |
|
- [ ] 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)`).
|
| 188 |
138 |
|
## Ultra Fuzz 2026-05-31 (Runs #6, #7 + S1)
|
| 189 |
139 |
|
|
| 190 |
140 |
|
### 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 |
141 |
|
### 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 |
142 |
|
### 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 |
143 |
|
- [ ] Drop unused `completion_effects` table — schema-only cleanup; harmless empty table.
|
| 215 |
144 |
|
|
| 216 |
145 |
|
### Notes on remaining MED/LOW (per Run #7 axis reports)
|
| 305 |
234 |
|
|
| 306 |
235 |
|
### Phase 1 — clear HIGH/CRITICAL caps (must do before launch)
|
| 307 |
236 |
|
|
| 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 |
237 |
|
### Phase 2 — close axis-dragging SERIOUS items
|
| 319 |
238 |
|
|
| 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 |
239 |
|
### Phase 3 — resilience & infra hardening
|
| 329 |
240 |
|
|
| 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 |
241 |
|
### Phase 4 — polish
|
| 337 |
242 |
|
|
| 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 |
243 |
|
### Phase 5 — chronic
|
| 349 |
244 |
|
|
| 350 |
245 |
|
- [~] **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 |
246 |
|
|
| 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 |
247 |
|
Follow-ups:
|
| 363 |
248 |
|
- [ ] **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 |
249 |
|
- [ ] **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 |
250 |
|
|
| 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 |
251 |
|
### Notes & non-actions
|
| 411 |
252 |
|
|
| 412 |
253 |
|
- Status-notification fan-out cooldown across overlapping tasks (`monitor.rs:213-237`) — single-replica today; harmless. Reconsider when adding a second instance.
|