Skip to main content

max / makenotwork

Audit remediation: Run 23 + Run 24 fixes, site-docs refresh Run 23: split exports/health/user into modules, consolidate cart and download access queries, stream content exports, harden archive decompression fallback, tighten CSV email validation, add README. Run 24: add starts_at promo validation to guest checkout, use atomic try_replace_storage for version file replacement, change pending_uploads to ON CONFLICT DO NOTHING, remove idempotency middleware double allocation. Site-docs updated for accuracy and consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-09 21:34 UTC
Commit: 92a08691b09b96c251448000fc077610a491ff03
Parent: 42405fe
51 files changed, +3173 insertions, -3160 deletions
@@ -0,0 +1,26 @@
1 + # MNW Server
2 +
3 + Rust web server for [makenot.work](https://makenot.work), a fair creator platform with 0% platform fees.
4 +
5 + Built with Axum, SQLx (PostgreSQL), and Askama templates.
6 +
7 + ## Documentation
8 +
9 + - **Architecture:** `docs/architecture.md`
10 + - **Contributing:** `CONTRIBUTING.md`
11 + - **Deployment:** `deploy/deploy.sh` (run from this directory)
12 + - **Schema:** `docs/schema.md`
13 + - **Test plan:** `docs/test_plan.md`
14 + - **Troubleshooting:** `docs/troubleshooting.md`
15 + - **Rollback:** `docs/rollback.md`
16 + - **Audit:** `docs/audit_review.md`
17 +
18 + ## Quick start
19 +
20 + ```sh
21 + cargo run # development server
22 + cargo test # run test suite
23 + cargo clippy # lint
24 + ```
25 +
26 + See `CONTRIBUTING.md` for coding patterns, macros, and conventions.
@@ -1,11 +1,11 @@
1 1 # MakeNotWork -- Audit Review
2 2
3 - **Last audited:** 2026-05-09 (Run 22, Ultra Fuzz -- 5-axis deep audit)
4 - **Previous audit:** 2026-05-08 (Run 21, Ultra Fuzz -- 5-axis deep audit)
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)
5 5
6 6 ## Overall Grade: A
7 7
8 - Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9. ~87,853 LOC. 1,215 test annotations. 105 migrations. 1 SERIOUS finding (UX), 0 CRITICAL. 3 cold spots. All Run 21 action items fixed (12/12). Security posture A+. Observability gaps resolved (embed/, payments/ now instrumented).
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.
9 9
10 10 ## Scorecard
11 11
@@ -13,19 +13,19 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.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 |
17 - | Security | A+ | SHA-256-based constant-time compare, fail-closed scanning, CSRF everywhere, Argon2id, HMAC webhooks, PKCE S256 |
18 - | Performance | A | Discover facet queries now parallelized via try_join!. Scheduler lock pinned. No remaining bottlenecks at launch scale |
19 - | Documentation | A- | Module-level //! on all major files. No README.md |
16 + | Testing | A | ~1,215 test annotations, proptest active, adversarial tests, comprehensive harness |
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 |
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 - | Frontend | A | Askama auto-escape, json_escape prevents JSON-LD XSS, HTMX patterns consistent |
21 + | Frontend | A | Askama auto-escape, json_escape prevents JSON-LD XSS, HTMX patterns consistent. CSRF field names consistent |
22 22 | Type Safety | A+ | 36 UUID newtypes, 25+ domain enums, validated string types, Cents/PriceCents monetary newtypes |
23 - | Observability | A | All route modules now instrumented including embed/ and payments/ (chronic items resolved) |
24 - | Concurrency | A | ON CONFLICT, FOR UPDATE, DashMap caches. Scheduler advisory lock now pinned to connection |
23 + | Observability | A | All route modules instrumented including embed/ and payments/ |
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 | 105 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout |
28 - | Codebase Size | A- | ~88K LOC. Oversized files remain (exports.rs, health.rs, tabs/user.rs) |
27 + | Migration Safety | A | 107 additive migrations, IF NOT EXISTS guards, TIMESTAMPTZ throughout |
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
31 31
@@ -35,7 +35,7 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9
35 35 | config.rs | A | A | A | A | n/a | A | A | A- | A |
36 36 | error.rs | A | A | A | A | n/a | A | A | A | A |
37 37 | auth.rs | A | A | A- | A+ | A | A | A | A | A |
38 - | csrf.rs | A | A | A | A | A- | A | A | A- | A |
38 + | csrf.rs | A+ | A | A | A+ | A- | A | A | A- | A |
39 39 | constants.rs | A+ | n/a | A | A | n/a | A- | A | n/a | A |
40 40 | helpers.rs | A | A | A | A | A | A | A- | B+ | A |
41 41 | rate_limit.rs | A | A | A- | A | A | A | A | A | A |
@@ -45,6 +45,7 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9
45 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 + | metrics.rs | A | A | n/a | A | A- | A | A | A | A |
48 49 | db/enums.rs | A | A | A | A | n/a | A | A+ | n/a | A |
49 50 | db/id_types.rs | A | A | A | n/a | n/a | A | A+ | n/a | A |
50 51 | db/validated_types.rs | A | A | A+ | A | A | A | A+ | n/a | A |
@@ -54,7 +55,7 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9
54 55 | db/transactions.rs | A | A | B+ | A | A | A | A | A | A |
55 56 | db/discover.rs | A | A | B | A | A | A | A | n/a | A- |
56 57 | db/cart.rs | A | A | B | A | A | A | A | n/a | A |
57 - | db/creator_tiers.rs | A | A | A | A | A | A | A | n/a | A |
58 + | db/creator_tiers.rs | A | A+ | A | A | A | A | A | n/a | A |
58 59 | db/versions.rs | A | A | A | A | A | A | A | n/a | A |
59 60 | db/builds.rs | A | A+ | A | A | A | A | A | n/a | A |
60 61 | db/pending_refunds.rs | A | A | B | A | A | A | A | n/a | A |
@@ -62,34 +63,41 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9
62 63 | db/promo_codes.rs | A | A | A | A | A | A | A | n/a | A |
63 64 | db/tips.rs | A | A | B | A | A | A | A | n/a | A |
64 65 | 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 |
65 67 | db/models/* | A | A | B+ | A | n/a | A- | A | n/a | A |
66 68 | types/ | A | A | B | A | n/a | A | A | n/a | A |
67 69 | scanning/ | A | A+ | A- | A+ | A- | A | A- | A | A |
70 + | scanning/archive.rs | A | A | A- | A- | A- | A | A- | A | A |
68 71 | payments/ | A | A | A- | A | A- | A | A | A | B+ |
69 72 | email/ | A | A | A | A | A- | A | A | B+ | A- |
70 - | scheduler/ | A | A | B+ | A | A | A | A | A- | A |
73 + | scheduler/ | A | A+ | B+ | A | A | A | A | A- | A |
71 74 | scheduler/cleanup.rs | A | A | n/a | A | A | A | A | A- | A |
72 75 | validation/ | A | A | A+ | A+ | A | B+ | A | n/a | A |
73 76 | import/ | A | A | A | A | B+ | A- | A | B+ | A |
74 77 | git/ | A | A | A | A+ | B+ | A- | A | B | A |
75 - | git_ssh.rs | A | A | A | A- | A | A- | A | B+ | A |
78 + | git_ssh.rs | A | A | A | A | A | A- | A | B+ | A |
76 79 | build_runner.rs | A | A- | B+ | A+ | A- | A- | A | A | A |
77 - | monitor.rs | A | A | A- | A | A | A | A | A | A |
78 - | templates/ | A | A- | n/a | **B+** | A | A- | A | B+ | B+ |
80 + | monitor.rs | A | A | A- | A | A | A | A | A+ | A |
81 + | templates/ | A | A- | n/a | A | A | A- | A | B+ | B+ |
79 82 | routes/auth.rs | A | A | n/a | A+ | A | A | A | A | A |
80 83 | routes/oauth.rs | A | A | n/a | A- | A | A | A | A- | A- |
81 84 | routes/admin/ | A | A | n/a | A | A | A | A | A | A |
82 - | routes/api/ | A | A | n/a | A | A | A | A | A | **B+** |
83 - | routes/api/exports.rs | A- | A- | n/a | A | **B-** | A | A | A | **B** |
84 - | routes/stripe/ | A | A | n/a | A | A- | A | A | A | **B+** |
85 + | 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 |
88 + | routes/api/exports/ | A- | A- | n/a | A | A- | A | A | A | A- |
89 + | routes/stripe/ | A | A | n/a | A | A- | A | A | A | B+ |
85 90 | routes/stripe/checkout/ | A | A | n/a | A- | A | A | A | A | A |
91 + | routes/stripe/checkout/cart.rs | A | A | n/a | A- | A | A | A | A | A |
86 92 | routes/synckit/ | A | A | n/a | A | A- | A | A | A | A |
87 93 | routes/pages/discover.rs | A | A | n/a | A | A | B+ | A | A | A |
88 - | routes/pages/ (other) | A | A | n/a | A | A | A | A | A- | **B+** |
94 + | routes/pages/ (other) | A | A | n/a | A | A | A | A | A- | B+ |
89 95 | routes/embed/ | A | A | n/a | A | A | A- | A | A | A |
90 96 | routes/git/ | A | A | n/a | A | A | A | A | A | A |
91 97 | 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 |
92 99 | routes/storage/uploads.rs | A | A | n/a | A | A | A | A | A | A |
100 + | routes/storage/downloads.rs | A | A | n/a | A | A- | A | A | A | A |
93 101 | routes/storage/images.rs | A | A | n/a | A | A | A | A | A | A |
94 102 | routes/postmark/ | A | A | n/a | A | A | A | n/a | A- | B+ |
95 103
@@ -97,197 +105,207 @@ Run 22: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.9
97 105
98 106 ### Cold Spots
99 107
100 - 1. **templates/ security (B+):** Three templates (`fan_plus.html`, `project.html`, `sandbox.html`) use `name="csrf_token"` but CSRF middleware extracts `_csrf`. Forms with these fields will fail CSRF validation with 403.
101 - 2. **routes/api/exports.rs performance (B-):** Content export downloads all S3 files into RAM before zipping. 2 GB cap exists but entire payload is buffered in memory. At concurrent exports, memory exhaustion is possible.
102 - 3. **routes/api/exports.rs size (B):** 842 LOC, above 500-line guideline. Chronic from Run 21.
108 + None. All Run 24 cold spots resolved in remediation.
103 109
104 - ### Resolved Cold Spots (from Run 21)
110 + ### Resolved Cold Spots (from Run 24)
105 111
106 - - ~~routes/pages/discover.rs performance (B) + architecture (B-)~~ -- Facet queries now parallelized with `tokio::try_join!`.
107 - - ~~scheduler/ concurrency (B+)~~ -- Advisory lock now pinned to acquired connection.
108 - - ~~db/cart.rs correctness (B+)~~ -- PWYW minimum now enforced in `effective_price_cents()`.
109 - - ~~routes/embed/ observability (B)~~ -- All embed handlers now instrumented.
110 - - ~~payments/ observability (B+)~~ -- All payment functions now instrumented.
112 + - ~~routes/api/guest_checkout.rs (B)~~ -- Added `starts_at` promo code validation. Matches all authenticated checkout paths.
113 + - ~~routes/storage/versions.rs (B-)~~ -- Now uses atomic `try_replace_storage` for file replacements. No separate decrement/increment.
114 +
115 + ### Resolved Cold Spots (from Run 23)
116 +
117 + - ~~versions.rs transaction safety (B)~~ -- Old S3 key now enqueued for deletion via `pending_s3_deletions`.
118 + - ~~cart.rs performance (B-)~~ -- Consolidated into single-query `toggle_cart_preflight()`.
119 + - ~~downloads.rs performance (B-)~~ -- Consolidated into single-query `check_item_access()`.
120 + - ~~exports.rs performance (B-) + size (B)~~ -- Files written directly to ZIP one-by-one. Module split into `exports/mod.rs` + `exports/content.rs`.
121 + - ~~archive.rs security (B+)~~ -- Decompression fallback uses 10x conservative multiplier.
122 + - ~~checkout/cart.rs correctness (B+)~~ -- Was false positive; atomic `try_increment_use_count` already in place at checkout creation.
111 123
112 124 ## Mandatory Surprises
113 125
114 - **Run 22 (5 surprises, one per axis):**
126 + **Run 24 (5 surprises, one per axis):**
115 127
116 - 1. **Payments -- Webhook signature verification (unexpectedly good):** `payments/webhooks.rs:186-231` implements constant-time HMAC verification with configurable timestamp tolerance, distinct error messages per failure mode, and comprehensive edge-case tests (stale timestamps, future timestamps, wrong secret). Gold standard implementation.
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.
117 129
118 - 2. **Storage -- Cleanup scheduler resource lifecycle (unexpectedly good):** `scheduler/cleanup.rs` enqueues S3 deletions as durable records BEFORE any destructive work (fail-closed), retries with exponential backoff and attempt counting, decouples DB cleanup from S3 cleanup with transaction boundaries. The `pending_s3_deletions` queue survives crashes. Production-grade pattern.
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.
119 131
120 - 3. **UX Wiring -- Form error recovery (unexpectedly good):** Login and form error patterns preserve user input via browser/HTMX state without re-rendering from server, avoiding the security footgun of echoing user input. Error messages are plain text, not reflected user input. Small UX detail that most platforms get wrong.
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.
121 133
122 - 4. **Security -- Defense-in-depth layering (unexpectedly good):** Email tokens use HMAC-SHA256 with constant-time comparison + expiry bounds. Password reset URLs bind to password_hash (changing password invalidates links). Login tokens use double-hash (token in email, hash in DB). Passkey counter prevents cloning. TOTP replay prevention via `last_used_step`. Six independent layers, each independently correct.
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.
123 135
124 - 5. **Performance -- Export buffering (unexpectedly bad):** `routes/api/exports.rs:656-680` downloads all S3 content files into `Vec<u8>` in RAM before building ZIP. With a 2 GB per-export cap, two concurrent exports could spike heap to 4+ GB. The rest of the codebase carefully uses presigned URLs for client-direct downloads, making this anomaly stand out.
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.
125 137
126 138 ### Previous Surprises
127 139
128 - **Run 21:** Pending refund bidirectional matching, claim_pending_build FOR UPDATE SKIP LOCKED, json_escape, SHA-256-based constant_time_compare, scheduler advisory lock unpinned (bad, now fixed).
140 + **Run 23:** Webhook signature gold standard, atomic storage quota enforcement, CSRF dual-layer extraction, archive decompression fallback (bad, fixed), advisory lock protocol.
129 141
130 - **Run 20:** Scanning module -- 6-layer anti-malware with actual decompression, VMProtect/UPX detection, ZIP bomb byte counting.
131 -
132 - **Run 19:** Hand-rolled Stripe v2 webhook signature verification with replay protection.
142 + **Run 22:** Webhook signature gold standard, cleanup scheduler durable queue, form error recovery, defense-in-depth 6 layers, export RAM buffering.
133 143
134 - **Run 18:** Sandbox tier mismatch bug (SmallFiles vs small_files). Fixed.
144 + **Run 21:** Pending refund bidirectional matching, claim_pending_build FOR UPDATE SKIP LOCKED, json_escape, SHA-256 constant_time_compare, scheduler advisory lock unpinned (bad, fixed).
135 145
136 - **Run 17:** TOCTOU-safe slug generation with retry loop + advisory lock pattern for sandbox IP cap.
146 + **Run 20:** Scanning module -- 6-layer anti-malware with decompression, VMProtect/UPX detection, ZIP bomb byte counting.
137 147
138 - **Run 15:** Session touch cache -- DashMap with 30s TTL avoids N+1 session queries.
148 + **Run 19:** Hand-rolled Stripe v2 webhook signature verification with replay protection.
139 149
140 150 ## Strengths
141 151
142 152 ### 1. Security-in-depth
143 - Zero SQL injection vectors across 200+ queries. Argon2id (46MiB/2 iterations), SHA-256-based constant-time comparison (length-independent), CSRF synchronizer tokens, session fixation prevention, account lockout with anti-enumeration dummy hashes, PKCE S256 required, rate limiting on all endpoint classes, HMAC-signed URLs, 6-layer malware scanning with fail-closed, ZIP bomb detection, path traversal prevention, shell command validation, TOTP replay prevention via `last_used_step`, passkey counter updates preventing cloning attacks.
153 + Zero SQL injection vectors across 200+ queries. Argon2id (46MiB/2 iterations), SHA-256-based constant-time comparison, CSRF synchronizer tokens (constant-time validated), session fixation prevention, account lockout with anti-enumeration dummy hashes on ALL login paths (web, SyncKit, OAuth), PKCE S256 required, rate limiting on all endpoint classes, HMAC-signed URLs, 6-layer malware scanning with fail-closed, ZIP bomb detection with 10x conservative multiplier, path traversal prevention, shell command validation, TOTP replay prevention via time step tracking, passkey counter updates.
144 154
145 155 ### 2. Type safety discipline
146 - 36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types (Username, Slug, KeyCode), Cents/PriceCents monetary newtypes with proptest coverage. Compile-time template verification via Askama. All money math in integer cents (i32/i64), zero floating point in money paths. `SUM(BIGINT)::BIGINT` cast used consistently across all aggregate queries. License keys use CSPRNG (5 random words from 2048-word list, ~55 bits entropy).
156 + 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.
147 157
148 158 ### 3. Payment robustness
149 - Three-layer webhook idempotency (event dedup table, status-based WHERE clauses, ON CONFLICT). Bidirectional pending refund matching. Atomic promo code reservation with cleanup on abandonment. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all checkout paths. Cart PWYW minimum now enforced. Tip amounts capped ($1-$10,000).
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.
150 160
151 - ### 4. Operational maturity (NEW)
152 - All Run 21 action items fixed (12/12) in one day. Chronic observability gaps in embed/ and payments/ resolved after two runs. Scheduler advisory lock pinned. Soft-delete purge now handles version S3 keys and storage decrement. Confirm uploads wrapped in transactions. Discover page parallelized. Pending S3 deletions durable queue deployed (migration 105).
161 + ### 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.
153 163
154 164 ## Weaknesses
155 165
156 - ### 1. Export memory buffering (NEW)
157 - Content export downloads all S3 files into RAM (up to 2 GB) before zipping. First real memory exhaustion vector at concurrent usage. Should stream ZIP directly or export to S3 with presigned download URL.
166 + ### Active (Run 24)
158 167
159 - ### 2. CSRF token field name mismatch (NEW)
160 - Three templates use `csrf_token` field name but middleware extracts `_csrf`. Affected forms: fan_plus.html, project.html tip form, sandbox.html. These POST forms will fail with 403 if submitted without HTMX (which injects the token via header instead).
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.
161 170
162 - ### 3. Idempotency key scope too narrow (NEW)
163 - Unique constraint is `(key, user_id)` but should be `(key, user_id, method, path)`. Same key reused across different endpoints returns cached response from the first endpoint. Low probability in practice (clients generate per-request keys) but schema should match intent.
171 + ### Resolved (Run 23)
164 172
165 - ### 4. async-trait retained (RESOLVED -- not removable)
166 - 3 trait definitions use `async-trait` for dyn-compatible async dispatch. Rust 2024 native async fn in traits is not dyn-compatible. Closing chronic item.
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.
167 177
168 178 ## Bug Reports by Axis
169 179
170 180 ### Payments
171 - 0 SERIOUS, 0 MINOR, 3 NOTE
181 + 0 CRITICAL, 1 SERIOUS, 0 MINOR, 2 NOTE
172 182
173 183 | # | Sev | Location | Description |
174 184 |---|-----|----------|-------------|
175 - | P1 | NOTE | `pricing.rs:134-148` | FixedPricing has no upper cap on amount. Not exploitable -- fixed-price path uses `item.price_cents`, not user input. |
176 - | P2 | NOTE | `pricing.rs:113-115` | FixedPricing `is_free()` hardcoded false. Unreachable -- `for_item()` routes price=0 to FreePricing. |
177 - | P3 | NOTE | `db/subscriptions.rs:399-407` | `has_active_subscription_to_project` doesn't check `cancel_at_period_end`. By design (access until period end). |
178 -
179 - Previous SERIOUS findings (P1 cart PWYW bypass from Run 21) -- **FIXED**.
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. |
180 188
181 189 ### Storage
182 - 0 SERIOUS, 1 MINOR, 1 NOTE
190 + 0 CRITICAL, 1 SERIOUS, 1 MINOR, 1 NOTE
183 191
184 192 | # | Sev | Location | Description |
185 193 |---|-----|----------|-------------|
186 - | S1 | MINOR | `db/pending_s3_deletions.rs` | No UNIQUE constraint on `(s3_key, bucket)` in pending_s3_deletions table. `ON CONFLICT DO NOTHING` is idempotent but schema should be explicit. |
187 - | S2 | NOTE | `storage.rs:446-465` | `extract_s3_key_from_url` includes bucket name for path-style URLs. Only used for project images which handle it. |
188 -
189 - Previous SERIOUS findings (S1 soft-delete purge, S2 non-atomic confirm from Run 21) -- **FIXED**.
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. |
190 197
191 198 ### UX Wiring
192 - 1 SERIOUS, 0 MINOR, 3 NOTE
199 + 0 SERIOUS, 1 MINOR, 3 NOTE
193 200
194 201 | # | Sev | Location | Description |
195 202 |---|-----|----------|-------------|
196 - | U1 | **SERIOUS** | `templates/pages/fan_plus.html:97`, `templates/pages/project.html:209`, `templates/pages/sandbox.html:24` | CSRF field name mismatch: templates use `name="csrf_token"` but `csrf.rs:81` extracts `_csrf`. Forms submitted without HTMX JS will fail with 403. HTMX path works (header injection). |
197 - | U2 | NOTE | `templates/pages/login.html:13-16` | Login form lacks CSRF. Intentional (pre-auth exempt). |
198 - | U3 | NOTE | `templates/public.rs:471-482` | BuyPageTemplate lacks csrf_token field entirely. Intentional (guest checkout). |
199 - | U4 | NOTE | `formatting.rs:4-13` | `format_price` uses f64 division. Correct for all practical prices with `{:.2}`. |
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). |
200 207
201 208 ### Security
202 - 0 SERIOUS, 1 MINOR, 2 NOTE
209 + 0 CRITICAL, 0 SERIOUS, 0 MINOR, 3 NOTE
203 210
204 211 | # | Sev | Location | Description |
205 212 |---|-----|----------|-------------|
206 - | X1 | MINOR | `db/idempotency.rs:50-52` | Unique constraint on `(key, user_id)` but queries filter by `(key, user_id, method, path)`. Same key on different endpoints returns wrong cached response. |
207 - | X2 | NOTE | `scanning/mod.rs:279` | Pipeline integration test only exercises 3 of 7 FileType variants. |
208 - | X3 | NOTE | `scanning/hash_lookup.rs:84` | MalwareBazaar `no_results` treated as Error (held for review). Deliberate fail-closed. |
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. |
209 216
210 217 ### Performance
211 - 0 SERIOUS, 2 MINOR, 2 NOTE
218 + 0 CRITICAL, 0 SERIOUS, 2 MINOR, 1 NOTE
212 219
213 220 | # | Sev | Location | Description |
214 221 |---|-----|----------|-------------|
215 - | F1 | MINOR | `routes/api/exports.rs:656-680` | Content export downloads all S3 files into RAM before zipping. 2 GB cap but full heap buffering. Two concurrent exports = 4 GB spike. |
216 - | F2 | MINOR | `metrics.rs:206` | Idempotency middleware buffers full response body (up to 1MB). |
217 - | F3 | NOTE | `routes/synckit/subscribe.rs:123` | SSE broadcast channel size 16. Lag handled by filtering. By design. |
218 - | F4 | NOTE | `routes/synckit/sync.rs:98-100` | Push/pull fetch all devices for validation. Bounded at 50 per app. |
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. |
219 225
220 226 ## Cross-Cutting Concerns
221 227
222 - ### CSRF field name inconsistency (UX + Security)
223 - Three templates use `csrf_token` instead of `_csrf`. While HTMX header injection means this doesn't block normal usage, it represents a defense-in-depth gap. If JS fails to load, these forms become non-functional. Single fix: standardize field names.
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.
224 230
225 - ### Export memory pattern (Performance + Storage)
226 - Export buffering is the sole remaining pattern where large data is loaded entirely into RAM. The rest of the codebase consistently uses streaming (presigned URLs, chunked S3 uploads, scan pipeline). Fixing this would complete the streaming pattern across the entire codebase.
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.
227 233
228 234 ## Components Successfully Stress-Tested
229 235
230 236 ### Payments (12 vectors survived)
231 - Webhook replay, double-credit, concurrent promo exhaustion, cross-user data access, self-purchase, negative/overflow amounts, floating-point money math, out-of-order webhooks, suspended creator purchases, SUM(BIGINT) pitfall, PWYW cart minimum enforcement, tip amount cap enforcement.
237 + Webhook replay, double-credit, concurrent promo exhaustion (atomic at DB level), cross-user data access, self-purchase, negative/overflow amounts, floating-point money math, out-of-order webhooks, suspended creator purchases, SUM(BIGINT) pitfall, PWYW cart minimum enforcement, tip amount cap enforcement.
232 238
233 239 ### Storage (10 vectors survived)
234 - Cross-user file overwrites, path traversal in filenames, content type smuggling, storage quota bypass, double-spend on idempotent confirm, malware file serving, orphaned upload cleanup, SUM(BIGINT) pitfall, transactional confirm uploads, soft-delete purge with version S3 keys.
240 + Cross-user file overwrites, path traversal in filenames, content type smuggling, storage quota bypass (atomic enforcement), double-spend on idempotent confirm, malware file serving, orphaned upload cleanup, SUM(BIGINT) pitfall, transactional confirm uploads, soft-delete purge with version S3 keys.
235 241
236 242 ### UX Wiring (10 vectors survived)
237 243 XSS via template injection, open redirect, user enumeration, markdown/HTML injection, pagination abuse, integer overflow in pricing, internal detail leakage, CSV injection, Unicode boundary attacks, JSON-LD breakout.
238 244
239 245 ### Security (17 vectors survived)
240 - Virus scan bypass via ClamAV downtime, ZIP bomb, content-type spoofing, path traversal in archives, session fixation, timing-based user enumeration, brute force login, X-Forwarded-For spoofing, session reuse after password change, OAuth code replay, PKCE downgrade, token prediction, CSRF on state-changing endpoints, SSH command injection, passkey cloning, TOTP replay, IDOR on passkeys/sessions.
246 + 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).
241 247
242 248 ### Performance (10 vectors survived)
243 - Connection pool exhaustion, file scanning memory, SSE connection accumulation, scheduler job accumulation, background task leaks, ZIP bombs, path traversal, shell injection, lock ordering, discover page concurrent load.
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).
244 250
245 251 ## Confidence Assessment
246 252
247 253 | Axis | Confidence | Notes |
248 254 |------|-----------|-------|
249 - | Payments | HIGH | Three-layer idempotency, integer money math, bidirectional refund matching. Cart PWYW now enforced. Tip cap enforced. CSPRNG license keys. |
250 - | Storage | HIGH | Presigned URL security solid. Confirms now transactional. Soft-delete purge handles version keys. Pending S3 deletions durable queue deployed. |
251 - | UX Wiring | HIGH | Askama auto-escape, json_escape defense-in-depth, comprehensive CSRF (one field name bug). No detail leakage. |
252 - | Security | HIGH | No CRITICAL or SERIOUS findings. Argon2id, constant-time everywhere, fail-closed scanning, PKCE S256. Idempotency key scope is low-risk. |
253 - | Performance | HIGH (current scale) | Discover parallelized. Scheduler lock pinned. Export buffering is sole remaining concern, low-frequency endpoint. |
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. |
254 260
255 261 ## Metrics
256 262
257 263 - Modules audited: 55+
258 - - Total cold spots: 3
259 - - Bugs by severity: 0 critical, 1 serious, 4 minor, 10 note
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
260 266 - Axes at A or above: 5/5
261 267
262 268 ## Axis Summary Grades
263 269
264 270 | Axis | Overall | Cold Spots | Mandatory Surprise |
265 271 |------|---------|------------|-------------------|
266 - | Payments | A | None | Webhook signature verification gold standard |
267 - | Storage | A | None | Cleanup scheduler durable S3 deletion queue |
268 - | UX Wiring | A- | templates/ CSRF field name (B+) | Form error recovery preserves input without reflecting |
269 - | Security | A | None | Defense-in-depth: 6 independent auth layers, each correct |
270 - | Performance | A- | exports.rs perf (B-), size (B) | Export RAM buffering anomaly vs streaming everywhere else |
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) |
271 277
272 278 ## Recommended Priority Order
273 279
274 - 1. **[SERIOUS] Fix CSRF field name mismatch** (`templates/pages/fan_plus.html:97`, `project.html:209`, `sandbox.html:24`) -- Change `name="csrf_token"` to `name="_csrf"` in all three templates. 5-minute fix, restores defense-in-depth for non-JS form submissions.
275 - 2. **[MINOR] Fix idempotency key unique constraint** (`db/idempotency.rs`) -- Add migration to change unique constraint to `(key, user_id, method, path)`. Prevents wrong cached response on key reuse across endpoints.
276 - 3. **[MINOR] Stream content exports** (`routes/api/exports.rs:656-680`) -- Replace in-memory buffering with streaming ZIP writer or export-to-S3-then-presign. Eliminates last RAM exhaustion vector.
277 - 4. **[MINOR] Add UNIQUE on pending_s3_deletions** -- `CREATE UNIQUE INDEX idx_pending_s3_deletions_key_bucket ON pending_s3_deletions(s3_key, bucket)`. Schema hygiene.
278 - 5. **[DEFERRED] Split oversized route files** -- exports.rs (842), health.rs (846), tabs/user.rs (815). Chronic from Run 21.
279 - 6. **[DEFERRED] Add README.md to server/** -- Chronic from Run 19.
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
280 286
281 287 ## Action Items
282 288
283 - ### Run 22 (2026-05-09)
289 + ### Run 24 (2026-05-09)
290 +
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.
284 297
285 - 82. **[SERIOUS]** Fix CSRF field name: change `csrf_token` to `_csrf` in `fan_plus.html`, `project.html`, `sandbox.html`
286 - 83. **[MINOR]** Add migration: unique constraint on `idempotency_keys(key, user_id, method, path)` replacing `(key, user_id)`
287 - 84. **[MINOR]** Stream content exports or export to S3 with presigned download (`routes/api/exports.rs:656-680`)
288 - 85. **[MINOR]** Add `UNIQUE(s3_key, bucket)` to `pending_s3_deletions` table (migration)
289 - 86. **[DEFERRED]** Split oversized route files: health.rs (846), exports.rs (842), tabs/user.rs (815) (chronic, from Run 21 #81)
290 - 87. **[DEFERRED]** Add README.md to server/ (chronic, from Run 19 #53 -> #63 -> #80)
298 + ### Run 23 (2026-05-09) -- All Fixed
299 +
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.**
291 309
292 310 ### Open (blocked on upstream)
293 311
@@ -296,61 +314,56 @@ Connection pool exhaustion, file scanning memory, SSE connection accumulation, s
296 314 25. Monitor aws-sdk-s3 for rustls-webpki 0.101.7 fix (RUSTSEC-2026-0049)
297 315 33. bincode unmaintained (RUSTSEC-2025-0141) -- upstream via syntect/yara-x, warning only
298 316
299 - ## Previous Action Item Verification (Run 21)
317 + ## Previous Action Item Verification (Run 23)
300 318
301 319 | # | Item | Status |
302 320 |---|------|--------|
303 - | 65 | Pin scheduler advisory lock to acquired connection | **Fixed** |
304 - | 66 | Enforce PWYW minimum in cart effective_price_cents() | **Fixed** |
305 - | 67 | Fix soft-delete purge: version S3 keys + storage decrement | **Fixed** |
306 - | 68 | Wrap confirm_upload DB writes in transaction | **Fixed** |
307 - | 69 | Optimize discover page: parallelize facet queries | **Fixed** |
308 - | 70 | Delete old S3 objects on image/audio/video replacement | **Fixed** |
309 - | 71 | Validate checkout cancel item_id as UUID | **Fixed** |
310 - | 72 | ClamAV contains("FOUND") -> ends_with("FOUND") | **Fixed** |
311 - | 73 | Case-normalize URL-encoded path traversal check | **Fixed** |
312 - | 74 | Add UNIQUE(s3_key) to pending_uploads | **Fixed** |
313 - | 75 | Add #[instrument] to embed/ handlers | **Fixed** |
314 - | 76 | Add #[instrument] to payments/ functions | **Fixed** |
315 - | 77 | Initialize PWYW amount_cents hidden field server-side | Not verified (low priority) |
316 - | 78 | Use AuthUser for OAuth authorize | Not verified (low priority) |
317 - | 80 | Add README.md to server/ | **Unfixed** (chronic, carried as #87) |
318 - | 81 | Split oversized route files | **Unfixed** (carried as #86) |
319 -
320 - 12 of 14 actionable Run 21 items fixed. 2 deferred items carried forward. No regressions.
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** |
330 +
331 + 9 of 9 Run 23 items verified fixed.
321 332
322 333 ### Chronic Items (unfixed across 3+ consecutive runs)
323 334
324 - | Item | First flagged | Runs unfixed |
325 - |------|--------------|-------------|
326 - | Add README.md to server/ | Run 19 | 4 (19, 20, 21, 22) |
327 -
328 - Note: embed/ and payments/ observability gaps resolved in Run 22 after 2 runs (20, 21). async-trait closed as not removable.
329 -
330 - ## Delta Since Run 21
331 -
332 - ### Fixed
333 - - All 5 SERIOUS Run 21 findings fixed (scheduler lock, cart PWYW, soft-delete purge, confirm transaction, discover queries)
334 - - All 7 MINOR Run 21 findings fixed (old S3 cleanup, cancel UUID, ClamAV FOUND, path traversal case, pending_uploads UNIQUE, embed instrumentation, payments instrumentation)
335 - - embed/ and payments/ now have full `#[instrument]` coverage -- resolves chronic observability gap
336 - - 4 new migrations (102-105): pending_uploads unique, unique_plays, nullable_item_id_indexes, pending_s3_deletions
337 - - Version bumped from 0.5.7 to 0.5.9
338 - - LOC grew from ~87,427 to ~87,853 (+426)
339 - - Test annotations from 1,218 to 1,215 (-3, likely test consolidation)
340 -
341 - ### New Findings (not in Run 21)
342 - - CSRF field name mismatch in 3 templates (SERIOUS)
343 - - Idempotency key unique constraint too narrow (MINOR)
344 - - Content export RAM buffering (MINOR)
345 - - Pending S3 deletions missing UNIQUE constraint (MINOR)
346 -
347 - ### Grade Changes
348 - - Performance: A- -> A (discover page fixed)
349 - - Concurrency: A- -> A (scheduler lock fixed)
350 - - Observability: A- -> A (embed/ + payments/ instrumented)
351 - - Overall: A (held)
352 - - Cold spots: 5 -> 3 (net -2)
353 - - SERIOUS findings: 5 -> 1 (net -4)
335 + None. Both previous chronic items (README.md, oversized route files) resolved in Run 23.
336 +
337 + ### False Positives Identified (Run 24)
338 +
339 + 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.
346 +
347 + ## Delta Since Run 23
348 +
349 + ### Fixed (from Run 23)
350 + All 9 Run 23 action items verified fixed. No regressions detected.
Lines truncated
@@ -10,7 +10,7 @@ Three documentation levels for every feature. Source: codebase harness DB (727 m
10 10
11 11 | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc |
12 12 |---------|-------------|:---:|:---:|:---:|
13 - | **Auth (login/signup/sessions)** | routes/auth.rs, auth.rs, db/sessions.rs, db/auth.rs | Y | -- | guide/01-getting-started |
13 + | **Auth (login/signup/sessions)** | routes/auth.rs, auth.rs, db/sessions.rs, db/auth.rs | Y | -- | guide/getting-started |
14 14 | **Passkeys (WebAuthn)** | routes/api/passkeys.rs, db/passkeys.rs | Y | -- | guide/security |
15 15 | **TOTP (2FA)** | routes/api/totp.rs, db/totp.rs | Y | -- | guide/security |
16 16 | **User profiles** | routes/api/users/profile.rs, db/users.rs | Y | -- | guide/profile |
M server/docs/todo.md +31 -190
@@ -1,12 +1,24 @@
1 1 # Makenotwork TODO
2 2
3 3 ## Status
4 - v0.5.9 deployed 2026-05-09. Audit grade A (Run 22). ~88K LOC, 1,215 tests, 0 warnings. Migration 105. Sprints 1-9 complete (see `todo_done.md`).
4 + v0.5.9 deployed 2026-05-09. Audit grade A (Run 24). ~88K LOC, 1,933 tests, 0 warnings. Migration 107. Sprints 1-9 complete (see `todo_done.md`).
5 5
6 6 Human tasks in `human_todo.md`. Completed items in `todo_done.md`.
7 7
8 8 ---
9 9
10 + ## Next Steps (Soft Launch)
11 +
12 + Priority order. See `human_todo.md` for the full manual testing feature map.
13 +
14 + 1. **Deploy** — Run 24 fixes (4 items) are ready to ship. Build, bump version, deploy.
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
17 + 4. **Invite testers** — generate invite codes, send hand-written emails per `docs/internal/outreach/tiers.md`
18 + 5. **Document undocumented features** — shopping cart, wishlist, creator pause (see below)
19 +
20 + ---
21 +
10 22 ## Documentation: Undocumented Shipped Features
11 23
12 24 These features are implemented but have no public-facing documentation yet. Add docs in the next documentation push.
@@ -26,187 +38,43 @@ These features are implemented but have no public-facing documentation yet. Add
26 38
27 39 ---
28 40
29 - ## Ultra Fuzz Run 22 (2026-05-09)
30 -
31 - ### SERIOUS (fix before launch)
32 - - [x] Fix CSRF field name: change `csrf_token` to `_csrf` in `fan_plus.html:97`, `project.html:209`, `sandbox.html:24`
41 + ## Ultra Fuzz Run 24 (2026-05-09) -- All Fixed
33 42
34 - ### MINOR (current phase)
35 - - [x] Add migration: unique constraint on `idempotency_keys(key, user_id, method, path)` replacing `(key, user_id)` (migration 106)
36 - - [ ] Stream content exports or export to S3 with presigned download (`routes/api/exports.rs:656-680`)
37 - - [x] Add `UNIQUE(s3_key, bucket)` to `pending_s3_deletions` table (migration 107)
43 + - [x] **SERIOUS**: Add `starts_at` validation to guest checkout (`routes/api/guest_checkout.rs:105`). Copied from `cart.rs:150`.
44 + - [x] **SERIOUS**: Fix version replace storage counter rollback (`routes/storage/versions.rs`). Now uses atomic `try_replace_storage` for file replacements.
45 + - [x] MINOR: Change pending_uploads ON CONFLICT to DO NOTHING (`db/pending_uploads.rs:17`)
46 + - ~~MINOR: Blog editor: preserve form input on validation error~~ -- false positive; JS `fetch()` keeps form state on error
47 + - [x] MINOR: Idempotency middleware: avoid double allocation (`metrics.rs:232`)
48 + - [ ] DEFERRED: Extract shared `validate_promo_code()` helper to prevent checkout path divergence
38 49
39 50 ---
40 51
41 - ## Ultra Fuzz Run 21 (2026-05-08)
42 -
43 - ### SERIOUS (fix before launch)
44 - - [x] Pin scheduler advisory lock to acquired connection — hold for tick duration (`scheduler/mod.rs:77-88`)
45 - - [x] Enforce PWYW minimum in cart `effective_price_cents()` — `.max(pwyw_min_cents)` (`db/cart.rs:104-108`)
46 - - [x] Fix soft-delete purge: query version S3 keys + decrement storage before CASCADE (`scheduler/cleanup.rs:188-222`)
47 - - [x] Wrap confirm_upload DB writes in single UPDATE — rollback storage on failure (`routes/storage/uploads.rs:225-248`)
48 - - [x] Optimize discover page: parallelize facet queries with try_join! (`routes/pages/public/discover.rs:292-513`)
49 -
50 - ### MINOR/MEDIUM (current phase)
51 - - [x] Delete old S3 objects on item image/audio/video replacement (`routes/storage/images.rs`, `routes/storage/uploads.rs`)
52 - - [x] Validate checkout cancel `item_id` as UUID (`routes/stripe/checkout/mod.rs:105-108`)
53 - - [x] Change ClamAV `contains("FOUND")` to `ends_with("FOUND")` (`scanning/clamav.rs:96`)
54 - - [x] Case-normalize URL-encoded path traversal check (`scanning/archive.rs:70`)
55 - - [x] Add UNIQUE(s3_key) to pending_uploads table (migration 102)
56 - - [x] Add `#[tracing::instrument(skip_all)]` to embed/ handlers (was mostly done; added item_player)
57 - - [x] payments/ already had #[instrument] on all async fns (webhook extractors are pure, no I/O)
58 - - [x] Initialize PWYW amount_cents hidden field server-side (`templates/pages/purchase.html`)
59 - - [x] Use re-auth for OAuth authorize with legacy sessions (`routes/oauth.rs:253-256`)
60 -
61 - ---
62 -
63 - ## Audit Remediations (2026-05-08, post-Sprint 9)
64 -
65 - - [x] Fix XSS: escape `s.category` and `s.url` in search suggestions innerHTML (discover.html)
66 - - [x] Fix XSS: replace inline `onsubmit` handler with `addEventListener` in collections.js
67 - - [x] Fix a11y: convert `<span onclick>` save buttons to `<button>` with `aria-label` on discover cards
68 - - [x] Performance: rewrite `get_user_purchases` double-nested correlated subquery — replaced with LATERAL joins counting total versions vs downloaded versions
69 - - [x] Remove unused import `spawn_email` in `stripe/checkout/cart.rs`
70 - - [x] Remove dead code `update_app_sync_sub_tier` in `db/app_sync.rs`
71 -
72 - ---
52 + ## Code Fuzz / Ultra Fuzz — Accepted Risks & Deferred
73 53
74 - ## Code Fuzz Findings (2026-05-08)
75 -
76 - Two-pass fuzz: initial scan + deep verification. Items marked REFUTED were disproven in the second pass. Items marked CONFIRMED were verified with exact evidence.
77 -
78 - ### Payments & Checkout
79 - - [x] SERIOUS: Cart refund only cleans up ONE item — changed `fetch_optional` to `fetch_all`, refund handler now iterates all matching transactions (`db/transactions.rs`, `routes/stripe/webhook/billing.rs`)
80 - - [x] MINOR: No buyer != seller validation in cart checkout — added `user.id == seller_id` guard (`routes/stripe/checkout/cart.rs`)
81 - - [x] MINOR: Guest checkout `increment_sales_count` error was silently swallowed — now propagates error so Stripe retries (`routes/stripe/webhook/checkout.rs`)
82 - - [x] MINOR: Tips lack `check_not_suspended()` — added (`routes/stripe/checkout/tips.rs`)
83 - - [x] MINOR: Orphaned pending transactions on partial cart failure — investigated: failed seller's transactions are never created (error precedes create_transaction); earlier sellers' sessions expire naturally; 25h stale cleanup is correct. No additional fix needed.
84 - - [x] MINOR: `CartLineItem.amount_cents` is `i32` while `Cents` wraps `i64` — changed to `i64`, removed redundant cast (`payments/checkout.rs`)
85 - - [x] MINOR: No Stripe minimum amount ($0.50) enforcement before creating session — added `STRIPE_MINIMUM_CHARGE_CENTS` check to all 3 checkout methods (`payments/checkout.rs`, `constants.rs`)
86 - - [x] MINOR: Cart items removed before Stripe session completes — moved cart removal to webhook handler so items survive cancelled checkouts (`routes/stripe/checkout/cart.rs`, `routes/stripe/webhook/checkout.rs`)
87 - - [x] MINOR: Project checkout uses `ItemId::nil()` — made item_id optional in CreateTransactionParams, CheckoutParams, and CheckoutMetadata; project purchases now use NULL; fixed unique indexes to exclude NULL item_id (migration 104)
88 - - [x] MINOR: v2 webhook handler swallows account update failures — on failure, unmarks event from processed set and returns 500 so Stripe retries (`routes/stripe/webhook_v2.rs`, `db/webhook_events.rs`)
89 - - ~~SERIOUS: Partial Stripe refunds revoke full access~~ REFUTED — `is_full_refund()` check at `billing.rs:272` prevents this
90 - - ~~MINOR: $0 items dropped in `process_seller_checkout`~~ REFUTED — unreachable; `process_seller_checkout` is only called with `promo_code: None`
91 -
92 - ### Pricing, Promo Codes & Transactions
93 - - [x] MINOR: Cross-creator promo code collision — added `code_purpose = 'free_access'` filter to `get_promo_code_by_code` (`db/promo_codes.rs`)
94 - - [x] MINOR: `remove_free_item_from_library` doesn't decrement promo code `use_count` — now returns and decrements via `release_use_count` (`db/transactions.rs`)
95 - - [x] MINOR: `claim_free_with_promo_code` doesn't record `promo_code_id` — added to INSERT (`db/transactions.rs`)
96 - - [x] MINOR: Tautology `if s == s { 23 } else { 0 }` in `parse_date` — removed dead branch (`routes/api/promo_codes.rs`)
97 - - [x] MINOR: Duplicate promo code creation returns 500 — added 23505 handling with friendly message (`routes/api/promo_codes.rs`)
98 - - [x] MINOR: Can create already-expired promo codes — added `expires_at > now()` check (`routes/api/promo_codes.rs`)
99 - - [x] MINOR: Subscription trial promo code `use_count` never released on abandoned checkout — subscription checkout now creates pending transaction row, cleaned up by existing 25h stale cleanup (`db/transactions.rs`, `routes/stripe/checkout/subscriptions.rs`, `routes/stripe/webhook/checkout.rs`)
100 - - [x] MINOR: `update_promo_code` allows setting `max_uses` below current `use_count` — added guard rejecting max_uses < current use_count (`routes/api/promo_codes.rs`)
101 - - [x] MINOR: License key generation after promo claim is non-transactional — `claim_free_with_promo_code` now accepts `LicenseKeyParams` and creates key inside the same transaction (`db/transactions.rs`, `routes/api/promo_codes.rs`, `routes/stripe/checkout/item.rs`)
102 - - [x] NOTE: `FixedPricing::validate_amount` has no upper cap — not exploitable, fixed-price checkout uses `item.price_cents` (`pricing.rs:134-143`)
103 -
104 - ### Subscriptions
105 - - [x] SERIOUS: Re-subscription after cancel silently no-ops — changed `ON CONFLICT DO NOTHING` to `DO UPDATE` with reactivation (`db/creator_tiers.rs`, `db/fan_plus.rs`)
106 - - [x] SERIOUS: `handle_subscription_updated` with `status=canceled` never sets `canceled_at` — all 4 `update_*_status` functions now set `canceled_at` via CASE/COALESCE (`db/subscriptions.rs`, `db/creator_tiers.rs`, `db/fan_plus.rs`, `db/app_sync.rs`)
107 - - [x] MINOR: `cancel_subscription` overwrites `canceled_at` on re-cancel — all 4 cancel functions now use `COALESCE(canceled_at, NOW())`
108 - - [x] MINOR: `resume_subscriptions_for_creator` doesn't filter on `status = 'active'` — added `AND status = 'active'` (`db/subscriptions.rs`)
109 - - [x] MINOR: `get_user_subscribed_item_ids` grants access to paused item subs — added `AND paused_at IS NULL` (`db/subscriptions.rs`)
110 - - [x] LOW: `get_project_subscriber_count` counts paused subscriptions — added `AND paused_at IS NULL` (`db/subscriptions.rs`)
111 - - [x] LOW: Grace period upload rejection returns bare 403 — changed to `BadRequest` with descriptive message (`db/creator_tiers.rs`)
112 - - [x] NOTE: Everything tier checkout hard-blocked — guard removed, tier is live and priced (`routes/stripe/checkout/subscriptions.rs`)
113 -
114 - ### Auth & Sessions
115 - - [x] MEDIUM: Passkey registration does not require re-authentication — added password confirmation to `register_start` + JS prompt (`routes/api/passkeys.rs`, `static/passkey.js`)
116 - - [x] MINOR: Password reset doesn't invalidate other sessions — added `delete_all_sessions_for_user` + cache eviction (`routes/pages/email_actions/password.rs`)
117 - - [x] LOW: Email verification HMAC lacks purpose prefix — added `"verify:"` prefix to generate and verify (`email/tokens.rs`)
118 - - [x] NOTE: CSRF exemption comment says SameSite=Strict but cookie is Lax — corrected comment (`csrf.rs:131`)
119 - - [x] NOTE: `authorize_post` duplicates `AuthUser` session validation logic — correct but fragile, accepted (`routes/oauth.rs:222-258`)
54 + Remaining open items from Runs 21-24 and Code Fuzz (2026-05-08). All SERIOUS items resolved through Run 23. Run 24 SERIOUS items above.
120 55
121 56 ### Storage & Uploads
122 - - [x] SERIOUS: Internal CLI upload skips S3 key validation — added `expected_prefix` check matching all other confirm handlers (`routes/api/internal/uploads.rs`)
123 - - [x] SERIOUS: Internal CLI upload increments storage AFTER writing DB record — moved `try_increment_storage` before DB writes, deletes S3 on quota failure (`routes/api/internal/uploads.rs`)
124 - - [x] MINOR: Insertion presign skips `check_presign_allowed` quota check — added quota check before presign (`routes/api/content_insertions.rs`)
125 - - [x] MINOR: Filenames sanitizing to empty create ambiguous S3 keys — fallback to `"file"` basename (`storage.rs`)
126 - - [x] MINOR: Insertion confirm stores client-supplied `mime_type` without re-validation — added `validate_content_type` at confirm (`routes/api/content_insertions.rs`)
127 - - [x] LOW: Media delete swallows S3 error then deletes DB record — now logs S3 errors, decrements storage before DB delete (`routes/storage/media.rs`)
128 - - [x] SERIOUS: Presigned URLs have no S3 lifecycle cleanup — added `pending_uploads` table, reaper job (24h), S3 lifecycle rule (36h) in human_todo (`routes/storage/uploads.rs`)
129 - - [x] MINOR: Storage counter decrement-then-increment for file replacement not transactional — added `try_replace_storage` that atomically decrements old + increments new in a single UPDATE (`db/creator_tiers.rs`, `routes/storage/uploads.rs`)
130 57 - [ ] MINOR: `classify_media` trusts client-supplied `content_type` for size limits — attacker wastes own quota (`media.rs:80-91`)
131 - - [x] MINOR: Creator cannot preview own draft content via stream/download — added `is_creator` bypass for draft, scan status, and payment checks (`routes/storage/downloads.rs`)
132 - - [x] LOW: Play count inflatable via unauthenticated spam — added per-IP rate limit (10 burst, 1/3s) on stream/download routes + unique_play_count tracking for authenticated users (migration 103, `user_plays` table)
133 58 - [ ] NOTE: `extract_s3_key_from_url` may include bucket name for path-style URLs — only used for project images which handle it (`storage.rs:427-446`)
134 59 - [ ] NOTE: Project image old-file cleanup relies on URL-to-key extraction — fragile but functional (`routes/storage/images.rs:169-177`)
135 60
136 61 ### File Scanning
137 - - [x] MEDIUM: Archive decompression read error silently swallowed — on read error, now falls back to claimed size instead of zero (`scanning/archive.rs`)
138 - - [x] MEDIUM: Audio/Insertion files with unrecognized magic bytes pass ALL layers — now Fail like images do (`scanning/content_type.rs`)
139 62 - [ ] MEDIUM: No scanning when scanner=None + trusted user — by-design trust model; needs async background scan mode to fix without breaking upload UX (`scanning/mod.rs:142-151`)
140 - - [x] MINOR: `ends_with("OK")` too permissive for ClamAV response parsing — changed to exact match `response == "stream: OK"` (`scanning/clamav.rs`)
141 63 - [ ] MINOR: Archive scan skips cover images with ZIP magic bytes — layer 1 catches type mismatch; removing skip would be defense-in-depth but no current exploit path (`scanning/archive.rs:29-36`)
142 - - [x] MINOR: Path traversal check misses URL-encoded and Unicode normalization variants — added `%2e%2e`, absolute path, and null byte checks (`scanning/archive.rs`)
143 - - [x] MINOR: Nested archive detection double-decompresses entries — magic bytes now captured during initial decompression pass (`scanning/archive.rs`)
144 - - [x] NOTE: Unrecognized video data gets `Pass` instead of failing — now Fail like images (`scanning/content_type.rs`)
145 - - [x] NOTE: Mach-O binaries get unconditional `Pass` — added suspicious import analysis matching PE/ELF pattern: ptrace, task_for_pid, mach_vm_write/protect, thread_create_running, posix_spawn, dlopen (`scanning/structural.rs`)
146 - - [x] NOTE: Download XSS blocklist missing `image/svg+xml` and `application/xhtml+xml` — added both (`scanning/content_type.rs`)
147 64
148 65 ### DB & Validation
149 - - [x] SERIOUS: `count_discover_items` missing `deleted_at IS NULL` — added filter (`db/discover.rs`)
150 - - [x] MINOR: `get_bundleable_items` includes soft-deleted items — added `deleted_at IS NULL` (`db/bundles.rs`)
151 - - [x] MINOR: `get_item_type_counts`, `get_price_range_counts`, `get_ai_tier_counts` missing `deleted_at IS NULL` — all three fixed (`db/discover.rs`)
152 - - [x] MINOR: `get_tag_counts` missing `deleted_at IS NULL` and `listed = true` — added both (`db/tags.rs`)
153 - - [x] MINOR: `get_all_tag_counts` missing `listed` and `deleted_at` filters — added (`db/tags.rs`)
154 - - [x] MINOR: `search_suggestions` exposes suspended/deactivated usernames — added `suspended_at IS NULL AND deactivated_at IS NULL` (`db/discover.rs`)
155 - - [x] NOTE: `discover_projects` item_count includes soft-deleted items — added `deleted_at IS NULL` to FILTER clause (`db/discover.rs`)
156 - - [x] MINOR: `get_items_by_user` and `count_items_by_user_projects` include soft-deleted items — added `deleted_at IS NULL` (`db/items.rs`)
157 - - [x] MINOR: `bulk_update_price` takes raw `i32` instead of `PriceCents` — changed to `PriceCents` (`db/items.rs`, `routes/api/items/bulk.rs`)
158 - - [x] MINOR: `duplicate_item` slug retry loop has no counter cap — added cap at 100 (`db/items.rs`)
159 - - [x] MINOR: `get_deleted_items_by_project` has no LIMIT clause — added LIMIT 500 (`db/items.rs`)
160 66 - [ ] MINOR: `item_slug_exists` considers soft-deleted items — intentional during 7-day recovery window (`db/items.rs:80-94`)
161 - - [x] MINOR: `get_all_users` accepts arbitrary `limit`/`offset` — clamped to 200 (`db/users.rs`)
162 - - [x] MINOR: Guest checkout auto-attach not in transaction — wrapped user lookup + transaction update in DB transaction (`db/transactions.rs`)
163 - - [x] NOTE: `set_bundle_items` no ownership check at DB layer — added `owner_id` param with bundle + item ownership verification (`db/bundles.rs`)
164 - - [x] NOTE: Idempotency key scope mismatch — widened PK to `(key, user_id, method, path)`, updated ON CONFLICT (migration 106, `db/idempotency.rs`)
165 -
166 - ### Git SSH & Build Runner
167 - - [x] MEDIUM: Unsanitized SSH command passthrough — `exec_git_shell` now receives a reconstructed command from validated components (`git_ssh.rs`)
168 - - [x] MEDIUM: Partial-failure builds create live OTA releases — now returns early with Failed status, no release created (`build_runner.rs`)
169 - - [x] MINOR: `&repo.description[..25]` byte-position slice panics on multi-byte UTF-8 — uses `chars().take(25)` (`git_ssh.rs`)
170 - - [x] LOW: `cmd_ssh_repo_delete` path not canonicalized — added `canonicalize` + `starts_with` check (`git_ssh.rs`)
171 - - [x] MEDIUM: Empty signature on automated builds — build runner now SCPs .sig files, passes to release. Key generation in human_todo (`build_runner.rs`)
172 - - [x] MINOR: TOCTOU race between `has_running_build` and `get_pending_build` — replaced with atomic `claim_pending_build` using `FOR UPDATE SKIP LOCKED` (`db/builds.rs`, `build_runner.rs`)
173 - - [x] LOW: Global build trigger token readable from repo hooks directory — replaced with per-repo HMAC (`build_runner.rs`)
174 - - [x] LOW: `get_latest_release` SQL `::int[]` cast crashes on non-numeric version parts — added regex guard, non-numeric sorts last (`db/ota.rs`)
175 - - [x] LOW: No version uniqueness enforcement on OTA releases — constraint already existed in migration 033, added `ON CONFLICT` handling returning 409 (`db/ota.rs`)
176 - - [x] LOW: S3 key collision on same-version rebuilds — resolved by OTA version uniqueness constraint (`db/ota.rs`)
177 - - [x] LOW: Build log stores unsanitized build output — added ANSI escape stripping (`build_runner.rs`)
178 - - [x] LOW: `StrictHostKeyChecking=accept-new` trusts on first SSH connect — now uses pinned known_hosts when `/opt/makenotwork/ssh/known_hosts` exists (`build_runner.rs`)
179 - - ~~MINOR: `hook_trigger` doesn't check `config.enabled`~~ REFUTED — DB query includes `AND enabled = true`
180 67
181 68 ### Scheduler & Infrastructure
182 - - [x] SERIOUS: Soft-deleted item purge never cleans S3 — added `get_expired_deleted_item_s3_keys` query + S3 delete before DB purge (`scheduler/cleanup.rs`, `db/items.rs`)
183 - - [x] MODERATE: Dead webhook retry storm — added `AND attempts < 5` guard to `get_retryable_events` query (`db/webhook_events.rs`)
184 - - [x] MINOR: WAM client double-slash URL — `trim_end_matches('/')` on base_url (`wam_client.rs`)
185 - - [x] NOTE: `COUNT(*) LIMIT 1` no-op in monitor — changed to `SELECT EXISTS(...)` (`monitor.rs`)
186 - - [x] MODERATE: S3 delete then DB delete crash gap — implemented `pending_s3_deletions` durable queue across all 5 deletion paths (migration 105)
187 - - [x] MINOR: Stale refund escalation sends duplicate alerts — `mark_escalated` now runs before alerts, skips on failure (`scheduler/webhooks.rs`)
188 69 - [ ] NOTE: Announcement emails lost on server restart — no delivery persistence (`scheduler/announcements.rs:59-85`)
189 - - [x] NOTE: Duplicate onboarding emails on step-advance DB failure — advance step before sending email; missing one email beats duplicates (`scheduler/announcements.rs`)
190 - - [x] NOTE: Idempotency middleware drops >1MB response bodies silently — check content-length before consuming body, skip caching for large responses (`metrics.rs`)
191 70 - [ ] NOTE: Unconfigured S3 reports `s3_ok = true` — intentional but masks misconfig (`monitor.rs:56-65`)
192 71 - [ ] NOTE: `X-Forwarded-For` spoofable without Cloudflare — accepted risk (`rate_limit.rs:26-31`)
193 - - ~~MINOR: Advisory lock accumulates across ticks~~ REFUTED — reentrant by design, harmless
194 -
195 - ### Email, Templates, Config & Import
196 - - [x] MEDIUM: Dev-mode email logging exposes sensitive tokens — body redacted from log output (`email/mod.rs`)
197 - - [x] MEDIUM: CSV import has no row count limit — added 100K row cap (`import/csv_converter.rs`)
198 - - [x] LOW: `parse_amount_cents` wrong for negative decimals — fixed sign handling (`import/csv_converter.rs`)
199 - - [x] LOW: `format_revenue` displays negative as `"$-5.00"` — changed to `"-$5.00"` (`formatting.rs`)
200 - - [x] LOW: `slugify` produces unbounded-length output — capped at 128 chars (`formatting.rs`)
201 - - [x] LOW: Dev signing secret uses UUID v4 — changed to 256-bit CSPRNG (`config.rs`)
202 - - [x] NOTE: Email verification HMAC lacks `"verify:"` prefix — fixed in Auth section above (`email/tokens.rs`)
203 - - [x] LOW: Negative `price_cents` produces malformed `price_decimal` in JSON-LD — fixed sign handling (`types/mod.rs`)
204 - - [x] NOTE: `sanitize_field` tab/CR branches are dead code — removed (`import/csv_converter.rs`)
205 - - [x] MEDIUM: Unsubscribe `action` parameter not validated against enum — added `UnsubscribeAction` enum with 9 variants, updated 17 call sites, fixed missing `notify_tip` handler (`email/tokens.rs`, 13 files)
206 - - [x] LOW: Issue reply signature truncated to 64 bits — switched hex to base64url encoding, now 96 bits in same 16 chars (`email/tokens.rs`)
207 - - [x] LOW: Import tier `price_cents` silently clamped from i64 to i32 — replaced with `try_into()` + descriptive error (`import/pipeline.rs`)
208 - - [x] LOW: Import `strip_html_tags` has no output length limit — capped at 512KB (`import/pipeline.rs`)
209 - - [x] LOW: Lossy i64-to-u32 casts in WaveStats/DbDiscoverItemRow — replaced with saturating casts (`types/conversions.rs`)
72 +
73 + ---
74 +
75 + ## S3/DB Crash-Gap Fix: `pending_s3_deletions` Durable Queue -- DONE
76 +
77 + All 5 paths migrated, scheduler retry job wired. See `todo_done.md` for details.
210 78
211 79 ---
212 80
@@ -223,38 +91,11 @@ Two-pass fuzz: initial scan + deep verification. Items marked REFUTED were dispr
223 91
224 92 ### Media Player
225 93 - [ ] Video URL fallback on S3 presign failure (migration: `video_url` column)
226 - - [x] Arrow key seek should use virtual timeline (segment-aware) — extracted `seekToVirtualMs`, keyboard handler uses virtual time in segment mode
227 -
228 - ### S3/DB Crash-Gap Fix: `pending_s3_deletions` Durable Queue
229 -
230 - S3 deletes then DB deletes create a crash gap: if the server dies between the two steps,
231 - the DB references deleted S3 objects (or S3 objects are orphaned with no DB reference).
232 -
233 - **Affected paths (5 total):**
234 - 1. `scheduler/cleanup.rs` — `cleanup_user_s3_and_delete` (user CASCADE, main + synckit buckets)
235 - 2. `scheduler/cleanup.rs` — `purge_expired_deleted_items` (soft-delete purge, main bucket)
236 - 3. `routes/ota.rs:250-257` — `delete_release` (OTA artifacts, synckit bucket)
237 - 4. `routes/storage/media.rs:374-384` — media file delete (main bucket)
238 - 5. `routes/api/content_insertions.rs:284-290` — insertion delete (main bucket)
239 -
240 - **Implementation:**
241 -
242 - - [x] Migration 105: `pending_s3_deletions` table
243 - - [x] DB module `db/pending_s3_deletions.rs`: enqueue, remove, get_stale
244 - - [x] `cleanup_user_s3_and_delete`: enqueue before S3/CASCADE, bail on failure
245 - - [x] `purge_expired_deleted_items`: enqueue before S3/purge, bail on failure
246 - - [x] `delete_release` (ota.rs): enqueue before S3/DB delete
247 - - [x] media file delete: enqueue before S3/DB delete
248 - - [x] insertion delete: enqueue before S3/DB delete
249 - - [x] Scheduler retry job `retry_pending_s3_deletions` (10min, batch 100, sandbox cadence)
250 - - [x] Wired into `scheduler/mod.rs` sandbox block
251 -
252 - **Edge cases:** S3 deletes are idempotent (404 = success). Duplicate rows are harmless. Enqueue failure = bail before any destructive work. Crash between enqueue and S3 = retry job picks up. Crash after S3 but before DB = next scheduler tick re-processes (re-enqueue is harmless). Advisory lock prevents concurrent schedulers.
253 94
254 95 ### Code Quality
255 96 - ~~Remove `async-trait`~~ — kept: all 3 traits are used as `dyn` objects; Rust 2024 async fn in traits is not dyn-compatible. `async-trait` is the correct tool until `dyn async fn` stabilizes (RFC 3245)
256 - - [ ] Add README.md to server/
257 - - [ ] Split oversized route files: exports.rs, license_keys.rs, health.rs, tabs/user.rs
97 + - [x] Add README.md to server/ (Run 23)
98 + - [x] Split oversized route files: exports.rs, health.rs, tabs/user.rs (Run 23)
258 99 - [ ] Monitor scheduler.rs, git/mod.rs for growth
259 100
260 101 ### Feature Completeness
@@ -365,7 +206,7 @@ MNW/server/src/
365 206 import/ (CSV converter, pipeline, intermediate format)
366 207 MNW/server/tests/
367 208 integration.rs, harness/, workflows/*.rs
368 - MNW/server/migrations/ (001-096)
209 + MNW/server/migrations/ (001-107)
369 210 MNW/server/templates/
370 211 MNW/server/deploy/
371 212 MNW/server/site-docs/public/, MNW/server/site-docs/unpublished/
@@ -331,3 +331,62 @@ All 6 docs updated with feature map cross-reference after /map-features audit.
331 331 - [x] License Keys docs — added offline verify endpoint, license.txt, revocation, PWYW, /api/v1/ prefix
332 332 - [x] SyncKit docs — added SSE, key rotation, subscription gating, batch_id, sync status
333 333 - [x] FAQ — added For Developers section, tier features, custom domains, data retention, content moderation
334 +
335 + ---
336 +
337 + ## Ultra Fuzz Run 22 (2026-05-09)
338 +
339 + - [x] Fix CSRF field name: change `csrf_token` to `_csrf` in `fan_plus.html:97`, `project.html:209`, `sandbox.html:24`
340 + - [x] Add migration: unique constraint on `idempotency_keys(key, user_id, method, path)` replacing `(key, user_id)` (migration 106)
341 + - [x] Add `UNIQUE(s3_key, bucket)` to `pending_s3_deletions` table (migration 107)
342 +
343 + ---
344 +
345 + ## Ultra Fuzz Run 21 (2026-05-08)
346 +
347 + ### SERIOUS
348 + - [x] Pin scheduler advisory lock to acquired connection — hold for tick duration (`scheduler/mod.rs:77-88`)
349 + - [x] Enforce PWYW minimum in cart `effective_price_cents()` — `.max(pwyw_min_cents)` (`db/cart.rs:104-108`)
350 + - [x] Fix soft-delete purge: query version S3 keys + decrement storage before CASCADE (`scheduler/cleanup.rs:188-222`)
351 + - [x] Wrap confirm_upload DB writes in single UPDATE — rollback storage on failure (`routes/storage/uploads.rs:225-248`)
352 + - [x] Optimize discover page: parallelize facet queries with try_join! (`routes/pages/public/discover.rs:292-513`)
353 +
354 + ### MINOR/MEDIUM
355 + - [x] Delete old S3 objects on item image/audio/video replacement (`routes/storage/images.rs`, `routes/storage/uploads.rs`)
356 + - [x] Validate checkout cancel `item_id` as UUID (`routes/stripe/checkout/mod.rs:105-108`)
357 + - [x] Change ClamAV `contains("FOUND")` to `ends_with("FOUND")` (`scanning/clamav.rs:96`)
358 + - [x] Case-normalize URL-encoded path traversal check (`scanning/archive.rs:70`)
359 + - [x] Add UNIQUE(s3_key) to pending_uploads table (migration 102)
360 + - [x] Add `#[tracing::instrument(skip_all)]` to embed/ handlers
361 + - [x] Initialize PWYW amount_cents hidden field server-side (`templates/pages/purchase.html`)
362 + - [x] Use re-auth for OAuth authorize with legacy sessions (`routes/oauth.rs:253-256`)
363 +
364 + ---
365 +
366 + ## Audit Remediations (2026-05-08, post-Sprint 9)
367 +
368 + - [x] Fix XSS: escape `s.category` and `s.url` in search suggestions innerHTML (discover.html)
369 + - [x] Fix XSS: replace inline `onsubmit` handler with `addEventListener` in collections.js
370 + - [x] Fix a11y: convert `<span onclick>` save buttons to `<button>` with `aria-label` on discover cards
371 + - [x] Performance: rewrite `get_user_purchases` double-nested correlated subquery — replaced with LATERAL joins
372 + - [x] Remove unused import `spawn_email` in `stripe/checkout/cart.rs`
373 + - [x] Remove dead code `update_app_sync_sub_tier` in `db/app_sync.rs`
374 +
375 + ---
376 +
377 + ## Code Fuzz Findings (2026-05-08)
378 +
379 + Two-pass fuzz: 100+ findings across payments, subscriptions, auth, storage, scanning, DB, git, scheduler, email/import. All SERIOUS and MEDIUM items fixed. See git history (commits `42405fe`, `f0e40b7`, `fed8192`, `80bfdd9`, `bac01d6`, `ba3bbac`) for full details. Key fixes:
380 +
381 + - Cart refund iterates all matching transactions (was single fetch)
382 + - Re-subscription after cancel now reactivates (was DO NOTHING)
383 + - `handle_subscription_updated` sets `canceled_at` across all 4 subscription types
384 + - Internal CLI upload validates S3 key prefix + storage order
385 + - Soft-deleted item purge cleans S3 via `pending_s3_deletions` durable queue (migration 105)
386 + - Passkey registration requires re-authentication
387 + - Password reset invalidates all other sessions
388 + - Build runner: atomic claim, per-repo HMAC, pinned known_hosts, ANSI stripping
389 + - Promo codes: collision fix, use_count release, transactional license keys
390 + - Unsubscribe action validated against enum (9 variants, 17 call sites)
391 + - CSV import: row cap, amount parsing, HTML strip limit
392 + - Archive scanning: URL-encoded path traversal, nested archive, content type hardening
@@ -0,0 +1,36 @@
1 + <div style="max-width: 400px; text-align: center;">
2 + <header style="margin-bottom: 2rem;">
3 + <div style="width: 100px; height: 100px; background: var(--light-background); margin: 0 auto 1rem; display: flex; align-items: center; justify-content: center; font-size: 2.5rem; opacity: 0.5; font-family: var(--font-heading);">AK</div>
4 + <h1 style="font-size: 1.8rem; margin-bottom: 0.25rem; font-family: var(--font-heading);">Ada Karras</h1>
5 + <div style="font-size: 0.9rem; opacity: 0.7; margin-bottom: 0.75rem;">adakarras</div>
6 + <p style="font-size: 0.95rem; max-width: 340px; margin: 0 auto;">Ambient composer and field recordist. New album every spring. Samples and stems available for remix.</p>
7 + <div style="margin-top: 0.75rem; display: flex; gap: 0.75rem; justify-content: center; align-items: center;">
8 + <button class="secondary" style="font-size: 0.85rem; padding: 0.4rem 0.8rem;">Follow (48)</button>
9 + </div>
10 + <div style="margin-top: 0.5rem; display: flex; gap: 1rem; justify-content: center; font-size: 0.85rem;">
11 + <span style="opacity: 0.7;">RSS Feed</span>
12 + <span style="opacity: 0.7;">Copy link</span>
13 + </div>
14 + </header>
15 +
16 + <section style="margin-bottom: 1.5rem;">
17 + <div class="card" style="text-align: center; padding: 0.75rem 1rem;">
18 + <div style="font-family: var(--font-heading); font-weight: bold; font-size: 1rem;">bandcamp.com/adakarras</div>
19 + <div style="font-size: 0.8rem; opacity: 0.7;">Back catalog and vinyl</div>
20 + </div>
21 + </section>
22 +
23 + <section>
24 + <h2 class="section-header" style="font-size: 1rem; text-align: left;">Projects</h2>
25 + <a class="card" style="display: block; text-decoration: none; color: inherit;">
26 + <div style="font-family: var(--font-heading); font-weight: bold; font-size: 1.05rem; margin-bottom: 0.25rem;">Liminality</div>
27 + <div style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.25rem;">Music &middot; 8 items</div>
28 + <div style="font-size: 0.85rem; opacity: 0.8;">Field recordings and processed ambient. Spring 2026.</div>
29 + </a>
30 + <a class="card" style="display: block; text-decoration: none; color: inherit;">
31 + <div style="font-family: var(--font-heading); font-weight: bold; font-size: 1.05rem; margin-bottom: 0.25rem;">Sample Library</div>
32 + <div style="font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.25rem;">Sample &middot; 24 items</div>
33 + <div style="font-size: 0.85rem; opacity: 0.8;">Field recordings, textures, and atmospheres for your productions.</div>
34 + </a>
35 + </section>
36 + </div>
@@ -0,0 +1,19 @@
1 + <div style="max-width: 500px;">
2 + <div class="form-group">
3 + <label>Project Name</label>
4 + <input type="text" value="Liminality">
5 + </div>
6 +
7 + <div class="form-group">
8 + <label>Description</label>
9 + <textarea rows="3">Field recordings and processed ambient. Eight tracks exploring the spaces between silence and sound.</textarea>
10 + </div>
11 +
12 + <div class="form-group">
13 + <label>Category</label>
14 + <input type="text" value="Music" placeholder="What kind of project is this?">
15 + <div class="hint">Choose an existing category or type a new one.</div>
16 + </div>
17 +
18 + <button class="primary">Save Changes</button>
19 + </div>
@@ -0,0 +1,58 @@
1 + <div style="max-width: 500px;">
2 + <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem 1.5rem;">
3 + <div class="form-group" style="margin-bottom: 0;">
4 + <label style="font-size: 0.85rem;">Type</label>
5 + <select style="width: 100%;">
6 + <option selected>Discount</option>
7 + <option>Free Access</option>
8 + <option>Free Trial</option>
9 + </select>
10 + </div>
11 + <div class="form-group" style="margin-bottom: 0;">
12 + <label style="font-size: 0.85rem;">Code</label>
13 + <input type="text" value="LAUNCH50" style="text-transform: uppercase;">
14 + </div>
15 + </div>
16 +
17 + <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem 1.5rem; margin-top: 0.75rem;">
18 + <div class="form-group" style="margin-bottom: 0;">
19 + <label style="font-size: 0.85rem;">Discount type</label>
20 + <select style="width: 100%;">
21 + <option selected>Percentage</option>
22 + <option>Fixed ($)</option>
23 + </select>
24 + </div>
25 + <div class="form-group" style="margin-bottom: 0;">
26 + <label style="font-size: 0.85rem;">Amount</label>
27 + <input type="text" value="50">
28 + </div>
29 + </div>
30 +
31 + <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem 1.5rem; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border);">
32 + <div class="form-group" style="margin-bottom: 0;">
33 + <label style="font-size: 0.85rem;">Max uses</label>
34 + <input type="text" placeholder="Unlimited">
35 + </div>
36 + <div class="form-group" style="margin-bottom: 0;">
37 + <label style="font-size: 0.85rem;">Starts</label>
38 + <input type="text" value="2026-05-15">
39 + </div>
40 + <div class="form-group" style="margin-bottom: 0;">
41 + <label style="font-size: 0.85rem;">Expires</label>
42 + <input type="text" value="2026-06-15">
43 + </div>
44 + <div class="form-group" style="margin-bottom: 0; grid-column: 1 / -1;">
45 + <label style="font-size: 0.85rem;">Scope</label>
46 + <select style="width: 100%;">
47 + <option selected>All items in project</option>
48 + <option>Liminality (album)</option>
49 + <option>Sample Library Vol. 1</option>
50 + </select>
51 + </div>
52 + </div>
53 +
54 + <div style="margin-top: 1rem;">
55 + <button class="secondary">Create Code</button>
56 + </div>
57 + <p class="hint" style="margin-top: 0.5rem;">Percentage: 1-100. Fixed: dollar amount (e.g. 5 = $5.00 off). Free access codes auto-generate if code left blank.</p>
58 + </div>
@@ -37,7 +37,7 @@ Each creator costs money to serve. The amount depends on the tier, because deliv
37 37 | Big Files | $30/mo | $4-9/mo | $21-26/mo |
38 38 | Everything | $60/mo | $5-10/mo | $50-55/mo |
39 39
40 - Costs include storage, CDN, transcoding, and shared infrastructure. The ranges reflect that a creator uploading weekly to a large audience costs more than one uploading monthly to a small one.
40 + Costs include storage, CDN, and shared infrastructure. The ranges reflect that a creator uploading weekly to a large audience costs more than one uploading monthly to a small one.
41 41
42 42 Flat pricing means high-activity creators are subsidized by the average. Your tier fee shouldn't punish you for success. See the [pricing calculator](/pricing) to compare what you'd keep here versus other platforms at any revenue level.
43 43
@@ -119,7 +119,7 @@ A note from the founder:
119 119
120 120 ## This Definition Will Change
121 121
122 - The generative AI landscape moves fast. This policy is a living document. When we update it, we will:
122 + This field moves fast. This policy is a living document. When we update it, we will:
123 123
124 124 - Publish the change with a version date
125 125 - Explain what changed and why
@@ -7,7 +7,7 @@ Flat fee. All your revenue passes through to you.
7 7 ## For Creators
8 8
9 9 1. **Sign up** and choose a pricing tier ($10-$60/month based on content type)
10 - 2. **Upload content**: text, audio, software, or digital files
10 + 2. **Upload content**: text, audio, video, software, or digital files
11 11 3. **Organize** using hierarchical tags and projects
12 12 4. **Set pricing**: free, pay-what-you-want, fixed price, or membership
13 13 5. **Get paid**: 0% platform fee, only payment processing fees
@@ -69,53 +69,9 @@ The gap widens as you grow. A percentage-cut platform is most expensive exactly
69 69 | **Big Files** | $30 | Video, games, large software | 20GB | 500GB |
70 70 | **Everything** | $60 | All features, current and future | 20GB | 500GB |
71 71
72 - All files (content, covers, downloads, supplementary materials) count toward total storage. Big Files and Everything creators can request a per-file size increase beyond 20GB from their dashboard.
72 + Every tier includes 0% platform fee on fan payments, custom profile, project organization, data export, memberships, RSS, promo codes, and 2FA/passkeys. Use the [pricing calculator](/pricing) to compare what you'd keep versus other platforms.
73 73
74 - Prices reflect what it costs to store and deliver each content type. Use the [pricing calculator](/pricing) to see what you'd keep at any revenue level, with a side-by-side comparison against other platforms.
75 -
76 - ### Choosing Your Tier
77 -
78 - | If you create... | Choose | You get |
79 - |------------------|--------|---------|
80 - | Blog posts, articles, newsletters | Basic ($10) | 50GB storage, 10MB/file |
81 - | Music, podcasts, audio | Small Files ($20) | 250GB storage, 500MB/file |
82 - | Software, plugins, sample packs | Small Files ($20) | 250GB storage, 500MB/file |
83 - | Games, large applications | Big Files ($30) | 500GB storage, 20GB/file |
84 - | Video content, courses | Big Files ($30) | 500GB storage, 20GB/file |
85 - | All current and future features + live streaming (roadmap) | Everything ($60) | 500GB storage, 20GB/file |
86 -
87 - ### What Every Tier Includes
88 -
89 - - Storage and per-file limits based on your tier (see table above)
90 - - Custom profile page (`/u/yourname`)
91 - - Project organization (`/p/projectname`)
92 - - Permanent content links (`/i/UUID`)
93 - - Complete data export (projects, items, blog posts, transactions)
94 - - Direct fan payments (0% platform fee)
95 - - Contact sharing with fans (opt-in at purchase)
96 - - RSS feed generation (project + blog feeds)
97 - - License keys, discount codes, download codes
98 - - Pay-what-you-want pricing option
99 - - Membership tiers with automated billing
100 - - Follows, broadcast emails, email notifications (sales, followers, releases, logins)
101 - - 2FA/TOTP, passkeys/WebAuthn, session management, account lockout
102 -
103 - ### Earn-Back Credit Program
104 -
105 - On the [roadmap](./roadmap.md#earn-back-credit-program). If your revenue doesn't cover your tier fees over 12 months, the difference is credited as free months the following year.
106 -
107 - ### Add-Ons
108 -
109 - Base tiers include all platform software features. Cloud sync, automated email, and DSP distribution have direct infrastructure costs and will be available as monthly add-ons at cost. See the [roadmap](./roadmap.md) for status.
110 -
111 - ### Billing
112 -
113 - - Monthly billing only, no annual contracts
114 - - Cancel anytime, effective immediately
115 - - No setup fees, no hidden charges
116 - - Upgrade or downgrade at any time
117 -
118 - See our [written guarantees](./guarantees.md) for pricing commitments.
74 + Monthly billing, no contracts, cancel anytime. An [earn-back credit program](./roadmap.md#earn-back-credit-program) is on the roadmap. See [Pricing Tiers](../guide/tiers.md) for full feature breakdowns, storage details, and tier selection guidance. See our [written guarantees](./guarantees.md) for pricing commitments.
119 75
120 76 ---
121 77
@@ -84,7 +84,7 @@ Everything listed here is live and working.
84 84 ### Platform
85 85
86 86 - **Source-available codebase**: PolyForm Noncommercial 1.0.0
87 - - **Creator applications**: Apply to create; most applications approved within a few days
87 + - **Creator applications**: Apply to create; most applications approved within 1 business day
88 88 - **Admin tools**: Waitlist management, creator approval, suspension/appeal processing, revenue reports, data export
89 89 - **Rich link previews**: Your content shows up properly when shared on social media, search engines, and podcast apps
90 90 - **Documentation**: Creator guide covering the full platform
@@ -100,7 +100,7 @@ Everything listed here is live and working.
100 100 - **Creator tier enforcement**: Storage tracking and per-tier limits with grace period
101 101 - **Health monitoring**: Uptime tracking, service connectivity checks
102 102 - **Malware scanning**: Every uploaded file is scanned for malware before it's made available
103 - - **Comprehensive automated test suite** covering all platform features
103 + - **Automated test suite** covering all platform features
104 104
105 105 ### Developer Infrastructure (SyncKit)
106 106
@@ -14,7 +14,7 @@ When a platform takes venture capital, the pressure compounds. Investors need re
14 14
15 15 ## The Alternative
16 16
17 - We charge a flat monthly fee based on what you need to host: $10 for text, $20 for audio and software, $30 for video and large files, $60 for live streaming and all current and future features. We take 0% of your revenue. The only deduction from fan payments is the payment processor's fee (~3%), which goes to the processor, not us.
17 + We charge a flat monthly fee based on what you need to host: $10 for text, $20 for audio and software, $30 for video and large files, $60 for all features, current and future. We take 0% of your revenue. The only deduction from fan payments is the payment processor's fee (~3%), which goes to the processor, not us.
18 18
19 19 Your tier fee funds the platform. No incentive to take a cut of your sales, show ads to your fans, or lock you in.
20 20
@@ -16,7 +16,7 @@ What are you making, and how should fans experience it?
16 16 | Sample packs or presets | Digital items | Any file format, download tracking |
17 17 | A course | Mixed item types in a project | Combine text lessons, audio, and downloads |
18 18
19 - The type determines which player/viewer fans get and what features are available (chapters, versioning, etc.).
19 + The type determines which player/viewer fans get and which features are available (chapters, versioning, license keys).
20 20
21 21 ## Items
22 22
@@ -28,12 +28,14 @@ For software products, license keys are generated automatically on purchase:
28 28
29 29 License keys appear in the fan's library after purchase.
30 30
31 - ## Discount Codes
31 + ## Promo Codes
32 32
33 - Create promotional codes with:
33 + Create promotional codes for discounts, free trials, and free access:
34 34
35 35 - **Percentage or fixed amount** off the price
36 - - **Scope**: Apply to a specific item or all your items
36 + - **Free trial** periods for memberships
37 + - **Free access** grants for individual items
38 + - **Scope**: Apply to a specific item, project, or all your items
37 39 - **Usage limits**: Cap how many times a code can be used
38 40 - **Expiration dates**: Codes stop working after a date you set
39 41 - **Auto-apply via URL**: Share a link with `?discount=CODE` to pre-fill the code
@@ -6,7 +6,7 @@ Strategies for getting the most out of Makenot.work.
6 6
7 7 ### Start with Your Project
8 8
9 - - **Single-creator projects** are straightforward: your name, your work, your earnings.
9 + - **Single-creator projects**: your name, your work, your earnings.
10 10 - **Collaborative projects** need upfront agreement on splits and who can publish what.
11 11 - **Multiple projects** make sense if you have distinct bodies of work (a music project and a writing project, for instance).
12 12
@@ -84,7 +84,7 @@ Tell fans what they're getting: how often you release, what membership includes,
84 84
85 85 ### Have a Refund Policy
86 86
87 - You control refund decisions. Consider adding a brief refund policy to your profile or project description. For digital goods, a reasonable policy might be:
87 + You control refund decisions. Consider adding a brief refund policy to your profile or project description. Examples of refund policies for digital goods:
88 88
89 89 - **No refunds after download** (standard for digital content)
90 90 - **Refunds within 24-48 hours if not downloaded** (generous, builds trust)
@@ -30,7 +30,7 @@ Pay once, own forever. Download in original format, re-download anytime, no DRM.
30 30
31 31 ### Memberships
32 32
33 - Monthly billing, cancel anytime. Access member-only content while subscribed; some creators grant permanent access to items released during your membership.
33 + Monthly billing, cancel anytime. Access member-only content while subscribed. Creators can grant permanent access to items released during your membership.
34 34
35 35 ### Pay-What-You-Want
36 36
@@ -24,14 +24,14 @@ To sell your work, apply for creator access:
24 24 1. Go to the [creators page](/creators) or your dashboard
25 25 2. Tell us what you make (20-500 characters). A portfolio or storefront link helps
26 26 3. Choose which tier fits your content type
27 - 4. Optionally, request a **free trial** (2-6 weeks)
27 + 4. Optionally, request a **free trial** (2 weeks to 3 months)
28 28 5. Submit your application
29 29
30 30 Most applications are approved within 1 business day. All applications receive a response within 5 business days.
31 31
32 32 ### Free Trials
33 33
34 - Check "Request a free trial" when you apply. Pick a trial length (2, 4, or 6 weeks) and briefly describe what you want to test. No credit card required. At the end, join a tier to keep your content live or export everything and walk away.
34 + Check "Request a free trial" when you apply. Pick a trial length (2 weeks, 1 month, 2 months, or 3 months) and briefly describe what you want to test. No credit card required. At the end, join a tier to keep your content live or export everything and walk away.
35 35
36 36 **Trial details:**
37 37 - Trials are available by application only. Check the box when you apply for creator access.
@@ -103,7 +103,7 @@ Projects organize your work -- albums, podcast feeds, or product lines.
103 103
104 104 **You are the merchant.** Payments go directly to your Stripe account. That means chargebacks ($15 dispute fee) come from your balance, not ours. A $5 refund is always cheaper than a disputed charge. See [Payouts](./payouts.md) for details.
105 105
106 - **Stripe is currently the only payment processor.** If Stripe doesn't operate in your country or suspends your account, you can still manage content and export data, but you won't receive payments until the issue is resolved. Check [Stripe's supported countries](https://stripe.com/global) before applying.
106 + **Stripe is currently the only payment processor.** If Stripe doesn't operate in your country or suspends your account, you can still manage content and export data, but you won't receive payments until the issue is resolved. See [Receiving Payouts](./payouts.md) for supported countries, payout schedules, and troubleshooting.
107 107
108 108 **This is a one-person operation.** Source code is public, all data is exportable, and fan payments are held in your Stripe account (not ours). Support responses happen during business hours, not 24/7. See [What We Guarantee](../about/guarantees.md) for the continuity plan.
109 109
@@ -75,7 +75,7 @@ Unpublish anytime to revert an item to draft. All settings and data are retained
75 75
76 76 From the project dashboard, select multiple items to:
77 77
78 - - **Publish**: Make several items public at once
78 + - **Publish**: Make multiple items public at once
79 79 - **Unpublish**: Revert multiple items to draft
80 80 - **Delete**: Remove multiple items permanently
81 81
@@ -63,11 +63,13 @@ When customization ships, it will be available on every tier.
63 63
64 64 When someone visits your profile, they see:
65 65
66 - 1. **Header image** (if set) spanning the top
67 - 2. **Profile picture** and **display name**
68 - 3. **Bio** text
69 - 4. **Links** as clickable buttons
70 - 5. **Projects** listed below, each showing its cover art, title, and description
66 + 1. **Profile picture** and **display name**
67 + 2. **Bio** text
68 + 3. **Follow button** and follower count
69 + 4. **Custom links** as clickable cards
70 + 5. **Projects** listed below, each showing title, category, item count, and description
71 +
72 + > [!UI] profile-layout
71 73
72 74 Most fans click through to a specific project within seconds, so your bio and links need to do their work fast.
73 75