| 1 |
1 |
|
# MNW Server — Todo
|
| 2 |
2 |
|
|
| 3 |
|
- |
Done: Ultra Fuzz Runs #1-#4 fully closed (all phases, the 41-test integration triage, and the third-party credits page at /docs/credits linked from the footer). Active: no Run #4 work remaining.
|
|
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 annual prices in `tiers.md` (e.g. `$54/yr`, `$108/yr`, `$162/yr`, `$324/yr`). 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 |
+ |
---
|
| 4 |
301 |
|
|
| 5 |
302 |
|
## Ultra Fuzz 2026-05-26 (Run #4)
|
| 6 |
303 |
|
|