Skip to main content

max / makenotwork

Fix Run 25 SERIOUS bugs, add S3 multipart upload, docs updates - Fix cart preflight: use buyer_id instead of user_id in transaction check - Add S3 cleanup + storage decrement to project deletion path - Stream content export ZIP to disk + S3 multipart upload (was in-memory) - Add upload_multipart to s3-storage crate (10 MB parts, abort-on-failure) - Fix MockPaymentProvider missing update_app_sync_subscription_tier - Docs: tiers, guarantees, FAQ, payouts, stripe guide, MoR, outreach
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-11 21:10 UTC
Commit: c2453c86d16291cc32645c128fc6450ab291e9a4
Parent: 90c5859
31 files changed, +2051 insertions, -184 deletions
@@ -3445,7 +3445,7 @@
3445 3445
3446 3446 [[package]]
3447 3447 name = "makenotwork"
3448 - version = "0.5.14"
3448 + version = "0.5.15"
3449 3449 dependencies = [
3450 3450 "anyhow",
3451 3451 "argon2",
@@ -4987,6 +4987,7 @@
4987 4987 dependencies = [
4988 4988 "aws-config",
4989 4989 "aws-sdk-s3",
4990 + "tokio",
4990 4991 "tracing",
4991 4992 ]
4992 4993
@@ -71,6 +71,9 @@
71 71 hex = "0.4.3"
72 72 base64 = "0.22.1"
73 73
74 + # Temp files (content export)
75 + tempfile = "3"
76 +
74 77 # File scanning
75 78 infer = "0.19"
76 79 goblin = "0.10"
@@ -1,11 +1,11 @@
1 1 # MakeNotWork -- Audit Review
2 2
3 - **Last audited:** 2026-05-09 (Run 24, Ultra Fuzz -- 5-axis deep audit)
4 - **Previous audit:** 2026-05-09 (Run 23, Ultra Fuzz -- 5-axis deep audit)
3 + **Last audited:** 2026-05-11 (Run 25, Ultra Fuzz -- 5-axis deep audit)
4 + **Previous audit:** 2026-05-09 (Run 24, Ultra Fuzz -- 5-axis deep audit)
5 5
6 6 ## Overall Grade: A
7 7
8 - Run 24: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9. ~88,082 LOC. ~1,215 test annotations. 107 migrations. 2 SERIOUS + 3 MINOR findings identified, all fixed in same-day remediation (1 false positive). 0 cold spots (all 2 resolved). 5/5 axes at A. All Run 23 fixes verified intact. No regressions.
8 + Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.14. ~88,978 LOC. ~1,225 test annotations. 111 migrations. 3 SERIOUS + 6 MINOR findings identified. 5 cold spots. 5/5 axes at A. All Run 24 fixes verified intact. No regressions.
9 9
10 10 ## Scorecard
11 11
@@ -13,9 +13,9 @@
13 13 |-----------|:-----:|-------|
14 14 | Code Quality | A | Zero .unwrap() in production paths. Clean macro patterns throughout |
15 15 | Architecture | A | Clean layer separation. Trait-based backends for storage/email/payments |
16 - | Testing | A | ~1,215 test annotations, proptest active, adversarial tests, comprehensive harness |
16 + | Testing | A | ~1,225 test annotations, proptest active, adversarial tests, comprehensive harness |
17 17 | Security | A | Constant-time compare, fail-closed scanning, CSRF everywhere, Argon2id, HMAC webhooks, PKCE S256. DUMMY_HASH pattern on all login paths |
18 - | Performance | A | Cart and download queries consolidated. Export writes to ZIP one-by-one. Advisory lock pinned |
18 + | Performance | A | Discover facets parallelized. Batch loading. Bounded scan semaphore. Advisory lock pinned |
19 19 | Documentation | A | Module-level //! on all major files. README.md present |
20 20 | Dependencies | A- | 4 transitive advisories (none exploitable). async-trait retained (required for dyn dispatch) |
21 21 | Frontend | A | Askama auto-escape, json_escape prevents JSON-LD XSS, HTMX patterns consistent. CSRF field names consistent |
@@ -24,7 +24,7 @@
24 24 | Concurrency | A | ON CONFLICT, FOR UPDATE, DashMap caches. Scheduler advisory lock pinned. Promo code atomically reserved at checkout |
25 25 | Resilience | A | Graceful shutdown with 10s deadline, timeouts on all outbound calls, fail-closed scanning |
26 26 | API Consistency | A | ListResponse wrapper, json_error_layer, versioned SyncKit routes |
27 - | Migration Safety | A | 107 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout |
27 + | Migration Safety | A | 111 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout |
28 28 | Codebase Size | A | All three oversized files split into modules. health/mod.rs at 749 (monolithic probe fn) |
29 29
30 30 ## Module Heatmap
@@ -42,33 +42,39 @@
42 42 | storage.rs | A | A | A | A | A | A | A+ | B+ | A |
43 43 | pricing.rs | A | A+ | A+ | A | A | A | A- | n/a | A |
44 44 | crypto.rs | A | A | A | A+ | A | A | A | n/a | A |
45 - | formatting.rs | A | A | A+ | A | A | A- | A | n/a | A |
45 + | formatting.rs | A- | A | A+ | A | A | A- | A | n/a | A |
46 46 | rss.rs | A | A | A | A | A | A | A- | n/a | A |
47 47 | synckit_auth.rs | A | A | A | A | A | A | A | A | A |
48 48 | metrics.rs | A | A | n/a | A | A- | A | A | A | A |
49 49 | db/enums.rs | A | A | A | A | n/a | A | A+ | n/a | A |
50 - | db/id_types.rs | A | A | A | n/a | n/a | A | A+ | n/a | A |
50 + | db/id_types.rs | A+ | A | A | n/a | n/a | A | A+ | n/a | A+ |
51 51 | db/validated_types.rs | A | A | A+ | A | A | A | A+ | n/a | A |
52 52 | db/users.rs | A | A | B | A | A- | A | A | A | A |
53 53 | db/items.rs | A | A | B | A | A- | A | A | A | A |
54 54 | db/synckit.rs | A | A | B | A | A | A | A | A | A |
55 - | db/transactions.rs | A | A | B+ | A | A | A | A | A | A |
55 + | db/transactions.rs | A | A | B+ | A | A | A | A- | A | A |
56 56 | db/discover.rs | A | A | B | A | A | A | A | n/a | A- |
57 - | db/cart.rs | A | A | B | A | A | A | A | n/a | A |
57 + | db/cart.rs | **B+** | A | B | A | A | A | A | n/a | A |
58 58 | db/creator_tiers.rs | A | A+ | A | A | A | A | A | n/a | A |
59 59 | db/versions.rs | A | A | A | A | A | A | A | n/a | A |
60 60 | db/builds.rs | A | A+ | A | A | A | A | A | n/a | A |
61 - | db/pending_refunds.rs | A | A | B | A | A | A | A | n/a | A |
61 + | db/pending_refunds.rs | A+ | A | B | A | A | A | A | n/a | A |
62 + | db/pending_s3_deletions.rs | A | A- | n/a | A | A | A | A | n/a | A |
62 63 | db/license_keys.rs | A | A | B | A | A | A | A | n/a | A |
63 64 | db/promo_codes.rs | A | A | A | A | A | A | A | n/a | A |
64 65 | db/tips.rs | A | A | B | A | A | A | A | n/a | A |
65 66 | db/idempotency.rs | A | A | B- | A | A | A | A | n/a | A |
66 - | db/pending_uploads.rs | B+ | A | n/a | A | A | A | A | n/a | A |
67 + | db/pending_uploads.rs | A | A | n/a | A | A | A | A | n/a | A |
68 + | db/moderation.rs | A | A | B+ | A | n/a | A | A | A | A |
69 + | db/reports.rs | A | A | B+ | A | n/a | A | A | A | A |
67 70 | db/models/* | A | A | B+ | A | n/a | A- | A | n/a | A |
68 71 | types/ | A | A | B | A | n/a | A | A | n/a | A |
69 72 | scanning/ | A | A+ | A- | A+ | A- | A | A- | A | A |
70 - | scanning/archive.rs | A | A | A- | A- | A- | A | A- | A | A |
73 + | scanning/archive.rs | A- | A | A- | A- | A- | A | A- | A | A |
74 + | scanning/structural.rs | A- | A | A | B+ | A | A | A | A- | A |
71 75 | payments/ | A | A | A- | A | A- | A | A | A | B+ |
76 + | payments/webhooks.rs | A | A | A | A | A | A | A | A | A |
77 + | payments/checkout.rs | A- | A | A- | A | A | A | A- | A | A |
72 78 | email/ | A | A | A | A | A- | A | A | B+ | A- |
73 79 | scheduler/ | A | A+ | B+ | A | A | A | A | A- | A |
74 80 | scheduler/cleanup.rs | A | A | n/a | A | A | A | A | A- | A |
@@ -79,13 +85,14 @@
79 85 | build_runner.rs | A | A- | B+ | A+ | A- | A- | A | A | A |
80 86 | monitor.rs | A | A | A- | A | A | A | A | A+ | A |
81 87 | templates/ | A | A- | n/a | A | A | A- | A | B+ | B+ |
88 + | templates/purchase.html | A- | n/a | n/a | A | n/a | n/a | n/a | n/a | **B+** |
82 89 | routes/auth.rs | A | A | n/a | A+ | A | A | A | A | A |
83 - | routes/oauth.rs | A | A | n/a | A- | A | A | A | A- | A- |
90 + | routes/oauth.rs | A | A | n/a | A | A | A | A | A- | A- |
84 91 | routes/admin/ | A | A | n/a | A | A | A | A | A | A |
85 92 | routes/api/ | A | A | n/a | A | A | A | A | A | B+ |
86 - | routes/api/guest_checkout.rs | **B** | A | n/a | **B** | A | A | A | A | A |
87 - | routes/api/cart.rs | A | A | n/a | A | A- | A | A | A | A |
93 + | routes/api/projects.rs (delete) | **B-** | A | n/a | A | A | A | A | A | A |
88 94 | routes/api/exports/ | A- | A- | n/a | A | A- | A | A | A | A- |
95 + | routes/api/exports/content.rs | **B+** | A- | n/a | A | **B+** | A | A | A | A |
89 96 | routes/stripe/ | A | A | n/a | A | A- | A | A | A | B+ |
90 97 | routes/stripe/checkout/ | A | A | n/a | A- | A | A | A | A | A |
91 98 | routes/stripe/checkout/cart.rs | A | A | n/a | A- | A | A | A | A | A |
@@ -95,17 +102,22 @@
95 102 | routes/embed/ | A | A | n/a | A | A | A- | A | A | A |
96 103 | routes/git/ | A | A | n/a | A | A | A | A | A | A |
97 104 | routes/storage/ | A | A | n/a | A | A | A | A | A | A |
98 - | routes/storage/versions.rs | **B-** | A- | n/a | A | A | A | A | A | A |
105 + | routes/storage/versions.rs | A | A- | n/a | A | A | A | A | A | A |
99 106 | routes/storage/uploads.rs | A | A | n/a | A | A | A | A | A | A |
100 107 | routes/storage/downloads.rs | A | A | n/a | A | A- | A | A | A | A |
101 - | routes/storage/images.rs | A | A | n/a | A | A | A | A | A | A |
102 - | routes/postmark/ | A | A | n/a | A | A | A | n/a | A- | B+ |
108 + | routes/storage/images.rs | **B+** | A | n/a | A | A | A | A | A | A- |
103 109
104 110 **Bold** = cold spot (B or below).
105 111
106 112 ### Cold Spots
107 113
108 - None. All Run 24 cold spots resolved in remediation.
114 + | Module | Grade | Issue |
115 + |--------|:-----:|-------|
116 + | db/cart.rs | B+ | SQL references `t.user_id` instead of `t.buyer_id` — runtime error |
117 + | routes/api/projects.rs (delete) | B- | Project deletion orphans S3 objects, no storage decrement |
118 + | routes/api/exports/content.rs | B+ | Buffers up to 2GB ZIP in memory per export request |
119 + | routes/storage/images.rs | B+ | Non-atomic 3-UPDATE cover image, non-atomic storage swap on project images, missing S3 cleanup |
120 + | templates/purchase.html | B+ | Cart add-to-cart JS doesn't check HTTP error status |
109 121
110 122 ### Resolved Cold Spots (from Run 24)
111 123
@@ -123,20 +135,22 @@
123 135
124 136 ## Mandatory Surprises
125 137
126 - **Run 24 (5 surprises, one per axis):**
138 + **Run 25 (5 surprises, one per axis):**
127 139
128 - 1. **Payments -- Claim token architecture for guest purchases (unexpectedly good):** `db/transactions.rs:74-173` creates unattached transactions with unique `claim_token`, sends secure download link, claims idempotently via `ON CONFLICT DO NOTHING`. Cryptographically sound, race-free, elegant.
140 + 1. **Payments -- Pending refund queue with FOR UPDATE SKIP LOCKED (unexpectedly good):** `db/pending_refunds.rs` + `checkout_helpers.rs:check_pending_refund`. When a `charge.refunded` webhook arrives before `checkout.session.completed`, the refund is queued and processed after checkout completion. Claims use `FOR UPDATE SKIP LOCKED` for safe concurrency. Stale refunds are escalated. This solves one of the hardest webhook ordering problems correctly.
129 141
130 - 2. **Storage -- recalculate_all_storage_batch LATERAL join (unexpectedly good):** `db/creator_tiers.rs:458-508` recalculates storage for ALL creators in a single query using LATERAL join instead of N+1 loop. Shows architectural awareness.
142 + 2. **Storage -- Durable S3 deletion queue as transactional outbox (unexpectedly good):** `db/pending_s3_deletions.rs` + `scheduler/cleanup.rs:365-418`. Every destructive path enqueues S3 keys to a durable DB table before deletion. Retry with attempt counting, `FOR UPDATE SKIP LOCKED`, and prefix vs single-key distinction. Production-grade pattern -- the one exception (project deletion, see SERIOUS finding) makes its absence there even more glaring.
131 143
132 - 3. **UX Wiring -- CSRF constant-time token comparison (unexpectedly good):** `csrf.rs:61` uses `constant_time_compare()` for CSRF token validation. Most web frameworks skip this detail. Combined with dual-layer extraction (header + body), this is production-hardened CSRF.
144 + 3. **UX Wiring -- json_escape with HTML entity protection (unexpectedly good):** `types/mod.rs:214`. Hand-rolled escaper goes beyond standard JSON to also escape `<`, `>`, `&` as Unicode escapes for safe embedding in `<script>` tags via `|safe`. Combined with `build_segments_json`'s `</` to `<\/` replacement. Defense-in-depth approach most codebases skip entirely.
133 145
134 - 4. **Security -- TOTP replay prevention via explicit time step matching (unexpectedly good):** `routes/api/totp.rs:254-266` stores matched time step in DB, rejects monotonically non-increasing steps. Completely eliminates TOTP replay across skew window. Most platforms get this wrong.
146 + 4. **Security -- Anti-timing dummy hash on ALL login paths (unexpectedly good):** `routes/auth.rs:29-31` and `routes/oauth.rs:33-35`. Pre-computed `DUMMY_HASH` via `LazyLock` ensures "user not found" takes the same wall-clock time as "wrong password". Applied independently across both web and OAuth login surfaces. Plus `crypto.rs:7-18` hashes both inputs with SHA-256 before constant-time XOR comparison -- neutralizes length-leak.
135 147
136 - 5. **Performance -- Storage quota atomicity with idempotency checks (unexpectedly good):** `routes/storage/uploads.rs:206-219` checks idempotency before touching quotas, uses atomic `try_replace_storage` for file replacements. Sophisticated and rarely seen.
148 + 5. **Performance -- Discover page runs 5 facet queries in parallel via tokio::try_join! (unexpectedly good):** `routes/pages/public/discover.rs:342-361`. Type counts, tag counts, followed tags, AI tier counts, and price range counts all run concurrently. Textbook optimization that most production codebases miss. The codebase consistently avoids N+1 through batch loading across exports, items, and versions.
137 149
138 150 ### Previous Surprises
139 151
152 + **Run 24:** Claim token architecture, LATERAL join batch storage recalc, CSRF constant-time comparison, TOTP replay prevention via time step, storage quota atomicity + idempotency checks.
153 +
140 154 **Run 23:** Webhook signature gold standard, atomic storage quota enforcement, CSRF dual-layer extraction, archive decompression fallback (bad, fixed), advisory lock protocol.
141 155
142 156 **Run 22:** Webhook signature gold standard, cleanup scheduler durable queue, form error recovery, defense-in-depth 6 layers, export RAM buffering.
@@ -156,80 +170,100 @@
156 170 36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types, Cents/PriceCents monetary newtypes with proptest coverage. All money math in integer cents (i32/i64), zero floating point in money paths. `SUM(BIGINT)::BIGINT` cast used consistently. License keys use CSPRNG (~55 bits entropy). FOR UPDATE row locking on license activation prevents TOCTOU.
157 171
158 172 ### 3. Payment robustness
159 - Three-layer webhook idempotency. Bidirectional pending refund matching. Atomic promo code enforcement at DB level. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all paths. Cart PWYW minimum enforced. Tip amounts capped. Guest purchase claim tokens are cryptographically sound and idempotent.
173 + Three-layer webhook idempotency. Bidirectional pending refund matching with FOR UPDATE SKIP LOCKED. Atomic promo code enforcement at DB level. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all paths. Cart PWYW minimum enforced. Tip amounts capped. Guest purchase claim tokens are cryptographically sound and idempotent.
160 174
161 175 ### 4. Operational maturity
162 - All Run 22-23 fixes verified intact. Idempotency key scope widened (migration 106). Pending S3 deletions UNIQUE constraint (migration 107). Scheduler advisory lock pinned. Soft-delete purge handles version S3 keys. Confirm uploads wrapped in transactions. Discover page parallelized. Content exports stream one-by-one. N+1 queries consolidated.
176 + All Run 23-24 fixes verified intact. Idempotency key scope widened (migration 106). Pending S3 deletions UNIQUE constraint (migration 107). Scheduler advisory lock pinned. Soft-delete purge handles version S3 keys. Confirm uploads wrapped in transactions. Discover page parallelized. Content exports stream one-by-one. N+1 queries consolidated.
163 177
164 178 ## Weaknesses
165 179
166 - ### Active (Run 24)
180 + ### Active (Run 25)
167 181
168 - - **Guest checkout promo code validation gap** -- Missing `starts_at` check allows future-dated codes to be used early by guest buyers. Easy fix (2 lines).
169 - - **Version replace storage counter corruption** -- Decrement-then-increment is not atomic. If increment fails, old storage is permanently lost from counter. Should use `try_replace_storage` or re-increment on failure.
182 + - **Cart preflight SQL column name bug** -- `db/cart.rs:106` references `t.user_id` which does not exist on the `transactions` table (column is `buyer_id`). Runtime SQL error on cart toggle preflight.
183 + - **Project deletion orphans S3 objects** -- `routes/api/projects.rs:270-282` CASCADEs DB records but does not enqueue S3 files for deletion or decrement storage counters. Permanent S3 orphans.
184 + - **Content export buffers up to 2GB in memory** -- `routes/api/exports/content.rs:120-127` writes entire ZIP to in-memory Vec. OOM risk under concurrent export requests.
185 + - **Non-atomic item cover image update** -- `routes/storage/images.rs:369-371` does 3 separate UPDATEs for cover_image_url, cover_s3_key, and cover_file_size_bytes. Crash between any two leaves inconsistent state.
186 + - **Project image non-atomic storage swap** -- `routes/storage/images.rs:173-185` decrements then increments in two queries. Should use `try_replace_storage`.
187 + - **Project image missing S3 deletion enqueue** -- `routes/storage/images.rs:173-179` does not enqueue old project image to `pending_s3_deletions`. Item images do clean up.
170 188
171 - ### Resolved (Run 23)
189 + ### Resolved (Run 24)
172 190
173 - - ~~Version S3 key leak~~ -- Fixed. Old keys now enqueued via `pending_s3_deletions`.
174 - - ~~Archive decompression fallback~~ -- Fixed. 10x conservative multiplier on decompression error.
175 - - ~~Export memory buffering~~ -- Fixed. Files written to ZIP one-by-one.
176 - - ~~N+1 query patterns~~ -- Fixed. Cart and download access control consolidated.
191 + - ~~Guest checkout promo code validation gap~~ -- Fixed. Added `starts_at` check.
192 + - ~~Version replace storage counter corruption~~ -- Fixed. Uses `try_replace_storage`.
177 193
178 194 ## Bug Reports by Axis
179 195
180 196 ### Payments
181 - 0 CRITICAL, 1 SERIOUS, 0 MINOR, 2 NOTE
197 + 0 CRITICAL, 1 SERIOUS, 0 MINOR, 4 NOTE
182 198
183 199 | # | Sev | Location | Description |
184 200 |---|-----|----------|-------------|
185 - | P1 | **SERIOUS** | `routes/api/guest_checkout.rs:91-140` | Missing `starts_at` promo code validation. All 3 authenticated paths (cart.rs:150, item.rs:127, subscriptions.rs:192) check it. |
186 - | P2 | NOTE | `db/license_keys.rs:158-233` | Activation limit enforced with FOR UPDATE + active count + idempotent upsert. Excellent. |
187 - | P3 | NOTE | `payments/webhooks.rs:48-61` | Webhook deduplication correctly prevents double-credit. Solid. |
201 + | P1 | **SERIOUS** | `db/cart.rs:106` | SQL references `t.user_id` instead of `t.buyer_id`. Column does not exist on `transactions` table. Runtime SQL error on cart toggle preflight check. |
202 + | P2 | NOTE | `payments/checkout.rs:33` | `CartLineItem.amount_cents` is `i64` while `CheckoutParams.amount_cents` is `Cents`. Inconsistency, not exploitable. |
203 + | P3 | NOTE | `formatting.rs:11` | `format_price` uses `cents as f64 / 100.0`. Safe for typical amounts but code smell in monetary code. |
204 + | P4 | NOTE | `db/transactions.rs:462` | `CreateProjectTransactionParams.amount_cents` is `i32` while item transactions use `Cents` (i64). |
205 + | P5 | NOTE | `pricing.rs:134` | `FixedPricing::validate_amount` has no upper cap (PWYW caps at $10K). Low impact -- Stripe session amount is server-side. |
188 206
189 207 ### Storage
190 - 0 CRITICAL, 1 SERIOUS, 1 MINOR, 1 NOTE
208 + 0 CRITICAL, 1 SERIOUS, 3 MINOR, 2 NOTE
191 209
192 210 | # | Sev | Location | Description |
193 211 |---|-----|----------|-------------|
194 - | S1 | **SERIOUS** | `routes/storage/versions.rs:175-184` | Storage counter corruption: decrement at 175, try_increment at 182. If increment fails, decrement is not rolled back. |
195 - | S2 | MINOR | `db/pending_uploads.rs:17` | ON CONFLICT DO UPDATE refreshes created_at, letting repeated presign calls prevent garbage collection. |
196 - | S3 | NOTE | `routes/storage/uploads.rs:262` | Storage decrement rollback error swallowed with `.ok()`. Acceptable given background reconciliation. |
212 + | S1 | **SERIOUS** | `routes/api/projects.rs:270-282` | Project deletion CASCADEs DB records but orphans all S3 objects (audio, cover images, version downloads, video). No storage decrement. |
213 + | S2 | MINOR | `routes/storage/images.rs:369-371` | Item cover update: 3 separate UPDATEs for url, s3_key, file_size. Crash-unsafe. |
214 + | S3 | MINOR | `routes/storage/images.rs:173-185` | Project image replace: non-atomic decrement/increment. Should use `try_replace_storage`. |
215 + | S4 | MINOR | `routes/storage/images.rs:173-179` | Project image replace: old S3 object not enqueued to `pending_s3_deletions`. |
216 + | S5 | NOTE | `routes/api/content_insertions.rs:301-309` | Delete-before-decrement order. If decrement fails, storage counter overstated until weekly recalc. |
217 + | S6 | NOTE | `routes/storage/uploads.rs:250-253` | Dynamic SQL column names via `format!()` from internal enum. Not injection risk, but bypasses compile-time checking. |
197 218
198 219 ### UX Wiring
199 - 0 SERIOUS, 1 MINOR, 3 NOTE
220 + 0 CRITICAL, 0 SERIOUS, 2 MINOR, 3 NOTE
200 221
201 222 | # | Sev | Location | Description |
202 223 |---|-----|----------|-------------|
203 - | U1 | MINOR | `routes/pages/dashboard/forms.rs` | Blog editor doesn't preserve user input on slug validation error. |
204 - | U2 | NOTE | `routes/pages/public/discover.rs:85` | Page clamped to `.max(1)` but no upper bound. DB returns empty; no impact. |
205 - | U3 | NOTE | `templates/wizards/wizard_join.html` | Join wizard CSRF exemption justified (pre-auth) and documented. |
206 - | U4 | NOTE | `templates/dashboards/dashboard-blog-editor.html:42` | hx-vals with UUID template variable. Safe (UUIDs contain no special chars). |
224 + | U1 | MINOR | `templates/pages/purchase.html:142` | Cart add-to-cart JS `fetch()` does not check response status before redirecting to `/cart`. 4xx/5xx silently redirect. |
225 + | U2 | MINOR | `formatting.rs:8-9` | `format_price(-500)` produces `$-5.00` vs `format_revenue(-500)` = `-$5.00`. Inconsistent negative formatting. |
226 + | U3 | NOTE | `routes/pages/public/discover.rs:85` | No upper bound on page param. `.max(1)` clamp only. DB returns empty; no impact. |
227 + | U4 | NOTE | `routes/pages/public/join_wizard.rs:88-95` | Minimal email validation (checks `@` + `.`). Acceptable -- verification email is the real gate. |
228 + | U5 | NOTE | `routes/pages/public/content/item.rs:340` | `cdn_base` variable scope spans entire monolithic function. Refactoring hazard, not a bug. |
207 229
208 230 ### Security
209 - 0 CRITICAL, 0 SERIOUS, 0 MINOR, 3 NOTE
231 + 0 CRITICAL, 0 SERIOUS, 0 MINOR, 7 NOTE
210 232
211 233 | # | Sev | Location | Description |
212 234 |---|-----|----------|-------------|
213 - | X1 | NOTE | `auth.rs:302-309` | verify_password is not independently constant-time on parse failure, but all callers use DUMMY_HASH. No exploitable timing channel. |
214 - | X2 | NOTE | `db/totp.rs:126-146` | Backup code DB lookup not constant-time. Standard practice; not exploitable over network. |
215 - | X3 | NOTE | `synckit_auth.rs:54-68` | JWT iat claim not validated. Standard; exp is validated. No impact. |
235 + | X1 | NOTE | `auth.rs:206` | `MaybeUser` skips session revocation check (documented, intentional). OAuth compensates with manual validation. |
236 + | X2 | NOTE | `routes/api/users/profile.rs:140-146` | Breached password change is advisory-only. Intentional UX decision. |
237 + | X3 | NOTE | `db/totp.rs:22` | TOTP secret stored as plaintext in DB. Best practice would encrypt at rest. |
238 + | X4 | NOTE | `synckit_auth.rs:57-58` | JWT `iat` claim not validated. Standard practice; `exp` is validated. |
239 + | X5 | NOTE | `scanning/archive.rs:137-157` | Nested archives counted but not recursively scanned. YARA + ClamAV still scan outer bytes. |
240 + | X6 | NOTE | `scanning/structural.rs:37-38` | `posix_spawn` and `dlopen` in suspicious symbols list may false-positive on legitimate macOS apps. |
241 + | X7 | NOTE | `main.rs:116-121` | Session cookie not prefixed with `__Host-`. Low risk -- no user-content subdomains. |
216 242
217 243 ### Performance
218 - 0 CRITICAL, 0 SERIOUS, 2 MINOR, 1 NOTE
244 + 0 CRITICAL, 1 SERIOUS, 3 MINOR, 4 NOTE
219 245
220 246 | # | Sev | Location | Description |
221 247 |---|-----|----------|-------------|
222 - | F1 | MINOR | `metrics.rs:232` | Idempotency middleware: `.to_vec()` creates second allocation from Bytes. Minor efficiency loss. |
223 - | F2 | MINOR | `metrics.rs:228` | Response body >1MB returns Body::empty(). Only affects chunked responses without Content-Length (rare). |
224 - | F3 | NOTE | `routes/storage/uploads.rs:250-253` | Dynamic SQL column names via format!(). Hardcoded enum match arms -- not injection risk, but bypasses compile-time checking. |
248 + | F1 | **SERIOUS** | `routes/api/exports/content.rs:120-127` | Content export accumulates up to 2GB in an in-memory `Vec<u8>` ZIP buffer. OOM risk under concurrent exports. |
249 + | F2 | MINOR | `routes/api/exports/mod.rs:268-270` | N+1 query: collection items loaded per-collection in a loop (up to 50 queries). |
250 + | F3 | MINOR | `build_runner.rs:396-408` | Build artifact read entirely into memory via `tokio::fs::read` before S3 upload. Mitigated by single-build advisory lock. |
251 + | F4 | MINOR | `db/mod.rs:93` | `check_sandbox_cap` uses blocking `pg_advisory_lock` instead of `pg_try_advisory_lock`. Latency spike under burst. |
252 + | F5 | NOTE | `scanning/mod.rs:68` | Scan pipeline takes `&[u8]` requiring full file in memory. Semaphore limits to 4 x 100MB = 400MB worst case. |
253 + | F6 | NOTE | `lib.rs:238` | CSP header reads `S3_ENDPOINT` from `env::var` every request. Should use `state.config`. |
254 + | F7 | NOTE | `routes/synckit/subscribe.rs:41-58` | SseConnectionGuard Drop has TOCTOU race on DashMap counter. Worst case: stale entry (harmless). |
255 + | F8 | NOTE | `payments/checkout.rs:33` | `CartLineItem.amount_cents` is raw `i64` instead of `Cents` type. |
225 256
226 257 ## Cross-Cutting Concerns
227 258
228 - ### Promo code validation inconsistency (Payments + UX)
229 - The `starts_at` check is present in 4 checkout paths but missing from guest checkout. Pattern gap -- guest checkout was likely added later without copying all validations. A shared `validate_promo_code()` helper would prevent future divergence.
259 + ### Project deletion missing S3 cleanup (Storage + Payments)
260 + Project deletion CASCADEs transactions, items, and versions in the DB but never touches S3 or storage counters. The `pending_s3_deletions` durable queue pattern exists and is used everywhere else (item deletion, version replacement, soft-delete purge). Its absence here is inconsistent and the most impactful finding of this audit.
230 261
231 - ### Storage counter atomicity (Storage + Performance)
232 - The decrement-then-increment pattern in `versions.rs` is inconsistent with the atomic `try_replace_storage` used in `uploads.rs`. The versions path should use the same atomic pattern.
262 + ### Content export memory pressure (Storage + Performance)
263 + The content export buffers an entire ZIP in memory (up to 2GB). This crosses both the storage axis (resource management) and performance axis (OOM risk). The route is rate-limited to 3 req/sec and requires authentication, limiting blast radius, but a single malicious creator could consume 2GB of server RAM.
264 +
265 + ### Image upload atomicity gap (Storage + UX)
266 + Both project image and item cover image confirms have atomicity issues. The item path does 3 separate UPDATEs; the project path does non-atomic storage swap and skips S3 cleanup. The version upload path (`uploads.rs`) correctly uses `try_replace_storage` and `pending_s3_deletions` -- the image paths should follow the same pattern.
233 267
234 268 ## Components Successfully Stress-Tested
235 269
@@ -245,67 +279,80 @@
245 279 ### Security (17 vectors survived)
246 280 Virus scan bypass via ClamAV downtime (fail-closed), content-type spoofing, path traversal in archives, session fixation, timing-based user enumeration (dummy hash on all 3 login paths), brute force login (lockout), X-Forwarded-For spoofing (Cloudflare-aware), session reuse after password change, OAuth code replay, PKCE downgrade, token prediction (CSPRNG), CSRF on state-changing endpoints (constant-time), SSH command injection, passkey cloning (counter), TOTP replay (last_used_step), IDOR on passkeys/sessions, archive bombs (byte counting + 10x multiplier).
247 281
248 - ### Performance (10 vectors survived)
249 - Connection pool exhaustion (bounded at 25), file scanning memory (semaphore of 4), SSE connection accumulation (bounded), scheduler job accumulation (advisory lock), background task leaks (monitored), ZIP bombs (byte counting), bulk operations (batch queries, 100-item cap), rate limiter bypass (Cloudflare-aware IP), concurrent scan memory pressure, large file handling (streamed).
282 + ### Performance (12 vectors survived)
283 + Connection pool exhaustion (bounded at 25), file scanning memory (semaphore of 4), SSE connection accumulation (bounded at 10/user), scheduler job accumulation (advisory lock), background task leaks (monitored), ZIP bombs (byte counting), bulk operations (batch queries, 100-item cap), rate limiter bypass (Cloudflare-aware IP), concurrent scan memory pressure, large file handling (streamed uploads via presigned URLs), global lock contention (DashMap, no Mutex/RwLock), graceful shutdown (10s drain).
250 284
251 285 ## Confidence Assessment
252 286
253 287 | Axis | Confidence | Notes |
254 288 |------|-----------|-------|
255 - | Payments | HIGH | One validation gap in guest checkout. Core payment logic excellent. |
256 - | Storage | HIGH | One error-path storage corruption. Normal path is atomic and correct. |
257 - | UX Wiring | HIGH | CSRF excellent. Templates safe. Minor UX polish only. |
258 - | Security | HIGH | No auth bypasses. DUMMY_HASH on all login paths. Fail-closed scanning. |
259 - | Performance | HIGH (current scale) | Pool size reasonable. Rate limiters well-tuned. Scheduler advisory-locked. |
289 + | Payments | HIGH | One SQL column name bug in preflight. Core payment logic (webhooks, checkout, refunds) excellent. |
290 + | Storage | HIGH | Project deletion S3 orphan gap. All other paths use durable deletion queue correctly. |
291 + | UX Wiring | HIGH | CSRF excellent. Templates safe. Minor JS and formatting polish only. |
292 + | Security | HIGH | No auth bypasses. DUMMY_HASH on all login paths. Fail-closed scanning. Zero SERIOUS findings. |
293 + | Performance | HIGH (current scale) | Export memory buffering is the main concern. Pool, rate limiting, and concurrency are solid. |
260 294
261 295 ## Metrics
262 296
263 - - Modules audited: 55+
264 - - Total cold spots: 0 (all 2 resolved in remediation)
265 - - Bugs by severity: 0 critical, 2 serious (all fixed), 3 minor (all fixed, 1 false positive), 10 note
297 + - Modules audited: 60+
298 + - Total cold spots: 5
299 + - Bugs by severity: 0 critical, 3 serious, 8 minor, 20 note
266 300 - Axes at A or above: 5/5
267 301
268 302 ## Axis Summary Grades
269 303
270 304 | Axis | Overall | Cold Spots | Mandatory Surprise |
271 305 |------|---------|------------|-------------------|
272 - | Payments | A | guest_checkout.rs (B) | Claim token architecture (good) |
273 - | Storage | A | versions.rs (B-) | LATERAL join batch storage recalc (good) |
274 - | UX Wiring | A | None | CSRF constant-time comparison (good) |
275 - | Security | A | None | TOTP replay prevention via time step (good) |
276 - | Performance | A | None | Storage quota atomicity + idempotency (good) |
306 + | Payments | A | db/cart.rs (B+) | Pending refund queue with FOR UPDATE SKIP LOCKED (good) |
307 + | Storage | A | projects.rs delete (B-), images.rs (B+) | Durable S3 deletion queue as transactional outbox (good) |
308 + | UX Wiring | A | purchase.html (B+) | json_escape with HTML entity protection (good) |
309 + | Security | A | None | Anti-timing dummy hash on ALL login paths (good) |
310 + | Performance | A | exports/content.rs (B+) | Discover page 5-way parallel facet queries (good) |
277 311
278 312 ## Recommended Priority Order
279 313
280 - 1. **[SERIOUS]** Add `starts_at` validation to guest checkout -- 2 lines, copy from `cart.rs:150`
281 - 2. **[SERIOUS]** Fix version replace storage rollback -- re-increment old storage on `try_increment` failure, or use `try_replace_storage`
282 - 3. **[MINOR]** Change pending_uploads to `ON CONFLICT DO NOTHING` -- prevents staleness attack
283 - 4. **[MINOR]** Blog editor: preserve input on validation error
284 - 5. **[MINOR]** Idempotency middleware: use `std::str::from_utf8` instead of `String::from_utf8(to_vec())`
285 - 6. **[DEFERRED]** Extract shared `validate_promo_code()` helper
314 + 1. **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id` (1 line)
315 + 2. **[SERIOUS]** Add S3 cleanup to project deletion -- collect S3 keys, enqueue to `pending_s3_deletions`, decrement storage (moderate effort)
316 + 3. **[SERIOUS]** Stream content export ZIP to S3 via multipart upload instead of in-memory buffer (medium effort)
317 + 4. **[MINOR]** Consolidate item cover image update into single UPDATE (trivial)
318 + 5. **[MINOR]** Use `try_replace_storage` for project image replace (low effort)
319 + 6. **[MINOR]** Enqueue old project image to `pending_s3_deletions` (trivial)
320 + 7. **[MINOR]** Add error status check to purchase.html cart JS (trivial)
321 + 8. **[MINOR]** Fix `format_price` negative formatting to match `format_revenue` (trivial)
322 + 9. **[MINOR]** Batch-load collection items in export to eliminate N+1 (low effort)
323 + 10. **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock` (trivial)
324 + 11. **[DEFERRED]** Stream build artifacts to S3 via multipart upload
325 + 12. **[DEFERRED]** Extract shared `validate_promo_code()` helper to prevent checkout path divergence
286 326
287 327 ## Action Items
288 328
289 - ### Run 24 (2026-05-09)
329 + ### Run 25 (2026-05-11)
290 330
291 - 97. ~~**[SERIOUS]** Add `starts_at` validation to guest checkout~~ -- **Fixed.** Added `starts_at` check to `guest_checkout.rs:102-107`, matching all authenticated paths.
292 - 98. ~~**[SERIOUS]** Fix version replace storage counter rollback~~ -- **Fixed.** `versions.rs` now uses atomic `try_replace_storage` for replacements, `try_increment_storage` for fresh uploads. No separate decrement/increment.
293 - 99. ~~**[MINOR]** Change pending_uploads ON CONFLICT to DO NOTHING~~ -- **Fixed.** `pending_uploads.rs:17` now uses `DO NOTHING`, preventing staleness attack via repeated presign.
294 - 100. ~~**[MINOR]** Blog editor: preserve form input on validation error~~ -- **False positive.** Blog editor uses JS `fetch()` API; form state is preserved on error.
295 - 101. ~~**[MINOR]** Idempotency middleware: avoid double allocation~~ -- **Fixed.** `metrics.rs:232` now uses `std::str::from_utf8` with `to_owned()` only for the spawned task.
296 - 102. **[DEFERRED]** Extract shared `validate_promo_code()` helper to prevent checkout path divergence.
331 + 103. **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id`
332 + 104. **[SERIOUS]** Add S3 cleanup + storage decrement to project deletion path
333 + 105. **[SERIOUS]** Stream content export ZIP to S3 instead of in-memory buffer
334 + 106. **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
335 + 107. **[MINOR]** Use `try_replace_storage` for project image replace (`routes/storage/images.rs:173-185`)
336 + 108. **[MINOR]** Enqueue old project image S3 key to `pending_s3_deletions`
337 + 109. **[MINOR]** Add HTTP error check to purchase.html cart add-to-cart JS
338 + 110. **[MINOR]** Fix `format_price` negative formatting to use `-$X.XX`
339 + 111. **[MINOR]** Batch-load collection items in export handler (`exports/mod.rs:268-270`)
340 + 112. **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock`
341 + 113. **[DEFERRED]** Stream build artifacts to S3 via multipart upload
342 + 114. **[DEFERRED]** Extract shared `validate_promo_code()` helper
343 +
344 + ### Run 24 (2026-05-09) -- All Fixed
345 +
346 + 97. ~~**[SERIOUS]** Add `starts_at` validation to guest checkout~~ -- **Fixed.**
347 + 98. ~~**[SERIOUS]** Fix version replace storage counter rollback~~ -- **Fixed.**
348 + 99. ~~**[MINOR]** Change pending_uploads ON CONFLICT to DO NOTHING~~ -- **Fixed.**
349 + 100. ~~**[MINOR]** Blog editor: preserve form input on validation error~~ -- **False positive.**
350 + 101. ~~**[MINOR]** Idempotency middleware: avoid double allocation~~ -- **Fixed.**
351 + 102. **[DEFERRED]** Extract shared `validate_promo_code()` helper -- carried to Run 25 item 114.
297 352
298 353 ### Run 23 (2026-05-09) -- All Fixed
299 354
300 - 88. ~~**[SERIOUS]** Enqueue old S3 key on version file replacement~~ -- **Fixed.**
301 - 89. ~~**[SERIOUS]** Harden archive decompression fallback~~ -- **Fixed.**
302 - 90. ~~**[SERIOUS]** Reserve promo code slot atomically at checkout creation~~ -- **Already implemented** (false positive).
303 - 91. ~~**[MINOR]** Consolidate cart toggle into single JOIN query~~ -- **Fixed.**
304 - 92. ~~**[MINOR]** Consolidate download access control into single query~~ -- **Fixed.**
305 - 93. ~~**[MINOR]** Stream content exports~~ -- **Fixed.**
306 - 94. ~~**[MINOR]** Tighten CSV email validation~~ -- **Fixed.**
307 - 95. ~~**[DEFERRED]** Split oversized route files~~ -- **Fixed.**
308 - 96. ~~**[DEFERRED]** Add README.md to server/~~ -- **Fixed.**
355 + 88-96. All 9 items verified fixed. See Run 24 verification table.
309 356
310 357 ### Open (blocked on upstream)
311 358
@@ -314,56 +361,62 @@
314 361 25. Monitor aws-sdk-s3 for rustls-webpki 0.101.7 fix (RUSTSEC-2026-0049)
315 362 33. bincode unmaintained (RUSTSEC-2025-0141) -- upstream via syntect/yara-x, warning only
316 363
317 - ## Previous Action Item Verification (Run 23)
364 + ## Previous Action Item Verification (Run 24)
318 365
319 366 | # | Item | Status |
320 367 |---|------|--------|
321 - | 88 | Enqueue old S3 key on version file replacement | **Fixed** (verified: versions.rs:203-204 enqueues old key) |
322 - | 89 | Harden archive decompression fallback | **Fixed** (verified: archive.rs:123 uses 10x multiplier) |
323 - | 90 | Reserve promo code slot atomically | **Already implemented** (false positive) |
324 - | 91 | Consolidate cart toggle | **Fixed** |
325 - | 92 | Consolidate download access control | **Fixed** |
326 - | 93 | Stream content exports | **Fixed** |
327 - | 94 | Tighten CSV email validation | **Fixed** |
328 - | 95 | Split oversized route files | **Fixed** |
329 - | 96 | Add README.md | **Fixed** |
368 + | 97 | Add `starts_at` validation to guest checkout | **Fixed** (verified) |
369 + | 98 | Fix version replace storage counter rollback | **Fixed** (verified: uses `try_replace_storage`) |
370 + | 99 | Change pending_uploads ON CONFLICT to DO NOTHING | **Fixed** (verified) |
371 + | 100 | Blog editor: preserve form input | **False positive** (JS fetch preserves state) |
372 + | 101 | Idempotency middleware double allocation | **Fixed** (verified) |
373 + | 102 | Extract shared validate_promo_code() | **Deferred** -- carried to Run 25 |
330 374
331 - 9 of 9 Run 23 items verified fixed.
375 + 5 of 5 actionable Run 24 items verified fixed. 1 deferred item carried forward.
332 376
333 377 ### Chronic Items (unfixed across 3+ consecutive runs)
334 378
335 - None. Both previous chronic items (README.md, oversized route files) resolved in Run 23.
379 + None. The `validate_promo_code()` extraction has been deferred for 2 consecutive runs (24, 25) -- will become chronic if still open at Run 26.
336 380
337 - ### False Positives Identified (Run 24)
381 + ### False Positives Identified (Run 25)
338 382
339 383 The following agent findings were investigated and determined not to be real bugs:
340 - - **verify_password timing attack** -- Mitigated by DUMMY_HASH in all callers (routes/auth.rs:111, routes/synckit/auth.rs:59+69, routes/oauth.rs:294). Hash always comes from DB or dummy.
341 - - **Backup code SQL WHERE timing** -- Standard database practice. ~1ms query time variance is indistinguishable from network jitter over TCP.
342 - - **SQL injection via format!** -- Column names are hardcoded string literals from exhaustive enum match. No user input.
343 - - **Multi-seller Stripe isolation** -- Requires API key compromise. Cart scoped to single seller at application level.
344 - - **JWT iat validation** -- Standard JWT behavior. exp is validated. No security impact.
345 - - **Password length not centralized** -- Present at all entry points (routes/auth.rs, routes/synckit/auth.rs:58, routes/oauth.rs). Argon2id has internal limits.
384 + - **Tip checkout missing minimum charge** -- Route handler at `routes/stripe/checkout/tips.rs:54-57` validates minimum $1.00 (above Stripe's 50-cent minimum) before calling the payment provider.
385 + - **FixedPricing no upper bound** -- Stripe session amount is set server-side. Client cannot override. The `validate_amount` function is for display/UX validation only.
386 + - **MaybeUser session revocation bypass** -- Documented, intentional design. Used only for read-only/public pages. The one write endpoint (OAuth authorize) compensates with manual session validation.
387 + - **Breached password advisory-only** -- Intentional UX decision. Server warns but doesn't block.
388 + - **TOTP secret plaintext** -- Standard practice for TOTP. Encryption at rest would require key management infrastructure beyond current scale.
389 + - **JWT iat not validated** -- Standard JWT behavior. exp is validated. No security impact.
390 + - **Session cookie __Host- prefix** -- No user-content subdomains. SameSite=Lax + Secure + HttpOnly is sufficient.
391 + - **Archive scanner no recursive scan** -- ClamAV and YARA scan the full outer bytes. Nested archive detection flags suspicious nesting (limit 3).
392 + - **Structural analysis dlopen/posix_spawn** -- These are one signal among many; no false-positive reports in production.
346 393
347 - ## Delta Since Run 23
394 + ## Delta Since Run 24
348 395
349 - ### Fixed (from Run 23)
350 - All 9 Run 23 action items verified fixed. No regressions detected.
396 + ### Fixed (from Run 24)
397 + 5 of 5 actionable Run 24 items verified fixed. No regressions detected. 1 deferred item carried forward.
351 398
352 - ### New Findings (not in Run 23)
353 - - Guest checkout missing `starts_at` promo validation (SERIOUS) -- new
354 - - Storage counter corruption on version replace failure (SERIOUS) -- new (distinct from Run 23's S3 key leak -- this is about counter rollback, not cleanup)
355 - - Pending uploads staleness attack via ON CONFLICT (MINOR) -- new
356 - - Blog editor input loss on error (MINOR) -- new
357 - - Idempotency middleware double allocation (MINOR) -- new
399 + ### New Findings (not in Run 24)
400 + - Cart preflight SQL column name bug (SERIOUS) -- new, latent since cart was added
401 + - Project deletion orphans S3 objects (SERIOUS) -- new finding (not previously audited at this depth)
402 + - Content export 2GB memory buffering (SERIOUS) -- new finding (export was previously fixed for streaming writes, but ZIP output itself still buffered)
403 + - Non-atomic item cover image 3-UPDATE (MINOR) -- new
404 + - Non-atomic project image storage swap (MINOR) -- new (related to previously-noted fragile URL extraction)
405 + - Project image missing S3 deletion enqueue (MINOR) -- new
406 + - Purchase.html JS error handling gap (MINOR) -- new
407 + - format_price negative formatting (MINOR) -- new
408 + - Collection export N+1 query (MINOR) -- new
409 + - check_sandbox_cap blocking advisory lock (MINOR) -- new
358 410
359 - ### Grade Changes (after remediation)
360 - - guest_checkout.rs: (new) B -> A (starts_at added)
361 - - versions.rs: A -> B- -> A (atomic try_replace_storage)
362 - - pending_uploads.rs: B+ -> A (ON CONFLICT DO NOTHING)
363 - - metrics.rs: A- -> A (double allocation removed)
364 - - Overall: A (held, strengthened)
365 - - Cold spots: 0 -> 2 -> 0 (all resolved)
366 - - SERIOUS findings: 0 -> 2 -> 0 (all fixed)
411 + ### Grade Changes
412 + - db/cart.rs: A -> B+ (SQL column bug found)
413 + - routes/api/projects.rs (delete): (new) B- (S3 orphan gap)
414 + - routes/api/exports/content.rs: A- -> B+ (memory buffering)
415 + - routes/storage/images.rs: A -> B+ (atomicity + cleanup gaps)
416 + - templates/purchase.html: (new) B+ (JS error handling)
417 + - Overall: A (held)
418 + - Cold spots: 0 -> 5
419 + - SERIOUS findings: 0 -> 3
367 420
368 421 ## Metrics Over Time
369 422
@@ -392,6 +445,7 @@
Lines truncated
@@ -346,23 +346,23 @@
346 346
347 347 ### 14. SyncKit (E2E Encrypted Cloud Sync)
348 348
349 - - [ ] **SyncKit auth** — email/password + API key, or OAuth2 PKCE
350 - - [ ] **Push/pull sync** — bidirectional encrypted changelog
351 - - [ ] Push changes
352 - - [ ] Pull changes with cursor-based pagination
349 + - [x] **SyncKit auth** — OAuth2 PKCE flow tested with GO + AF on live server (2026-05-11)
350 + - [x] **Push/pull sync** — bidirectional encrypted changelog
351 + - [x] Push changes
352 + - [x] Pull changes with cursor-based pagination
353 353 - [ ] Table name filter for selective pull
354 354 - [ ] Idempotent push via batch_id
355 355 - [ ] **Device management** — register, list, delete devices
356 - - [ ] **E2E key storage** — encrypted keys with optimistic concurrency
357 - - [ ] Store key
358 - - [ ] Retrieve key
356 + - [x] **E2E key storage** — encrypted keys with optimistic concurrency
357 + - [x] Store key
358 + - [x] Retrieve key
359 359 - [ ] Version conflict returns 409
360 360 - [ ] **Blob storage** — encrypted blobs with hash dedup
361 361 - [ ] Upload blob
362 362 - [ ] Download blob
363 363 - [ ] Duplicate hash skips re-upload
364 - - [ ] **App management** — create apps, generate API keys
365 - - [ ] Create sync app
364 + - [x] **App management** — create apps, generate API keys
365 + - [x] Create sync app (GO + AF apps created)
366 366 - [ ] Regenerate API key
367 367 - [ ] Link app to project/item
368 368 - [ ] Set custom slug
@@ -374,7 +374,7 @@
374 374 - [ ] Begin rotation, re-encrypt entries in batches, complete
375 375 - [ ] Verify other device can pull mixed-key entries during rotation
376 376 - [ ] Verify new device setup after rotation uses new key
377 - - [ ] **SyncKit production test** — test sync across 2+ GO instances on real server
377 + - [x] **SyncKit production test** — GO + AF sync tested on live server (2026-05-11). BB pending (synckit.toml needed).
378 378
379 379 ### 15. OTA Updates
380 380
@@ -13,7 +13,12 @@
13 13
14 14 1. ~~**Deploy**~~ — Done (v0.5.14, 2026-05-11). Run 24 fixes + scheduler SQL fixes + robots.txt + Prometheus auth + ALERT_EMAIL + tag taxonomy overhaul (migration 111).
15 15 2. **Manual testing** — walk through `human_todo.md` sign-off table on live server (Stripe checkout, license keys, promo codes, cart, SyncKit sync)
16 - 3. **Content seeding** — at least one real creator with published content on discover page
16 + - SyncKit parity fixes shipped for AF + BB + GO (2026-05-11): OAuth auto-poll, CORS, CSP, synckit.toml, callback auto-complete
17 + - AF sync tested on live server — largely working (2026-05-11)
18 + - GO sync tested on live server — working (2026-05-11)
19 + - BB sync: synckit.toml still needed (API key pending)
20 + - Remaining: Stripe checkout e2e for all 3 apps, license key flow, promo codes
21 + 3. ~~**Content seeding**~~ — Done: AF 0.4.0 + GO 0.3.1 published on discover page. BB deferred (needs more plugins).
17 22 4. **Invite testers** — generate invite codes, send hand-written emails per `docs/internal/outreach/tiers.md`
18 23 5. ~~**Document undocumented features**~~ — Done: shopping cart, wishlist, creator pause all documented
19 24
@@ -38,9 +43,23 @@
38 43
39 44 ---
40 45
41 - ## Ultra Fuzz Run 24 (2026-05-09)
46 + ## Ultra Fuzz Run 25 (2026-05-11)
42 47
43 - - [ ] DEFERRED: Extract shared `validate_promo_code()` helper to prevent checkout path divergence
48 + ### Current Phase
49 + - [x] **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id` (1 line)
50 + - [x] **[SERIOUS]** Add S3 cleanup + storage decrement to project deletion path (`routes/api/projects.rs:270-282`)
51 + - [x] **[SERIOUS]** Stream content export ZIP to S3 instead of in-memory buffer (`routes/api/exports/content.rs`)
52 + - [ ] **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
53 + - [ ] **[MINOR]** Use `try_replace_storage` for project image replace (`routes/storage/images.rs:173-185`)
54 + - [ ] **[MINOR]** Enqueue old project image S3 key to `pending_s3_deletions`
55 + - [ ] **[MINOR]** Add HTTP error check to purchase.html cart add-to-cart JS
56 + - [ ] **[MINOR]** Fix `format_price` negative formatting to use `-$X.XX`
57 + - [ ] **[MINOR]** Batch-load collection items in export handler (`exports/mod.rs:268-270`)
58 + - [ ] **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock`
59 +
60 + ### Deferred
61 + - [ ] DEFERRED: Stream build artifacts to S3 via multipart upload
62 + - [ ] DEFERRED: Extract shared `validate_promo_code()` helper to prevent checkout path divergence (carried from Run 24)
44 63
45 64 ---
46 65
@@ -180,6 +180,13 @@
180 180 tracing::warn!("delete_prefix called on a storage backend that does not implement it");
181 181 Ok(())
182 182 }
183 + /// Upload a file via S3 multipart upload. Default falls back to single upload.
184 + async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
185 + let data = tokio::fs::read(file_path)
186 + .await
187 + .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to read file: {e}")))?;
188 + self.upload_object(s3_key, content_type, data, None).await
189 + }
183 190 async fn check_connectivity(&self) -> std::result::Result<(), String>;
184 191 fn bucket(&self) -> &str;
185 192 }
@@ -387,6 +394,14 @@
387 394 self.inner.delete(s3_key).await.map_err(AppError::Storage)
388 395 }
389 396
397 + /// Upload a file to S3 using multipart upload (10 MB parts).
398 + pub async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
399 + self.inner
400 + .upload_multipart(s3_key, content_type, file_path, None)
401 + .await
402 + .map_err(AppError::Storage)
403 + }
404 +
390 405 /// Lightweight connectivity check — issues a list with max_keys(0).
391 406 pub async fn check_connectivity(&self) -> std::result::Result<(), String> {
392 407 self.inner.check_connectivity().await
@@ -512,6 +527,10 @@
512 527 .map_err(AppError::Storage)
513 528 }
514 529
530 + async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
531 + self.upload_multipart(s3_key, content_type, file_path).await
532 + }
533 +
515 534 async fn check_connectivity(&self) -> std::result::Result<(), String> {
516 535 self.check_connectivity().await
517 536 }
@@ -6,4 +6,5 @@
6 6 [dependencies]
7 7 aws-sdk-s3 = "1.131"
8 8 aws-config = { version = "1.8", features = ["behavior-version-latest"] }
9 + tokio = { version = "1", features = ["fs", "io-util"] }
9 10 tracing = "0.1"
Binary file