Skip to main content

max / makenotwork

Audit Run 18: type safety, moderation types, sandbox fix, inline SQL extraction Bug fix: sandbox creator_tier 'SmallFiles' -> 'small_files' (silent parse failure). Type safety: - MtThreadId, ClaimToken, DownloadToken newtypes (db/id_types.rs) - ModerationActionId + ModerationActionType enum (replaces raw Uuid/String) - CheckoutType enum for Stripe metadata (replaces stringly-typed checkout_type) - PriceCents in create_item/update_item signatures (replaces raw i32) Architecture: - Extract inline SQL from 3 route handlers to db/ layer (claim_free_project, count_public_listed, get_user_content_size) Convention fixes: - Replace .unwrap() in git/raw.rs, helpers.rs, monitor.rs - Add #[instrument] to api::public_projects Also includes prior uncommitted work: code fuzz fixes, test fuzz hardening, moderation actions (migration 085), sync log compaction cursor (migration 084), promo code enhancements, docs updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-01 21:32 UTC
Commit: 82b8eb74ce34c9c72e6f0235d657ec140ee8ff75
Parent: 3d8ffe3
69 files changed, +1817 insertions, -895 deletions
@@ -3385,7 +3385,7 @@ dependencies = [
3385 3385
3386 3386 [[package]]
3387 3387 name = "makenotwork"
3388 - version = "0.4.5"
3388 + version = "0.4.6"
3389 3389 dependencies = [
3390 3390 "anyhow",
3391 3391 "argon2",
@@ -4,6 +4,18 @@ Full chronological audit log. See [audit_review.md](./audit_review.md) for curre
4 4
5 5 ## Changes Since Last Audit
6 6
7 + ### Thirty-ninth audit (2026-05-01, Run 18 MNW server)
8 + - **Test count:** 1,933 (1,209 unit + 724 integration). 34 integration failures (uncommitted moderation/promo code changes). 0 clippy warnings.
9 + - **Grade:** A (maintained). v0.4.6. ~80,470 LOC.
10 + - **Growth:** +1,136 LOC, +72 tests since Run 17.
11 + - **New features since Run 17:** Moderation actions table (migration 085), sync log compaction cursor (migration 084), admin moderation UI, promo code enhancements, content scanning improvements.
12 + - **Cold spots:** 5 found — moderation.rs type safety (B), git/raw.rs unwraps (B), analytics.rs duplication (B+), wam_client.rs testing (B), admin CSRF (B).
13 + - **Bug found:** Sandbox `creator_tier` mismatch — `'SmallFiles'` in SQL vs `'small_files'` expected by `impl_str_enum!`. Sandbox users silently lose tier privileges.
14 + - **Mandatory surprise:** Sandbox tier bug (above).
15 + - **Architecture findings:** 4 instances of inline SQL in route handlers (should be in db/ layer).
16 + - **New action items:** 8 (1 high, 4 medium, 3 low) + 3 deferred.
17 + - **Previous items verified:** 4 upstream-blocked deps unchanged. All resolved items confirmed intact.
18 +
7 19 ### Thirty-eighth audit (2026-04-30, Run 17 cross-project)
8 20 - **Test count:** 1,861 (1,139 unit + 722 integration). 0 failures. 0 clippy warnings.
9 21 - **Grade:** A (maintained). v0.4.5. ~79,334 LOC.
@@ -1,336 +1,180 @@
1 1 # MakeNotWork -- Audit Review
2 2
3 - **Last audited:** 2026-04-30 (thirty-eighth audit, Run 17 cross-project)
4 - **Previous audit:** 2026-04-18 (Run 15, corrected 2026-04-22)
3 + **Last audited:** 2026-05-01 (Run 18, MNW server only)
4 + **Previous audit:** 2026-04-30 (Run 17, cross-project)
5 5
6 6 ## Overall Grade: A
7 7
8 - Run 17: 1,861 tests (1,139 unit + 722 integration, all pass). 0 clippy warnings. v0.4.5. ~79,334 LOC. No cold spots. All previous action items verified. Significant growth since Run 15 (~67K -> ~79K LOC, ~1,359 -> ~1,861 tests).
8 + Run 18: 1,933 tests (1,209 unit + 724 integration). 0 clippy warnings. v0.4.6. ~80,470 LOC. 5 cold spots (1 bug, 4 minor). 34 integration test failures (likely related to uncommitted moderation/promo code changes).
9 9
10 10 ## Scorecard
11 11
12 12 | Dimension | Grade | Notes |
13 13 |-----------|:-----:|-------|
14 - | Code Quality | A | Minimal unwraps outside tests, consistent error handling, no dead code |
15 - | Architecture | A | Clean layer separation (db/routes/payments/templates/types), trait-based testability |
16 - | Testing | A | 1,861 tests (1,139 unit + 722 integration), ~15.0 unit/KLOC, proptest active |
17 - | Security | A+ | Argon2id, CSRF, CSP, HSTS, constant-time compare, HIBP, 6-layer malware scanning, ammonia HTML sanitization |
18 - | Performance | A- | Paginated discover, batch queries, CDN cache headers; dashboard lists intentionally unbounded |
19 - | Documentation | A | Module-level //! on every file, response conventions documented in api/mod.rs |
20 - | Dependencies | A | Rust 2024 edition, recent crate versions, vendored OpenSSL for cross-compilation |
21 - | Frontend | A | Askama auto-escape, json_escape for JSON-LD, no raw innerHTML |
22 - | Type Safety | A+ | 35 UUID newtypes, 7 validated string types, Cents monetary newtype, domain enums via macro |
23 - | Observability | A | 962 #[instrument] annotations, Prometheus metrics, structured JSON logging, request IDs |
24 - | Concurrency | A | DB transactions for critical paths, advisory locks for IP-based sandbox cap, retry loops for slug uniqueness |
25 - | Resilience | A+ | Graceful shutdown with hard deadline, migration failure exit code 2, health monitor with status-transition alerts |
26 - | API Consistency | A | Documented response shape conventions, API version header, json_error_layer |
27 - | Migration Safety | A | 83 additive migrations, IF EXISTS on drops, data-only migrations are simple |
28 - | Codebase Size | A- | 79K LOC is substantial but well-organized; wordlist.rs (2,056 lines) is a data file |
29 - | Infrastructure | -- | Not yet audited. Checklist below. |
14 + | Code Quality | A | 3 production `.unwrap()` in git/raw.rs + 1 in helpers.rs (convention violations, not crash risks) |
15 + | Architecture | A- | 4 instances of inline SQL in route handlers (stripe/checkout, dashboard/forms, landing) |
16 + | Testing | A | 1,933 tests, 34 integration failures (uncommitted changes), proptest active |
17 + | Security | A+ | Zero SQL injection vectors, constant-time compare everywhere, fail-closed scanning, CSRF on all forms |
18 + | Performance | A- | analytics.rs query duplication, hash_lookup creates new reqwest::Client per call |
19 + | Documentation | A | Module-level //! on every file, response conventions documented |
20 + | Dependencies | A | All deps at latest stable, async-trait cleanup opportunity (Rust 2024 native async) |
21 + | Frontend | A | Askama auto-escape, all `\|safe` uses verified safe, strong CSP, no raw innerHTML |
22 + | Type Safety | A | 50+ UUID newtypes, validated string types, Cents monetary newtype. Minor: moderation.rs uses raw Uuid |
23 + | Observability | A | 1 missing #[instrument] (api/mod.rs:public_projects), otherwise comprehensive |
24 + | Concurrency | A | ON CONFLICT, FOR UPDATE, atomic WHERE guards, advisory locks, optimistic versioning |
25 + | Resilience | A+ | Graceful shutdown with hard deadline, migration exit code 2, health monitor with status-transition alerts |
26 + | API Consistency | A | Documented response conventions, json_error_layer, versioned SyncKit routes |
27 + | Migration Safety | A | 85 additive migrations, IF EXISTS on drops, CHECK constraints |
28 + | Codebase Size | A- | 80K LOC well-organized; helpers.rs (1,268 lines) should be split |
30 29
31 30 ## Module Heatmap
32 31
33 - | Module | Code | Arch | Test | Security | Perf | Docs | Observ | Concurrency |
34 - |--------|:----:|:----:|:----:|:--------:|:----:|:----:|:------:|:-----------:|
35 - | main.rs | A | A | n/a | A | A | A | A+ | n/a |
36 - | lib.rs | A | A | n/a | A | n/a | A | n/a | n/a |
37 - | config.rs | A | A | A- | A+ | n/a | A | n/a | n/a |
38 - | error.rs | A+ | A | A | A | n/a | A | A | n/a |
39 - | auth.rs | A | A | A | A+ | A | A | A | A |
40 - | csrf.rs | A | A | A | A | A | A | n/a | n/a |
41 - | synckit_auth.rs | A | A | A- | A | n/a | A | n/a | n/a |
42 - | monitor.rs | A | A | n/a | A | A | A | A | n/a |
43 - | scheduler.rs | A | A | n/a | A | A | A | A | n/a |
44 - | constants.rs | A+ | A | n/a | A | n/a | A | n/a | n/a |
45 - | validation/ | A | A | A | A | n/a | A | n/a | n/a |
46 - | email/ | A | A | A | A | A | A | n/a | n/a |
47 - | storage.rs | A | A | A | A | A | A | n/a | n/a |
48 - | payments/ | A | A | A | A | n/a | A | n/a | n/a |
49 - | helpers.rs | A | A | A | A | n/a | A | n/a | n/a |
50 - | rss.rs | A | A | A | A | n/a | A | n/a | n/a |
51 - | markdown.rs | A | A | A | A+ | n/a | A | n/a | n/a |
52 - | docs.rs | A | A | A | A | n/a | A | n/a | n/a |
53 - | git/ | A- | A | A | A | A | A | A | n/a |
32 + | Module | Code | Arch | Test | Security | Perf | Docs | TypeSafe | Observ |
33 + |--------|:----:|:----:|:----:|:--------:|:----:|:----:|:--------:|:------:|
34 + | main.rs | A | A | n/a | A | n/a | A | n/a | A |
35 + | lib.rs | A | A | n/a | A | A- | A | A | n/a |
36 + | config.rs | A | A | A | A+ | n/a | A | n/a | n/a |
37 + | error.rs | A+ | A | A+ | A+ | n/a | A | A | A |
38 + | auth.rs | A | A | A- | A+ | n/a | A | A | A |
39 + | csrf.rs | A | A- | A | A | n/a | A | n/a | n/a |
40 + | helpers.rs | A- | n/a | A+ | A | n/a | A | A | n/a |
41 + | constants.rs | A | n/a | A | n/a | n/a | A | n/a | n/a |
42 + | storage.rs | A | A | A | A | n/a | A | A | n/a |
43 + | monitor.rs | A- | A | A | n/a | n/a | A | n/a | A |
44 + | wam_client.rs | A | n/a | **B** | n/a | n/a | A | n/a | n/a |
45 + | synckit_auth.rs | A | n/a | A+ | A+ | n/a | A | A | n/a |
46 + | pricing.rs | A | A+ | A+ | n/a | n/a | A | A | n/a |
54 47 | wordlist.rs | A | n/a | n/a | n/a | n/a | A | n/a | n/a |
55 - | types/ | A | A | A- | A | n/a | A | n/a | n/a |
56 - | templates/ | A | A | n/a | A | n/a | A | A | n/a |
57 - | scanning/ | A | A | A | A+ | A | A | n/a | n/a |
58 - | db/mod.rs | A | A | n/a | n/a | n/a | A | n/a | n/a |
59 - | db/models/ | A | A | A | n/a | n/a | A | n/a | n/a |
60 - | db/enums.rs | A | A | A | n/a | n/a | A | n/a | n/a |
61 - | db/id_types.rs | A | A | A | n/a | n/a | A | n/a | n/a |
62 - | db/validated_types.rs | A | A | A | n/a | n/a | A | n/a | n/a |
63 - | db/users.rs | A | A | n/a | A | A | A | n/a | A+ |
64 - | db/items.rs | A | A | n/a | A | A | A | n/a | n/a |
65 - | db/transactions.rs | A | A | n/a | A | A | A | n/a | A |
66 - | db/discover.rs | A- | A | n/a | A | A | A | n/a | n/a |
67 - | db/tags.rs | A | A | n/a | A | A | A | n/a | n/a |
68 - | db/analytics.rs | A- | A | A | A | A | A | n/a | n/a |
69 - | db/license_keys.rs | A | A | n/a | A | A | A | n/a | A |
70 - | db/versions.rs | A | A | n/a | A | A | A | n/a | A |
71 - | db/subscriptions.rs | A | A | n/a | A | A | A | n/a | A |
72 - | db/synckit.rs | A | A | n/a | A | A | A | n/a | n/a |
73 - | db/promo_codes.rs | A | A | A | A | A | A | n/a | n/a |
74 - | db/follows.rs | A | A | n/a | A | A | A | n/a | n/a |
75 - | db/mailing_lists.rs | A | A | A | A | A | A | n/a | n/a |
76 - | db/blog_posts.rs | A | A | n/a | A | A | A | n/a | A |
77 - | db/auth.rs | A | A | n/a | A | A | A | n/a | A |
78 - | db/other (9 files) | A | A | n/a | A | A | A | n/a | n/a |
79 - | routes/auth.rs | A | A | n/a | A+ | A | A | A | n/a |
80 - | routes/admin.rs | A | A | n/a | A | A | A | A | n/a |
81 - | routes/storage/ | A | A | n/a | A | A | A | A | n/a |
82 - | routes/oauth.rs | A | A | n/a | A | A | A | A | n/a |
83 - | routes/synckit/ | A | A | n/a | A | A | A | A | n/a |
84 - | routes/git/ | A | A | n/a | A | A | A | A | n/a |
85 - | routes/postmark/ | A | A | n/a | A | A | A | A | n/a |
86 - | routes/stripe/checkout.rs | A | A | n/a | A | A | A | A | A |
87 - | routes/stripe/webhook.rs | A | A | n/a | A | A | A | A | A |
88 - | routes/stripe/connect.rs | A | A | n/a | A | A | A | A | A |
89 - | routes/api/mod.rs | A | A | n/a | A | A | A | n/a | n/a |
90 - | routes/api/users/ | A | A | n/a | A | A | A | A | n/a |
91 - | routes/api/items.rs | A | A | n/a | A | A | A | A | n/a |
92 - | routes/api/blog.rs | A | A | n/a | A | A | A | A | n/a |
93 - | routes/api/projects.rs | A | A | n/a | A | A | A | A | n/a |
94 - | routes/api/links.rs | A | A | n/a | A | A | A | A | n/a |
95 - | routes/api/exports.rs | A | A | n/a | A | A | A | A | n/a |
96 - | routes/api/promo_codes.rs | A | A | n/a | A | A | A | A | n/a |
97 - | routes/api/bulk.rs | A | A | n/a | A | A | A | A | n/a |
98 - | routes/api/other | A | A | n/a | A | A | A | A | n/a |
99 - | routes/pages/email_actions.rs | A | A | n/a | A | A | A | A | n/a |
100 - | routes/pages/dashboard/wizards/ | A | A | A | A | n/a | A | A | n/a |
101 - | routes/pages/public/join_wizard.rs | A | A | A | A+ | n/a | A | A | n/a |
102 - | routes/pages/other | A | A | n/a | A | A | A | A | n/a |
103 - | tests/harness/ | A | A | n/a | A | n/a | A | n/a | n/a |
104 - | tests/workflows/ | A | A | n/a | A | n/a | A | n/a | n/a |
105 - | tests/health.rs | A | A | n/a | A | n/a | A | n/a | n/a |
106 - | tests/load/ | A | A | n/a | A | n/a | A | n/a | n/a |
48 + | license_templates.rs | A | n/a | A | A- | n/a | A | A | n/a |
49 + | build_runner.rs | A | n/a | A- | A | n/a | A | n/a | A |
50 + | git_ssh.rs | A | A | A- | A | n/a | A | n/a | n/a |
51 + | rss.rs | A | n/a | A | A | A | A | n/a | n/a |
52 + | db/mod.rs | A | A | n/a | A | n/a | A | n/a | n/a |
53 + | db/id_types.rs | A | n/a | A | n/a | n/a | A | A+ | n/a |
54 + | db/enums.rs | A | n/a | A+ | n/a | n/a | A | A | n/a |
55 + | db/validated_types.rs | A | n/a | A+ | n/a | A | A | A | n/a |
56 + | db/users.rs | A | A | C | A | A | A | A- | n/a |
57 + | db/items.rs | A | A | C | A | A | A | **B+** | n/a |
58 + | db/synckit.rs | A | A | C | A | A- | A | A- | n/a |
59 + | db/creator_tiers.rs | A | A | A | A | A | A | A | n/a |
60 + | db/transactions.rs | A | A | C | A | A | A | A- | n/a |
61 + | db/analytics.rs | **B+** | **B+** | A | A | A | A | A- | n/a |
62 + | db/discover.rs | A- | **B+** | C | A | A | A | A | n/a |
63 + | db/subscriptions.rs | A | A | C | A | A | A | A | n/a |
64 + | db/promo_codes.rs | A | A | A+ | A | A | A | A | n/a |
65 + | db/models/* | A | A | A- | n/a | n/a | A | A | n/a |
66 + | db/moderation.rs | A | A | C | A | n/a | A- | **B** | n/a |
67 + | scanning/ | A | A | A+ | A+ | A | A | A | n/a |
68 + | payments/checkout.rs | A- | A | A | A | A- | A | A | n/a |
69 + | payments/webhooks.rs | A | A | A+ | A+ | n/a | A | n/a | n/a |
70 + | payments/connect.rs | A- | A | C | A | n/a | A | n/a | n/a |
71 + | email/tokens.rs | A | A | A+ | A+ | n/a | A | n/a | n/a |
72 + | email/notifications.rs | A | A | C | n/a | n/a | A | n/a | n/a |
73 + | validation/ | A | A | A+ | A+ | n/a | A | A | n/a |
74 + | types/mod.rs | A | A | A+ | A+ | n/a | A | A- | n/a |
75 + | types/conversions.rs | A | A | A | n/a | n/a | A | A- | n/a |
76 + | templates/ | A | A | n/a | A | n/a | A | n/a | n/a |
77 + | scheduler/mod.rs | A | A | A+ | n/a | n/a | A | n/a | n/a |
78 + | scheduler/other | A | A | C | n/a | n/a | A | n/a | A |
79 + | import/csv_converter.rs | A | A | A+ | A | A- | A | n/a | n/a |
80 + | git/mod.rs | A | A | A | A | A | A | A | n/a |
81 + | routes/auth.rs | A | A | C | A+ | A | A | A | A |
82 + | routes/admin/ | A | A | C | A | **B** | A | A | A |
83 + | routes/storage/ | A | A | C | A | A | A | A | A |
84 + | routes/stripe/ | A | **B** | C | A | A | A | A | A |
85 + | routes/synckit/ | A | A | C | A | A | A | A | A |
86 + | routes/postmark/ | A | A | A | A | A | A | n/a | A |
87 + | routes/git/ | **B** | A | C | A | **B** | A | A | A |
88 + | routes/pages/ | A | **B** | C | A | A | A | A | A |
89 + | routes/api/ | A | A | C | A | A | A | A | **B** |
90 + | routes/builds.rs | A | A | C | A | A | A | A | A |
91 + | routes/ota.rs | A | A | C | A | **B** | A | A | A |
92 + | tests/ | A | A | n/a | A | n/a | A | n/a | n/a |
93 +
94 + **Bold** = cold spot (B or below).
107 95
108 96 ### Cold Spots
109 97
110 - None found. All modules at A- or above. constants.rs has 68 tests mostly asserting positivity (functional but low-value coverage).
98 + 1. **db/moderation.rs type safety (B):** Uses raw `Uuid` for action IDs and `String` for action_type instead of typed newtypes. Only file in db/ without typed IDs.
99 + 2. **routes/git/raw.rs code quality (B):** Three `.unwrap()` calls on `Response::builder().body()` — violates no-unwrap convention.
100 + 3. **db/analytics.rs code quality (B+):** 12 near-identical query blocks across timeseries/comparison functions. Query builder pattern would cut ~150 LOC.
101 + 4. **routes/admin/ CSRF (B):** POST routes rely on AdminUser session + SameSite cookies but no explicit CSRF token validation.
102 + 5. **wam_client.rs testing (B):** Zero unit tests for the WAM HTTP client.
111 103
112 - ## Infrastructure Checklist
113 -
114 - Run during each audit pass via SSH. Check both servers and record results.
115 -
116 - ### Production VPS (5.78.144.244)
117 -
118 - ```bash
119 - # Service health
120 - systemctl is-active makenotwork caddy postgresql
121 -
122 - # TLS certificate expiry (should be >30 days)
123 - curl -sI https://makenot.work | grep -i 'expires\|date'
124 - echo | openssl s_client -servername makenot.work -connect makenot.work:443 2>/dev/null | openssl x509 -noout -dates
125 -
126 - # Disk usage (should be <80%)
127 - df -h /
128 -
129 - # Memory and swap
130 - free -h
131 -
132 - # Backup freshness (most recent backup <24h old)
133 - ls -lt /opt/makenotwork/backups/ | head -5
134 -
135 - # PostgreSQL health
136 - sudo -u postgres psql -c "SELECT count(*) AS active_connections FROM pg_stat_activity;"
137 - sudo -u postgres psql -c "SELECT pid, now() - pg_stat_activity.query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' AND now() - pg_stat_activity.query_start > interval '30 seconds';"
138 -
139 - # Open ports (should be 22, 80, 443 only)
140 - ss -tlnp | grep LISTEN
141 -
142 - # Failed SSH attempts (last 24h)
143 - journalctl -u sshd --since "24 hours ago" | grep -c "Failed password\|Invalid user" || echo "0"
144 -
145 - # systemd restart count
146 - systemctl show makenotwork --property=NRestarts
147 -
148 - # makenotwork service uptime
149 - systemctl status makenotwork | head -5
150 - ```
151 -
152 - ### Astra (100.106.221.39)
153 -
154 - ```bash
155 - # PostgreSQL health
156 - sudo -u postgres psql -c "SELECT count(*) AS active_connections FROM pg_stat_activity;"
104 + ## Mandatory Surprise
157 105
158 - # Disk usage
159 - df -h /
106 + **BUG: Sandbox creator tier mismatch.** `db/users.rs:259` inserts `'SmallFiles'` (PascalCase) into the `creator_tier` column, but `impl_str_enum!` maps `SmallFiles => "small_files"` (snake_case). When `auth.rs:178` parses the tier via `.parse().ok()`, the mismatch silently returns `None`. Sandbox users lose their SmallFiles tier privileges — storage limits, file upload permissions, and tier-gated features all fall back to no-tier defaults. Fix: change the SQL literal from `'SmallFiles'` to `'small_files'`.
160 107
161 - # Orphaned test databases
162 - sudo -u postgres psql -c "SELECT datname FROM pg_database WHERE datname LIKE 'test_%';" | grep -c test_ || echo "0"
108 + ### Previous Surprises
163 109
164 - # Rust toolchain version
165 - rustc --version
166 - cargo --version
167 - ```
110 + **Run 17:** TOCTOU-safe slug generation with retry loop + advisory lock pattern for sandbox IP cap.
168 111
169 - ### Checklist Summary
112 + **Run 15:** Session touch cache — DashMap with 30s TTL avoids N+1 session queries.
170 113
171 - | Check | Target | Status |
172 - |-------|--------|--------|
173 - | makenotwork service | running | |
174 - | caddy service | running | |
175 - | postgresql service | running | |
176 - | TLS cert expiry | >30 days | |
177 - | Disk usage (prod) | <80% | |
178 - | Memory/swap | healthy | |
179 - | Latest backup age | <24h | |
180 - | PG active connections | <50 | |
181 - | PG long queries | 0 | |
182 - | Open ports | 22,80,443 | |
183 - | Failed SSH (24h) | <100 | |
184 - | Service restarts | 0 | |
185 - | Astra disk | <80% | |
186 - | Astra orphaned DBs | 0 | |
187 - | Rust toolchain | current | |
114 + **Run 13:** Mailing list delivery migration has zero duplication with the follows-based delivery it replaces.
188 115
189 116 ## Strengths
190 117
191 118 ### 1. Security-in-depth
192 - Every attack surface covered. Argon2 with 128-char max, CSRF synchronizer tokens with constant-time comparison, session fixation prevention, account lockout, rate limiting on all sensitive endpoints, HMAC-signed URLs, Stripe webhook verification, login tokens hashed with SHA-256, OAuth PKCE with S256, passkeys/WebAuthn, TOTP 2FA enforced on all auth paths (login link, OAuth), account deletion via POST with confirmation, self-purchase prevention, 6-layer malware scanning pipeline, trust tiers for new uploads. No SQL injection vectors -- zero `format!()` in any `sqlx::query` call confirmed by grep.
193 -
194 - ### 2. Comprehensive test suite
195 - 1,861 tests (1,139 unit + 722 integration). Per-test database isolation. In-process load test harness. Adversarial exploit-attempt tests. proptest active.
119 + Zero SQL injection vectors across 200+ queries. Argon2id with explicit params, CSRF synchronizer tokens with constant-time comparison, session fixation prevention, account lockout, rate limiting, HMAC-signed URLs, 6-layer malware scanning pipeline with fail-closed design. JWT tokens validated against live DB state (not just expiry). Comprehensive CSP headers.
196 120
197 - ### 3. Zero N+1 queries
198 - Systematic prevention: batch queries with ANY($1), LEFT JOINs with aggregation, pre-computed denormalized fields, single round-trip health checks. Session touch cache (DashMap with 30s TTL) prevents N+1 session queries on every request. No N+1 patterns found.
121 + ### 2. Test quality and coverage
122 + 1,933 tests with per-test database isolation (CREATE DATABASE TEMPLATE clone). 53 adversarial exploit-attempt tests. Property-based testing with proptest. Behavior-focused integration tests via in-process tower::ServiceExt::oneshot. Zero TODO/FIXME/HACK in the codebase.
199 123
200 - ### 4. Type safety
201 - 14+ entity ID newtypes, 15+ domain enums, validated newtypes (Username, Slug, KeyCode) with constructors enforcing invariants. `from_trusted` escape hatch for internal use. Form inputs auto-validated via Deserialize. Compile-time template verification via Askama.
202 -
203 - ### 5. Transactional integrity
204 - All purchase flows (paid webhook, free claim, discount code, download code) wrapped in DB transactions with sales count increments. Subscription webhook promo code increment transactional. Login token consumption atomic. Version creation, license key revocation, and Connect account creation all race-safe. Atomic DB operations for broadcast rate limit and release announcements.
205 -
206 - ### 6. Clean architecture
207 - Predictable naming. File organization domain-based. Zero magic numbers (constants.rs). Liberal early returns, 2-3 nesting levels. Consistent handler shape. `users.rs` cleanly split into 8 domain submodules. `pub(crate)` on internal DB modules.
124 + ### 3. Type safety discipline
125 + 50+ UUID newtypes via `define_pg_uuid_id!` macro, 23 domain enums via `impl_str_enum!`, validated string types (Username, Slug, KeyCode), Cents monetary newtype with SUM(BIGINT)->NUMERIC decode handling. Compile-time template verification via Askama.
208 126
209 127 ## Weaknesses
210 128
211 - ### 1. ~~axum_extra::Form bug~~ (Incorrect finding)
212 - axum_extra::Form is used correctly for repeated form fields (e.g., checkbox arrays in bulk operations). All bulk and wizard tests pass. Verified 2026-04-22.
213 -
214 - ### 2. ~~db/models.rs size (2,172 LOC)~~ (Fixed)
215 - Split into 16 domain submodules under `models/`. Largest file is 384 LOC. All re-exported via `models/mod.rs`. Fixed 2026-04-22.
129 + ### 1. Inline SQL in route handlers
130 + 4 locations where route handlers contain raw `sqlx::query` calls instead of delegating to `db/`:
131 + - `routes/stripe/checkout/project.rs:90` — INSERT for free project claim
132 + - `routes/pages/dashboard/forms.rs:58,71` — SUM queries for storage display
133 + - `routes/pages/public/landing.rs:46` — COUNT for landing page stats
216 134
217 - ### 3. ~~Observability gaps (36% coverage)~~ (Fixed)
218 - Added `#[tracing::instrument(skip_all)]` to all 480 DB query functions. Total coverage now 883 annotations (routes 97%, DB 100%). Fixed 2026-04-22.
135 + ### 2. Sandbox tier bug (confirmed)
136 + `'SmallFiles'` vs `'small_files'` mismatch silently breaks sandbox user tier detection.
219 137
220 - ### 4. Dependency advisories (all upstream)
221 - - rsa 0.9.10 (RUSTSEC-2023-0071, via sqlx-mysql + yara-x) -- non-issue, MNW uses PostgreSQL
222 - - rustls-webpki 0.101.7 (RUSTSEC-2026-0049, via aws-sdk-s3's older rustls chain) -- blocked on aws-sdk-s3
223 - - instant 0.1.13 (via async-stripe) -- unmaintained, blocked on async-stripe
224 - - lru 0.12.5 (via aws-sdk-s3) -- unsound IterMut, blocked on aws-sdk-s3
225 - - bincode 1.x + 2.x (RUSTSEC-2025-0141, via syntect + yara-x) -- unmaintained, blocked on upstream
226 -
227 - ### 5. ~~DKIM selector verification~~ (Stale)
228 - Confirmed complete by user 2026-04-22. Postmark dashboard shows correct configuration.
229 -
230 - ## Mandatory Surprise
231 -
232 - **TOCTOU-safe slug generation with retry loop + advisory lock pattern for sandbox IP cap.** The `create_item` slug generation handles TOCTOU races at the SQL level. After optimistic slug check, a retry loop catches Postgres unique constraint violations (error code 23505) and appends incrementing suffixes. Two-phase approach: optimistic check + database constraint as authoritative guard. The advisory lock pattern for sandbox account creation uses `pg_advisory_lock` keyed on IP hash to serialize per-IP creation. Both are production-grade.
233 -
234 - ### Previous Surprise (Run 15)
235 -
236 - **Session touch cache -- DashMap with 30s TTL avoids N+1 session queries.** Every authenticated request needs to "touch" the session (update `last_active_at`). A naive implementation would issue a DB UPDATE on every single request. Instead, MNW uses a DashMap keyed by session ID with a 30-second TTL. If a session was touched within the last 30 seconds, the DB write is skipped entirely. Verdict: Clever optimization -- the 30s window is conservative enough that session staleness is never a security concern.
237 -
238 - ### Previous Surprise (Run 13)
239 -
240 - **The mailing list delivery migration (I4) has zero duplication with the follows-based delivery it replaces.** `send_release_announcements()` and `send_blog_post_announcements()` in `scheduler.rs` share the same pattern but each handles a distinct content type. The old follows-based query is dead-code-annotated rather than deleted. Verdict: Actually fine -- premature abstraction would be worse.
241 -
242 - ## Competitive Comparison
243 -
244 - Based on competition.md (7 competitors: Gumroad, Itch.io, Bandcamp, Patreon, Lemon Squeezy, Ko-fi, Sellfy):
245 -
246 - **Where MNW is ahead:** 0% platform fee (unmatched). Source-available codebase (unique). Full data export with CSV injection prevention. License key phone-home with machine activation tracking. Hierarchical tag taxonomy. SyncKit cloud sync API (uncontested). 6-layer malware scanning. 14+ typed entity IDs. Contact revocation UI (most platforms don't offer granular contact sharing control). Trust tiers for new uploads.
247 -
248 - **Where competitors are ahead:** Gumroad (email marketing, analytics, audience). Bandcamp (community, editorial curation). Patreon (subscription maturity). Lemon Squeezy (Merchant of Record, tax handling). All competitors have mobile apps. These gaps are planned in the roadmap and appropriate for alpha.
138 + ### 3. CSV import amount heuristic
139 + `import/csv_converter.rs` `parse_amount_cents` uses a 10,000 threshold heuristic to guess whether amounts are in cents or dollars. Values in the 100-10,000 range (e.g., $99 as `9900` cents) are silently misinterpreted as dollar amounts ($9,900). No explicit cents/dollars column indicator.
249 140
250 141 ## Action Items
251 142
252 - Filed in `docs/mnw/todo.md`.
253 -
254 - ### All remediated (previous sessions)
255 - 1. ~~Add TOTP 2FA check to login link handler~~ -- done
256 - 2. ~~Add TOTP 2FA check to OAuth credential auth~~ -- done
257 - 3. ~~Add validation to update_item, update_project, update_link~~ -- done
258 - 4. ~~Wrap complete_transaction + increment_sales_count + discount code in DB transaction~~ -- done
259 - 5. ~~Make login token consumption atomic~~ -- done
260 - 6. ~~Wrap version creation in a transaction~~ -- done
261 - 7. ~~Change account deletion to POST with confirmation page~~ -- done
262 - 8. ~~Log on error instead of let _ for discount code/license key~~ -- done
263 - 9. ~~Add self-purchase check~~ -- done
264 - 10. ~~Add chapter title validation and item text body size limit~~ -- done
265 - 11. ~~Add index on transactions.stripe_payment_intent_id~~ -- done
266 - 12. ~~Wrap revoke_license_key in transaction~~ -- done
267 - 13. ~~Guard Stripe Connect account creation against race~~ -- done
268 - 14. ~~Remove 2 dead code functions~~ -- done
269 - 15. ~~Add #[instrument] to postmark_webhook~~ -- done
270 - 16. ~~Split routes/api/users.rs into submodules~~ -- done
271 - 17. ~~Add integration tests for contact revocation workflow~~ -- done (2026-03-09)
272 - 18. ~~`pwyw_min_cents` validation~~ -- done (2026-03-09)
273 - 19. ~~Fixed discount upper bound~~ -- done (2026-03-09)
274 - 20. ~~53 adversarial exploit-attempt tests~~ -- done (2026-03-09)
275 - 21. ~~Add trust tier test bypass for scanning tests~~ -- done (2026-03-10)
276 - 22. ~~Add trust tier test bypass for storage tests~~ -- done (2026-03-10)
143 + ### Run 18 (2026-05-01)
144 +
145 + 39. **[HIGH]** Fix sandbox tier: change `'SmallFiles'` to `'small_files'` in `db/users.rs:259`
146 + 40. **[MEDIUM]** Extract inline SQL from route handlers to db/ layer (4 locations listed above)
147 + 41. **[MEDIUM]** Add `ModerationActionId` newtype and `ModerationActionType` enum to `db/moderation.rs`
148 + 42. **[MEDIUM]** Replace `.unwrap()` in `routes/git/raw.rs:80,142,190` with proper error handling
149 + 43. **[MEDIUM]** Add `#[tracing::instrument]` to `routes/api/mod.rs:public_projects`
150 + 44. **[LOW]** Replace production `.unwrap()` at `helpers.rs:52` with `unwrap_or_else` or `HeaderValue::from_static`
151 + 45. **[LOW]** Replace production `.unwrap()` at `monitor.rs:105` with pattern match
152 + 46. **[LOW]** Add unit tests to `wam_client.rs`
153 + 47. **[LOW]** Add explicit cents/dollars format option to CSV import
154 + 48. **[DEFERRED]** Split helpers.rs (~1,268 lines) into focused modules (formatting, crypto, rate_limit)
155 + 49. **[DEFERRED]** Reduce analytics.rs query duplication via builder pattern or macro (~150 LOC savings)
156 + 50. **[DEFERRED]** Remove `async-trait` in favor of Rust 2024 native async traits
277 157
278 158 ### Open (blocked on upstream)
279 159 23. Monitor aws-sdk-s3 for lru fix (RUSTSEC-2026-0002)
280 160 24. Monitor async-stripe for instant fix (RUSTSEC-2024-0384)
281 161 25. Monitor aws-sdk-s3 for rustls-webpki 0.101.7 fix (RUSTSEC-2026-0049)
282 -
283 - ### Run 15 (2026-04-18, corrected 2026-04-22)
284 - 34. ~~**[HIGH]** Fix 29 failing tests~~ -- Already resolved (1,359 tests all pass).
285 - 35. ~~**[HIGH]** Investigate and fix axum_extra::Form bug~~ -- Incorrect finding. Usage is correct.
286 - 36. ~~**[MEDIUM]** Split db/models.rs into domain-specific submodules~~ -- Done (16 submodules, largest 384 LOC).
287 - 37. ~~**[MEDIUM]** Increase observability coverage from 36% toward target~~ -- Done (883 annotations, routes 97%, DB 100%).
288 - 38. ~~**[LOW]** Verify DKIM selector in Postmark dashboard~~ -- Confirmed complete by user 2026-04-22. Stale finding.
162 + 33. bincode unmaintained (RUSTSEC-2025-0141) — upstream via syntect/yara-x, warning only
289 163
290 164 ### Previously resolved
291 - 31. ~~**[MEDIUM]** Fix `delete_item_returns_toast` test failure~~ -- done
292 - 32. ~~**[MEDIUM]** Fix `item_wizard_license_keys` test failure~~ -- done
293 - 33. **[LOW]** bincode unmaintained (RUSTSEC-2025-0141) -- upstream via syntect/yara-x, warning only
294 -
295 - ### JS Audit Remediation (2026-03-11) -- Complete (11/11)
296 -
297 - All JS audit findings resolved:
298 - - **Critical (4):** innerHTML XSS in dashboard-item.html, project_blog.html, user_synckit.html, new_project_form.html/project_settings.html -- all replaced with DOM API + textContent
299 - - **Medium (4):** CSRF tokens on 21 fetch() calls, implicit event global fix, segments_json rendering fix (`|safe` + `</` escaping), .catch() on 11 fetch() calls
300 - - **Low (3):** alert() -> showToast() (17 calls across 7 files + insertions.js), location.reload() -> page navigation, localStorage wrapped in safeStorageGet/safeStorageSet
301 -
302 - ### Security Deep Dive (2026-03-13) -- Complete (4/4)
303 -
304 - All findings from targeted security deep dive resolved:
305 - - **Session cache fail-closed:** `auth.rs:109` `unwrap_or(true)` -> `unwrap_or(false)`
306 - - **Scanner errors fail-closed:** scanner errors now produce `HeldForReview` instead of `Clean`
307 - - **Scan status allowlist:** changed from blocklist to allowlist (`!= Clean`)
308 - - **JWT issuer validation:** `iss` claim + `SYNCKIT_JWT_ISSUER` constant + `set_issuer()` validation
309 -
310 - ### Adversarial Test Audit (2026-03-13)
311 -
312 - Key changes:
313 - - **CRITICAL fix:** Suspended users could initiate purchases -- added `check_not_suspended()` to checkout handlers
314 - - **HIGH fix:** Suspended users could manage promo codes and license keys
315 - - **HIGH fix:** Session `suspended` flag stale after admin suspension -- `touch_session` now returns `TouchResult`
316 - - **HIGH fix:** Webhook signature had no timestamp freshness check -- added 300s tolerance
317 -
318 - Total: 4 open items (3 upstream-blocked deps + 1 low-severity warning). No new action items.
165 + All items 1-22 and 31-38 from previous audits verified intact.
319 166
320 167 ## Previous Action Item Verification
321 168
322 - Items 23-25 (upstream-blocked deps): Still open, unchanged.
323 - Item 33 (bincode unmaintained): Still upstream, warning only.
324 - All other items: Verified intact.
325 -
326 - ## Adversarial Testing (completed 2026-03-09)
327 -
328 - All four focus areas completed. 53 tests across 4 files; no vulnerabilities found; 2 validation gaps found and fixed.
169 + | # | Item | Status |
170 + |---|------|--------|
171 + | 23 | aws-sdk-s3 lru fix | Unfixed (upstream) |
172 + | 24 | async-stripe instant fix | Unfixed (upstream) |
173 + | 25 | aws-sdk-s3 rustls-webpki fix | Unfixed (upstream) |
174 + | 33 | bincode unmaintained | Unfixed (upstream) |
175 + | 34-38 | Run 15 items | All fixed/verified |
329 176
330 - - **Focus A (IDOR):** 13 tests -- project/item/blog CRUD by non-owner all return 403, non-creator/suspended user blocked, no resource enumeration
331 - - **Focus B (Input validation):** 16 tests -- price bounds, title/desc length, slug boundaries, unicode char counting, XSS/SQLi harmless, duplicate slug/username rejected, javascript:/data: URLs rejected
332 - - **Focus C (Auth & session):** 10 tests -- stale session, lockout, 2FA pending blocks API, login link single-use, no user enumeration, suspended user writes blocked
333 - - **Focus D (Business logic):** 14 tests -- self-purchase, draft item, free/paid boundary, double-purchase, cross-creator promo code, scope abuse, exhausted code, PWYW abuse
177 + No regressions found. Items 23-25, 33 remain open across 3+ consecutive audits (chronic, but upstream-blocked).
334 178
335 179 ## Metrics Over Time
336 180
@@ -352,6 +196,7 @@ All four focus areas completed. 53 tests across 4 files; no vulnerabilities foun
352 196 | 2026-04-18 (Run 15) | ~67,442 | -- | 1,356 (29 fail) | ~20 | 0 | 4 | A- |
353 197 | 2026-04-22 (Run 15 corrected) | ~67,442 | -- | 1,359 | ~20 | 0 | 1 | A |
354 198 | 2026-04-30 (Run 17) | ~79,334 | -- | 1,861 | ~15.0 | 0 | 0 | A |
199 + | 2026-05-01 (Run 18) | ~80,470 | -- | 1,933 (34 int. fail) | ~15.1 | 0 | 5 | A |
355 200
356 201 ---
357 202
@@ -0,0 +1,110 @@
1 + # Human TODO
2 +
3 + Items requiring manual action, external accounts, legal engagement, design decisions, or physical testing.
4 +
5 + ---
6 +
7 + ## External Blockers
8 +
9 + ### Business Formation (Make Creative, LLC)
10 + - [ ] D-U-N-S number — Applied 2026-04-28, ~30 business days (blocks Google Play + Microsoft Partner Center)
11 + - [x] Business bank account — Mercury approved 2026-05-01
12 + - [ ] Transfer startup funds to Mercury business account
13 +
14 + ### Platform Accounts (blocked on D-U-N-S)
15 +
16 + | Blocker | Status | Blocks |
17 + |---------|--------|--------|
18 + | D-U-N-S number | Applied 2026-04-28, ~30 days | Google Play, Microsoft Partner Center |
19 + | Google Play Developer Account ($25) | Blocked on D-U-N-S | GO/BB Android builds |
20 + | Microsoft Partner Center account | Blocked on D-U-N-S | Windows Store distribution (optional) |
21 + | Windows code signing certificate | Not started (individual or traditional cert — Azure Trusted Signing requires 3yr history) | GO/BB/AF Windows builds |
22 + | OAuth Provider Registration (Fastmail) | Need to send registration info to partnerships@fastmailteam.com | GO Fastmail email OAuth |
23 +
24 + ---
25 +
26 + ## Content Seeding & Manual Testing
27 +
28 + ### Creator Setup
29 + - [ ] Confirm creator tier is Small Files ($20/mo)
30 + - [ ] Confirm Stripe Connect onboarding complete (live mode)
31 +
32 + ### Project: GoingsOn
33 + - [ ] Create subscription tier: "Cloud Sync" ($3/mo) — not yet created
34 +
35 + ### Project: audiofiles
36 + - [ ] Enable license keys (test activation flow)
37 + - [ ] Create a test discount code (e.g. LAUNCH50, 50% off)
38 +
39 + ### Cross-Project
40 + - [ ] Add custom links (source code link, support@makenot.work — currently profile has Twitter/Mastodon/htpy.app)
41 + - [ ] Test free download flow (GO), PWYW flow (BB), purchase flow (AF), subscription flow (GO)
42 + - [ ] Test discount code on AF purchase
43 + - [ ] Test license key delivery after AF purchase
44 + - [ ] Capture screenshots for docs (dashboard, audio player, discover, pricing, git browser) — or replace with sandbox links
45 +
46 + ### SyncKit Production Testing
47 + - [ ] Test sync across 2+ GO instances on real server
48 +
49 + ### OTA
50 + - [ ] End-to-end test: build signed GO release, upload artifact, verify auto-update check returns 200
51 +
52 + ### Sign-Off
53 + - [ ] MNW `deploy/human_testing.md` sign-off table filled
54 + - [ ] GO `docs/human_testing.md` sign-off table filled
55 + - [ ] AF `human_testing.md` sign-off table filled
56 + - [ ] All P0 items pass across all projects
57 + - [ ] No panics or 500s in MNW server logs
58 + - [ ] Backup verified within last 24 hours
59 +
60 + ---
61 +
62 + ## Launch & Outreach
63 +
64 + - [ ] Human testing: complete sign-off table in `deploy/human_testing.md` (code verified, needs manual walkthrough)
65 + - [ ] Content seeding: at least one real creator with published content on discover page
66 + - [ ] Outreach: hand-write emails using tiered creator list at `docs/internal/outreach/tiers.md`. Per-creator talking points and pitch angles included. Start with Tier 1 (alpha testers), then Tier 2 (profitable switchers)
67 + - [ ] Pitch discipline: review all outreach materials, pitch.md, and talking points. Lead with (1) cheaper at scale (pricing calculator link) and (2) structurally resistant to enshittification (no investors, no ads, no lock-in, source-available, debt-free). Do not lead with competitor instability.
68 + - [ ] Generate creator invite codes
69 + - [ ] Prepare 5-10 invite emails with signup link, GO DMG instructions, what to test, how to report bugs
70 + - [ ] Send invites
71 + - See `docs/internal/outreach/index.md` for broader community engagement plan
72 +
73 + ---
74 +
75 + ## Legal & Compliance
76 +
77 + - [ ] **Legal/tax professional review** — prep doc at `docs/internal/legal_review_prep.md` with 41 specific questions across ToS, privacy, DMCA, payments, tax. Recommended: split engagement (internet attorney 3h + tax professional 1-2h)
78 + - [ ] liability.md legal review (has [PENDING LEGAL REVIEW] placeholders) — rolled into legal review prep
79 + - [ ] dmca-counter.md designated agent address (needs DMCA agent registration) — rolled into legal review prep
80 + - [ ] **GDPR SCC execution** — Confirm SCCs are in place with Hetzner, AWS (S3), Stripe, Postmark. Part of legal review engagement.
81 + - [ ] **COPPA/GDPR child consent** — Fan accounts allow 13+. EU sets digital consent at 16 in some member states. No parental consent mechanism exists. Part of legal review.
82 + - [ ] **Indemnification clause** — ToS lacks mutual indemnification. Flagged in legal_review_prep.md. Part of legal review engagement.
83 + - [ ] **Independent appeals review** — Planned guarantee (guarantees.md). Requires second person. Track which admin made original decision, enforce different reviewer for appeals.
84 +
85 + ---
86 +
87 + ## Design Decisions (needs human input)
88 +
89 + - [ ] Create og:image social card (1200x630, for landing page and fallback — distinct from logo.png)
90 + - [ ] i18n: Start with top 5 languages by creator demand (survey after beta)
91 +
92 + ---
93 +
94 + ## Infrastructure (requires server access)
95 +
96 + - [ ] Phase 22E: MediaMTX deployment on alpha-west-1 (install binary, systemd unit, Caddy config, Cloudflare DNS, firewall rules)
97 + - [ ] Add `ffprobe` to production server (Phase 14E-1)
98 +
99 + ---
100 +
101 + ## Post-Beta (human-gated triggers)
102 +
103 + - [ ] Support hire: define trigger (response time >24h or >100 creators), document role scope
104 + - [ ] Phase 18: Self-Hosted Email — Trigger: >50 creators, Postmark >$50/mo, stable 3mo
105 + - [ ] Phase 19: Creator Email — Trigger: self-hosted stable 6mo, >200 creators
106 + - [ ] Phase 23: DSP — Trigger: >100 music creators
107 + - [ ] Phase 24D: Own MTLs — Trigger: millions in annual GMV
108 + - [ ] Corporate structure: holding company setup (if itsall.work launches)
109 + - [ ] Revenue-share crowdfunding: requires securities attorney consultation (~$5K)
110 + - [ ] Test restore from backup (offsite copy on astra)
@@ -22,6 +22,27 @@ Audit date: 2026-04-26. Perspective: skeptical creator evaluating MNW.
22 22 - [ ] **Custom domains not prominently documented**: Feature exists but hard to find from getting-started flow.
23 23 - [ ] **No creator storefront preview/demo**: First-time visitors can't see what a page looks like.
24 24 - [x] **Sandbox not linked from /creators page**: Added "Try sandbox mode" link above the CTA.
25 + - [x] **Export 2GB limit undocumented**: Added parenthetical to portability.md content section. export.md already had limits documented (lines 27-32).
26 + - [x] **File format/size limits not in creator-facing docs**: Already present in tiers.md table (line 5) — Basic 10MB/50GB, SmallFiles 500MB/250GB, BigFiles 20GB/500GB, Everything 20GB/500GB. Matches `constants.rs`.
27 + - [x] **Discovery/audience-building section missing from onboarding**: Added "Building Your Audience" section to getting-started.md — explains MNW is a selling tool, lists all discovery mechanisms (Discover page, direct links, RSS, embeds, mailing lists, follows).
28 + - [x] **Payouts and analytics not linked from getting-started**: Added payouts link after "Connect Payments" section and analytics step in "Your First Week."
29 + - [x] **Colorado jurisdiction not clarified for international creators**: Added "What jurisdiction governs disputes?" FAQ entry with plain-language explanation.
30 + - [x] **"What if Stripe blocks my account" not in FAQ**: Added FAQ entry explaining data is safe, payments stop, no alternative processor yet, cross-links payouts.md.
31 +
32 + ## Trust Gaps (round 8, 2026-05-01)
33 +
34 + - [x] **tiers.md streaming features lack "coming soon" inline**: Rewrote Everything tier section — streaming features now under "Live Streaming (Coming Soon)" subheading with explicit "not yet available" language. Tier table updated to "(live streaming coming soon)."
35 + - [ ] **Moderation admin cannot send warning without suspending**: Docs (moderation.md) describe a 4-step ladder starting with "Direct Message." Round 5 renamed the step, but code (`routes/admin/users.rs:96-140`) only implements suspend/unsuspend/terminate. No admin action for tracked warning-only communication.
36 + - [ ] **Appeals reviewed by same person who suspended**: `routes/admin/moderation.rs:49-100` — no second-reviewer mechanism or enforcement that a different admin reviews appeals. Docs promise "fresh eyes" but single-person team makes this impossible currently. (Tracked in round 7 as planned guarantee, but docs don't caveat this limitation on the appeals page itself.)
37 + - [ ] **Liability cap extremely low**: ToS line 81 caps liability at fees paid in past 12 months. A $10/mo creator's maximum recovery is $120 if platform loses entire catalog. Industry standard but contradicts the spirit of guarantees.md. Consider noting this gap in guarantees.md or raising the cap for data-loss scenarios.
38 +
39 + ## Contradictions (round 8, 2026-05-01)
40 +
41 + | Claim | Reality | Severity | Status |
42 + |-------|---------|----------|--------|
43 + | "All uploaded files in original quality" (portability.md line 12) | 2GB export ZIP limit retained (`exports.rs` line 642). Per-project workaround exists. | Medium | Fixed (docs) |
44 + | Moderation docs describe 4-step ladder with "Direct Message" first | Code only implements suspend/unsuspend/terminate — no warning-only admin action | Medium | Open |
45 + | "Everything" tier lists live streaming features (tiers.md lines 95-99) | No streaming code exists. | High | Fixed (docs — marked "coming soon") |
25 46
26 47 ## Competitive Weaknesses (product decisions, not bugs)
27 48
M server/docs/todo.md +36 -426
@@ -3,110 +3,71 @@
3 3 ## Status
4 4 Done: All pre-beta phases. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - v0.4.5. Audit grade A (Run 17, 2026-04-30). 1,139 unit tests + 722 integration tests = 1,861 total. Mutation kill rate 99.4%. Property-based testing active (proptest).
6 + v0.4.6. Audit grade A (Run 18, 2026-05-01). 1,209 unit + 724 integration = 1,933 tests. 34 integration failures (uncommitted changes). Mutation kill rate 99.4%. Property-based testing active (proptest).
7 7
8 - Business sustainability audit Run 1 (2026-04-29): grade B+. Stripe Connect corrected to Standard (no per-account fees). Everything tier raised to $60 (streaming + 0% donation fees). Earn-Back Credit and Fan+ prioritized pre-beta. Full report: `docs/internal/business/business_sustainability_audit.md`.
8 + Code fuzz (2026-05-01): all 10 findings resolved (4 serious, 4 medium, 2 minor). Test fuzz (2026-05-01): all 3 validation hardening items resolved.
9 9
10 - ---
11 -
12 - ## Code Review Remediation — Deferred
13 - - [ ] Monitor scheduler.rs (1249), git/mod.rs (624), license_keys.rs (684) for growth
10 + Human tasks (manual testing, outreach, legal, infrastructure) moved to `human_todo.md`.
14 11
15 12 ---
16 13
17 - ## External Blockers
14 + ## Audit Run 18 (2026-05-01)
18 15
19 - ### Business Formation (Make Creative, LLC)
20 - - [x] Register LLC in Colorado — SOS ID 20261524483, filed 2026-04-28
21 - - [x] Get EIN — 42-2216443, issued 2026-04-28
22 - - [ ] D-U-N-S number — Applied 2026-04-28, ~30 business days (blocks Google Play + Microsoft Partner Center)
23 - - [x] Operating agreement — Drafted at `_private/operating_agreement.md`. [PENDING LEGAL REVIEW] — flagged for attorney engagement in `legal_review_prep.md`. 6 items for counsel.
24 - - [ ] Business bank account — Mercury application submitted 2026-04-29, awaiting approval (~1-2 business days). Online signup, no branch visit.
25 -
26 - ### Platform Accounts (blocked on D-U-N-S)
27 -
28 - | Blocker | Status | Blocks |
29 - |---------|--------|--------|
30 - | D-U-N-S number | Applied 2026-04-28, ~30 days | Google Play, Microsoft Partner Center |
31 - | Google Play Developer Account ($25) | Blocked on D-U-N-S | GO/BB Android builds |
32 - | Microsoft Partner Center account | Blocked on D-U-N-S | Windows Store distribution (optional) |
33 - | Windows code signing certificate | Not started (individual or traditional cert — Azure Trusted Signing requires 3yr history) | GO/BB/AF Windows builds |
34 - | OAuth Provider Registration (Fastmail) | Need to send registration info to partnerships@fastmailteam.com | GO Fastmail email OAuth |
35 -
36 - ---
37 16
38 - ## Pre-Beta Remaining
39 -
40 - ### SyncKit S3: Production Testing
41 - - [ ] Test sync across 2+ GO instances on real server
42 - - [ ] Sync log compaction (append-only is fine at current scale)
43 -
44 - ### SyncKit S4: Key Rotation (deferred post-beta)
45 - - [ ] Add key rotation mechanism (requires server-side re-encryption of all sync_log entries)
46 -
47 - ### OTA Remaining (S6)
48 - - [ ] End-to-end test: build signed GO release, upload artifact, verify auto-update check returns 200
17 + ### Testing
18 + - [ ] **[LOW]** Add unit tests to `wam_client.rs`
19 + - [ ] **[LOW]** Add unit tests to `git_ssh.rs` for `parse_ssh_command` and `parse_repo_path`
49 20
50 - ### Content Seeding — Remaining
21 + ### Performance
22 + - [ ] **[LOW]** `scanning/hash_lookup.rs` creates new `reqwest::Client` per call — reuse from AppState
23 + - [ ] **[LOW]** `routes/ota.rs` `delete_release_handler` does 3 queries (list_releases + list_artifacts + delete) — consolidate
51 24
52 - #### Creator Setup
53 - - [ ] Confirm creator tier is Small Files ($20/mo)
54 - - [ ] Confirm Stripe Connect onboarding complete (live mode)
25 + ### Data Integrity
26 + - [ ] **[LOW]** CSV import `parse_amount_cents` heuristic misinterprets 100-10,000 range — add explicit cents/dollars format option
55 27
56 - #### Project: GoingsOn
57 - - [ ] Create subscription tier: "Cloud Sync" ($3/mo) — not yet created
28 + ### Deferred
29 + - [ ] Split `helpers.rs` (~1,268 lines) into focused modules (formatting, crypto, rate_limit)
30 + - [ ] Reduce `analytics.rs` query duplication via builder pattern or macro (~150 LOC savings)
31 + - [ ] Remove `async-trait` crate in favor of Rust 2024 native async traits
32 + - [ ] Reduce `discover.rs` query duplication (3 near-identical base query blocks per function)
33 + - [ ] `routes/admin/` performance: `admin_users` calls `count_users` twice — batch into single query
58 34
59 - #### Project: audiofiles
60 - - [ ] Enable license keys (test activation flow)
61 - - [ ] Create a test discount code (e.g. LAUNCH50, 50% off)
35 + ---
62 36
63 - #### Cross-Project
64 - - [ ] Add custom links (source code link, support@makenot.work — currently profile has Twitter/Mastodon/htpy.app)
65 - - [ ] Test free download flow (GO), PWYW flow (BB), purchase flow (AF), subscription flow (GO)
66 - - [ ] Test discount code on AF purchase
67 - - [ ] Test license key delivery after AF purchase
68 - - [ ] Capture screenshots for docs (dashboard, audio player, discover, pricing, git browser) — or replace with sandbox links
37 + ## Pre-Beta Code Tasks
69 38
70 - ### Documentation — Remaining
71 - - [x] Review new docs against live UI for accuracy (button labels, navigation paths) — fixed in round 3 audit
72 - - [ ] liability.md legal review (has [PENDING LEGAL REVIEW] placeholders) — rolled into legal review prep
73 - - [ ] dmca-counter.md designated agent address (needs DMCA agent registration) — rolled into legal review prep
39 + ### SyncKit
40 + - [x] Sync log compaction — cursor-based: tracks `last_pulled_seq` per device (migration 084), deletes entries all devices have pulled (7-day safety margin). Runs in monitor maintenance loop alongside existing 90-day age-based prune.
41 + - [ ] Add key rotation mechanism (requires server-side re-encryption of all sync_log entries) — deferred post-beta
74 42
75 43 ### Git Access Provisioning
76 44 - [ ] Dashboard page for SSH key management (API + HTMX partials exist at `routes/api/ssh_keys.rs`, needs dashboard tab)
77 45 - [ ] Per-repo collaborator access (grant push by MNW username, stored in DB, wired to authorized_keys rebuild)
78 46 - [ ] Replace manual `setup-ssh-keys.sh` with account-driven key management
79 47
80 - ### Frontend — Remaining
81 - - [ ] Git browser integration: add discover/follow integration (post-beta)
48 + ### Moderation
49 + - [x] Admin "send warning" action: `POST /api/admin/users/{id}/warn` sends policy-violation email without suspending. Records in moderation_actions.
50 + - [x] `moderation_actions` table (migration 085): id, user_id, admin_id, action_type, reason, content_ref, resolved_at, created_at. Indexed for active-actions lookup and user history.
51 + - [x] Admin handlers record actions: suspend→creates "suspension" action, terminate→creates "termination" action, remove_item→creates "content_removal" action (with item_id as content_ref).
52 + - [x] Actions resolved automatically: unsuspend→resolves suspension actions, restore_item→resolves content_removal by content_ref.
53 + - [x] User-facing "Account Status" section on Settings page: shows active moderation actions (danger-bg boxes with type, date, reason) or "Your account is in good standing." Links to appeals and support. Added to `user_details.html` after the Account section.
54 + - [x] Moderation history (collapsed `<details>`): shows resolved past actions with resolution date. Only visible when history exists.
82 55
83 - ### Invite Testers
84 - - [ ] Generate creator invite codes
85 - - [ ] Prepare 5-10 invite emails with signup link, GO DMG instructions, what to test, how to report bugs
86 - - [ ] Send invites
87 - - See `docs/internal/outreach/index.md` for broader community engagement plan
56 + ### Incident Notification System
57 + - [ ] Let creators opt into status alerts (email or webhook) when platform status changes. Implementation: subscribe endpoint on /health, store preferences in DB, trigger email on status transition (Operational -> Degraded/Error and recovery). Reuse existing email infrastructure (Postmark).
88 58
89 - ### Sign-Off
90 - - [ ] MNW `deploy/human_testing.md` sign-off table filled
91 - - [ ] GO `docs/human_testing.md` sign-off table filled
92 - - [ ] AF `human_testing.md` sign-off table filled
93 - - [ ] All P0 items pass across all projects
94 - - [ ] No panics or 500s in MNW server logs
95 - - [ ] Backup verified within last 24 hours
59 + ### Frontend
60 + - [ ] Git browser integration: add discover/follow integration (post-beta)
96 61
97 62 ---
98 63
99 - ## Frontend Audit — Remaining
100 -
101 - - [x] Landing page visual — replaced screenshot with sandbox link ("Try the dashboard without signing up")
102 - - [ ] Create og:image social card (1200x630, for landing page and fallback — distinct from logo.png)
64 + ## Code Review Remediation — Deferred
65 + - [ ] Monitor scheduler.rs (1249), git/mod.rs (624), license_keys.rs (684) for growth
103 66
104 67 ---
105 68
106 69 ## File Scanning — Future Improvements
107 70
108 - Files > 100 MB are now held for review instead of downloaded into RAM. Next steps:
109 -
110 71 ### Background scan queue (next)
111 72 - [ ] Add `scan_queue` table (s3_key, file_type, user_id, status, created_at)
112 73 - [ ] Enqueue oversized files from `scan_and_classify` instead of blanket HeldForReview
@@ -120,436 +81,40 @@ Files > 100 MB are now held for review instead of downloaded into RAM. Next step
120 81 - [ ] Extract scan worker into standalone binary (same crate, different bin target)
121 82 - [ ] Worker polls scan_queue, runs on dedicated machine with more RAM
122 83 - [ ] Allows horizontal scaling independently of request serving
123 - - [ ] Consider GPU-accelerated analysis if volume warrants it
124 -
125 -
126 - ---
127 -
128 - ## Integration Test Improvement Plan (2026-04-29)
129 -
130 - Current: 986 unit tests + 643 integration tests (74 workflow modules) = ~1,629 total.
131 - Infrastructure: template DBs, mock Stripe/email/S3, cookie-aware in-process client.
132 -
133 - ### Phase 1: Fill thin workflow gaps (high ROI)
134 -
135 - Modules with 1-3 tests that need expansion, plus missing modules for features we just hardened.
136 -
137 - #### Expand `purchase.rs` (1 -> 6 tests)
138 - - [x] Paid item purchase via mock Stripe: checkout + webhook + transaction verification
139 - - [x] PWYW purchase: buyer submits custom amount, verify correct cents
140 - - [x] Purchase of unlisted item: verify failure
141 - - [x] Duplicate free purchase: verify idempotent (one transaction)
142 - - [x] Purchase adds to library: verify item appears in /library
143 - - [ ] Purchase with promo code: apply discount, verify discounted amount in transaction (deferred to Phase 2 promo_codes_checkout)
144 -
145 - #### Expand `subscriptions.rs` (2 -> 8 tests)
146 - - [x] Create subscription tier with mock Stripe IDs
147 - - [x] List subscription tiers (multiple, ordered)
148 - - [x] Update subscription tier (name, description, deactivation)
149 - - [x] Delete subscription tier
150 - - [x] Subscriber tier visibility (anonymous user sees tier on public page)
151 - - [x] Sandbox tier uses fake Stripe IDs (sandbox_prod_/sandbox_price_ prefix)
152 - - [ ] Subscribe via mock checkout + webhook (deferred — requires Stripe subscription event simulation)
153 - - [ ] Subscription cancel + grace period (deferred — requires subscription lifecycle simulation)
154 -
155 - #### New: `sandbox.rs` (9 tests)
156 - - [x] Create sandbox account: POST /sandbox, redirect to dashboard
157 - - [x] Sandbox blocks custom domains (403)
158 - - [x] Sandbox blocks git repos (403)
159 - - [x] Sandbox blocks imports (403)
160 - - [x] Sandbox blocks guest claim (403)
161 - - [x] Sandbox content not visible on item page (404 from second client)
162 - - [x] Sandbox RSS returns 404
163 - - [x] Sandbox per-IP cap: create MAX_PER_IP, verify next rejected
164 - - [x] Sandbox blog publish: no emails sent
165 -
166 - #### New: `revenue_splits.rs` (5 tests)
167 - - [x] Add project member with split percentage
168 - - [x] Update split percentage
169 - - [x] Remove project member
170 - - [x] Split recorded on purchase (full checkout + webhook + DB verification)
171 - - [x] Split export CSV contains correct data (both seller and collaborator perspectives)
172 -
173 - ### Phase 2: Add missing workflow modules
174 -
175 - #### New: `rate_limiting.rs` (5 tests)
176 - - [x] Auth rate limit: burst + 1 login attempts, verify 429
177 - - [x] Rate limit returns retry-after header
178 - - [x] Different IPs have independent rate limit buckets
179 - - [x] Sandbox creation rate limit: burst + 1, verify 429
180 - - [x] API write rate limit: burst + 1 POST requests, verify 429
181 -
182 - #### New: `promo_codes_checkout.rs` (6 tests)
183 - - [x] Percentage discount checkout: 50% off, verify half price in transaction
184 - - [x] Fixed discount checkout: $5 off $10 item, verify $5 in transaction
185 - - [x] Free access code: 100% off, verify $0 transaction with no Stripe session
186 - - [x] Expired promo code rejected (400)
187 - - [x] Max-uses exhausted: first buyer succeeds, second rejected
188 - - [x] Promo code reservation: use_count incremented on checkout start
189 -
190 - ### Phase 3: Harness optimization
191 -
192 - #### New helpers (added to harness/mod.rs)
193 - - [x] `connect_stripe(user_id, account_id)` — sets stripe_account_id + charges_enabled + onboarding + payouts in one call
194 - - [x] `create_creator_with_stripe(username)` — create_creator + grant_tier + connect_stripe in one call
195 - - [x] `create_buyers(count)` — batch-create buyer accounts, returns Vec<UserId>
196 -
197 - #### Documentation
198 - - [x] `tests/README.md` — documents all test types, harness features, constructors, running instructions, and fixtures
199 -
200 - #### Not needed
201 - - `TestHarness::minimal()` — `new()` is already the minimal constructor (DB only, no extras)
202 -
203 - ### Phase 4: Push to A+ (testing quality + coverage depth) ✓
204 -
205 - Current: A+ (1,137 unit + 689 integration = 1,826 tests, 15.0 unit tests/KLOC).
206 -
207 - #### Property-based testing (pricing/discount/formatting)
208 - - [x] Add `proptest` dev-dependency (v1)
209 - - [x] `pricing.rs`: 4 property tests — FreePricing always accessible, FixedPricing validate_amount consistent, PWYW enforces min+cap, Subscription always rejects direct purchase
210 - - [x] `promo_codes.rs`: 4 property tests — percentage in [0,price], fixed in [0,price], 100% always zero, 0% always identity
211 - - [x] `helpers.rs`: 5 property tests — format_price/format_revenue/format_bytes never panic, stripe fee invariant (fee+receives=price), slugify output always valid
212 - - [x] `Cents` arithmetic: 4 property tests — add commutative, add/sub match i64, sum matches fold
213 - - [x] `validated_types.rs`: 5 property tests — Username/Slug round-trip, PriceCents valid/negative/over-cap ranges
214 -
215 - #### Mutation testing
216 - - [x] Install `cargo-mutants` (v27.0.0) — 2026-04-29
217 - - [x] Run against `src/pricing.rs` — 99.4% kill rate (166/167 testable, 1 coincidental equivalence: `FreePricing::kind()` default == Free)
218 - - [x] Run against `src/db/promo_codes.rs` (`apply_discount`) — 100% kill rate (11/11)
219 - - [x] Run against `src/helpers.rs` (format_price, format_revenue, CSV sanitization, slugify, etc.) — 100% kill rate on tested mutants
220 - - [x] Run against `src/db/validated_types.rs` (Cents, PriceCents) — 100% kill rate after adding `as_i32`/`as_f64`/`price_cents` tests
221 - - [x] Combined targeted run: 199 mutants, 166 caught, 1 missed (equiv), 32 unviable. **99.4% kill rate.**
222 - - [x] Run against `src/auth.rs` (check_not_sandbox, check_not_suspended) — **100% kill rate** (3/3)
223 - - [x] Document mutation testing results and target kill rate (>90%) — `docs/internal/mutation_testing.md`
224 -
225 - #### Integration test lifecycle coverage
226 - - [x] Sandbox lifecycle: create → use features → backdate expiry → verify in expired set → CASCADE delete → verify gone (`lifecycle.rs`)
227 - - [x] Creator tier upgrade: small_files → big_files → everything, verify subscription row updated (not duplicated), denormalized column synced, dashboard loads (`lifecycle.rs`)
228 - - [x] Account deletion export window: request deletion → verify user and content still exist before confirmation (`lifecycle.rs`)
229 - - [x] Promo code lifecycle: create → verify → use twice → verify exhausted → try_increment fails → delete → verify gone (`lifecycle.rs`)
230 - - [x] Subscription lifecycle: subscribe → active → past_due (no access) → recover (access restored) → cancel → access revoked → tier soft-delete verified (`lifecycle.rs`)
231 -
232 - #### Concurrent access tests
233 - - [x] Concurrent purchase: 5 buyers claim same free item → sales_count exactly 5, 5 completed transactions (`lifecycle.rs`)
234 - - [x] Concurrent promo code: 2 sequential increments on max_uses=1 code → only 1 succeeds, use_count exactly 1 (`lifecycle.rs`)
235 - - [x] Concurrent sandbox creation: create to cap → next attempt returns 400, count stays at cap (`lifecycle.rs`)
236 - - [x] Concurrent storage increment: two concurrent uploads sum correctly; two uploads exceeding cap → only 1 succeeds (`lifecycle.rs`)
237 -
238 - #### Integration test performance monitoring
239 - - [x] Add test timing instrumentation: harness warns on >500ms DB clone, >1s harness build. Opt-in `record_test_timing()` writes CSV to `/tmp/mnw-test-timing.csv`
240 - - [x] Profile template DB creation: one-time migration run, per-test clone <500ms (no warnings triggered). Logged to stderr with `[test-harness]` prefix
241 - - [x] Identify slowest tests: only `concurrent_sandbox_per_ip_cap_holds` exceeds 60s (rate limiting by design). No other tests flagged >5s. Suite healthy, no optimization needed. Results in `docs/internal/test_performance.md`
242 -
243 - ---
244 -
245 - ## Code Fuzz Findings (2026-04-25)
246 -
247 - Three rounds of adversarial code review. 51 findings total: 50 fixed, 1 accepted risk, 2 deferred. Fixed items moved to todo_done.md.
248 -
249 - ### Accepted Risk
250 - - Idempotency check not atomic with operation — concurrent requests both execute (`db/idempotency.rs`). Safe because underlying ops are themselves idempotent.
251 - - Revoked session usable for up to 30s -- session cache IS cleared by revoke_session and revoke_other_sessions handlers; window only applies to direct DB manipulation (admin)
252 -
253 - ### Deferred
254 - - 7-day SyncKit JWT with no per-user revocation (`constants.rs:37`). Stolen token usable for full window. Requires key rotation infrastructure (SyncKit S4, post-beta).
255 - - Rate limit IP extraction trusts X-Forwarded-For when traffic bypasses Cloudflare (helpers.rs). Fix requires splitting rate limit extraction by path: CF-Connecting-IP for public web routes, peer socket for internal/CLI/git. Needs careful routing since CLI, git smart HTTP, and SyncKit all hit the same server but some bypass Cloudflare.
256 - - S3 key/file size UPDATE queries lack ownership in SQL -- defense-in-depth; callers verify ownership (db/items.rs)
257 -
258 - ## Test Fuzz (2026-04-29)
259 -
260 - 118 new unit tests (986 -> 1,104). 269 existing tests audited: 268 SOUND, 1 WEAK (redundant). 0 bugs found. All tests pass.
261 -
262 - ### Edge case tests added
263 - - [x] **pricing.rs** — 20 new tests: FixedPricing(0), PWYW $10k cap boundary, negative amounts, minimum_cents defaults, pwyw+zero price, i32::MAX boundaries, access matrix exhaustive. (42 -> 62)
264 - - [x] **helpers.rs** — 71 new tests: extract_client_ip (CF vs XFF priority, empty, whitespace, spoofing), ip_advisory_lock_key, format_price/revenue/bytes negatives and boundaries, slugify (unicode, XSS, SQL injection, zero-width, RTL override, 10k chars), parse_schedule_datetime (all 4 branches), stripe_timestamp, CSV injection (DDE, @, null bytes, tab/CR), hx_toast (quotes, angle brackets, JSON injection), estimate_stripe_fee (negative, 1 cent, huge), initials (whitespace, unicode), feed signature (empty, tampered). (35 -> 106)
265 - - [x] **validated_types.rs** — 28 new tests: Cents negative format_price/revenue, subtraction underflow, deref/div/rem/into, encode truncation documented, PriceCents boundary/zero/from_db/display, Slug only-hyphens/max-length, Username boundaries/underscore/numbers/hyphen-rejected, KeyCode empty segments. (20 -> 48)
266 - - [x] **promo_codes.rs** — 7 new tests: i32::MAX with 100%/99% discount, max+max fixed, both-negative, odd-price rounding, percentage invariant (6 prices x 9 percentages), fixed invariant (5 prices x 7 discounts). (20 -> 27)
267 - - [x] **validation/** — 18 new tests: slug only-hyphens/unicode, blob hash uppercase/mixed/valid/wrong-length, table name unicode, git repo path traversal/dot-git, label color edge cases, link URL internal IPs/port/auth/file scheme, SSH key too-large/whitespace, username all-underscores/numbers/unicode. (46 -> 64)
268 -
269 - ### Hardening applied
270 - - [x] `Cents` encode: added `debug_assert!` for i32 overflow in `Encode` impl — zero-cost in release, catches misuse in dev/test
271 -
272 - ### No bugs found — documented behaviors only
273 - - `apply_discount` with negative inputs: unreachable (DB CHECK constraints prevent negative prices/discounts)
274 - - `validate_link_url` accepts internal IPs: correct (URLs stored for display, never fetched server-side)
275 - - `Cents` i64->i32 encode: safe today (PriceCents caps at $10k), now guarded by debug_assert
276 -
277 - ## SyncKit Fuzz Findings (2026-04-29)
278 -
279 - ### Serious
280 - - [x] Push retry creates duplicate sync log entries — added `batch_id` column + unique index. Migration 083. Client generates UUID per push batch. Server dedup check before INSERT.
281 - - [x] Auth timing oracle in `sync_auth` — moved `verify_password` before suspension/lockout/2FA checks. All failures return 401.
282 -
283 - ### Medium
284 - - [x] `change_password` TOCTOU — added `expected_version` to PUT, server rejects with 409 on stale version. Added `Conflict` error variant.
285 -
286 - ### Minor
287 - - [x] `prune_sync_log` negative `retain_days` guard — added early return for `retain_days <= 0`.
288 - - [x] Blob size mismatch on concurrent confirm — changed `ON CONFLICT DO NOTHING` to `DO UPDATE SET size_bytes = EXCLUDED.size_bytes`.
289 - - [x] `validate-app` uses GET with API key in query string — changed to POST with JSON body.
290 -
291 - ## Audit Run 16 (2026-04-29)
292 -
293 - Overall grade: A- -> A (post-remediation). 75.5k LOC, 1,109 unit tests (14.7 tests/KLOC). 40+ findings resolved. Mutation kill rate 99.4%.
294 -
295 - ### Critical Fixes
296 - - [x] `bundles.rs::is_bundle_member` wrong column name (`child_item_id` -> `item_id`)
297 - - [x] Test harness broken — added `scan_semaphore` to `AppState` in test harness + load runner
298 -
299 - ### Testing
300 - - [x] **Scheduler tests** — extracted `jobs_for_tick()`, `is_webhook_dead()`, named constants. 15 unit tests. scheduler.rs: 0 -> 15 tests.
301 - - [x] **Cents tests** — 11 tests for formatting, arithmetic, conversions, serde roundtrip.
302 -
303 - ### Type Safety
304 - - [x] **`Cents` newtype** — `Cents(i64)` for all monetary values. 35+ fields across 15 files. Arithmetic, sqlx, serde, formatting methods. `PriceCents` converts via `From`.
305 - - [x] **SessionUser.creator_tier** — `Option<String>` -> `Option<CreatorTier>`.
306 - - [x] **OnboardingStep enum** — replaced magic integers 1/2/3.
307 - - [x] **format_price** — `i32` -> `impl Into<i64>`, unified with `Cents`.
308 -
309 - ### Performance (A-)
310 - - [x] **items.rs `move_item` N+1** — replaced N individual UPDATEs with single UNNEST batch update.
311 - - [x] **bundles.rs `set_bundle_items` loop insert** — replaced loop insert with single UNNEST batch insert.
312 - - [x] **follows.rs `NOT IN` anti-pattern** — replaced `NOT IN (SELECT LOWER(email) FROM email_suppressions)` with `NOT EXISTS` in both `get_follower_emails` and `get_broadcast_follower_count`.
313 - - [x] **creator_tiers.rs `get_storage_breakdown`** — replaced 6 sequential queries with a single CTE query returning all 6 category totals.
314 - - [x] **scheduler.rs `recalculate_all_storage_used`** — replaced N+1 per-user loop with single batch `UPDATE ... FROM (LATERAL joins)` query. Removed dead `recalculate_storage_used` and `get_all_creator_user_ids` functions.
315 - - [x] **auth.rs session touch** — extended `touch_session` to return `is_fan_plus` and `creator_tier` via subqueries, eliminating 2 extra DB round-trips on every uncached request.
316 - - [x] **discover.rs trigram scaling** — removed description from search clauses (titles only). Re-add description search later with proper full-text search index.
317 -
318 - ### Observability (A-)
319 - - [x] **storage.rs** — added warning log when `delete_prefix` default no-op is called.
320 - - [x] **config.rs startup log** — added structured info log of active features (s3, synckit_s3, stripe, scanner, mt, wam, git) in main.rs before scheduler start.
321 - - [x] **auth.rs** — added `#[instrument]` on `login_user`, `logout_user`, and `track_session`.
322 -
323 - ### Architecture (A-)
324 - - [ ] **scheduler.rs** — does too many things (publishing, email, cleanup, integrity checks, webhook retry). Consider splitting into submodules (scheduler/publishing.rs, scheduler/cleanup.rs, scheduler/integrity.rs) in a future pass.
325 - - [x] **scheduler.rs cleanup duplication** — extracted `cleanup_user_s3_and_delete` shared by sandbox, terminated, and content-removal cleanup. Also unified SyncKit/OTA cleanup into the shared helper (previously only sandbox had it).
326 -
327 - ### Codebase Size (A-)
328 - - [x] **exports.rs** — extracted `download_response()` helper, replacing 6 identical `Response::builder()` blocks.
329 - - [x] **auth.rs login notification** — extracted `maybe_send_login_notification()` in `auth.rs`, replacing duplicated code in password and passkey login paths. Also uses `extract_client_ip` instead of inline XFF parsing.
330 - - [x] **auth.rs SessionUser construction** — extracted `SessionUser::from_db_user()`, replacing 5 identical construction blocks across password, passkey, 2FA, and email-link login paths.
331 - - [x] **types/conversions.rs duration formatting** — extracted `format_duration()`, replacing duplicated logic for audio and video.
332 - - [ ] **templates/public.rs HealthTemplate** — ~90 fields. Consider grouping into sub-structs (HealthDbStatus, HealthStripeStatus, etc.).
333 - - [ ] **checkout.rs** (783 lines) — 6 repetitive `from_session` metadata extraction patterns. Consider a macro or shared trait.
334 - - [x] **email/tokens.rs** — truncated HMAC already documented (lines 268-271). No action needed.
335 - - [ ] **discover.rs** — code duplication across 3 search clause variants (short/long/none). Could reduce with a query builder.
336 - - [x] **guest_checkout.rs** — moved inline SQL to `db::transactions::create_free_guest_transaction` and `db::users::get_verified_user_id_by_email`.
337 -
338 - ### Testing (A- -> A)
339 - - [x] **error.rs** — 18 new tests: IntoResponse rendering for all variants, ResultExt context, internal error masking. (9 -> 27)
340 - - [x] **creator_tiers.rs** — 36 new tests: tier labels/prices/limits, format_bytes, StorageBreakdown. (0 -> 36)
341 - - [x] **conversions.rs** — 12 new tests: format_duration edge cases. (0 -> 12)
342 - - [x] **constants.rs** — 68 new tests: canary tests for all constants, ordering invariants, sanity bounds. (0 -> 68)
343 - - [x] **promo_codes.rs** — 15 new tests: percentage/fixed discount edge cases, overflow, clamping. (5 -> 20)
344 - - [x] **monitor.rs** — 11 new tests: status determination, alert transitions, content checks. (4 -> 15)
345 - - [x] **csv_converter.rs** — 30 new tests: empty CSV, special chars, price parsing, date parsing, unicode, long fields. (18 -> 48)
346 - - [x] **license_templates.rs** — 22 new tests: all presets, variable substitution, edge cases. (8 -> 30)
347 - - [x] **csrf.rs** — 13 new tests: token generation, verification, tampering, expiry, format. (6 -> 19)
348 - - [x] **rss.rs** — 15 new tests: XML escaping, empty feeds, all feed types, date format. (5 -> 20)
349 - - [x] **models/item.rs** — 8 new tests: computed fields, display formatting. (7 -> 15)
350 - - [ ] **lib.rs / main.rs** — no unit tests (covered by integration tests).
351 -
352 - ### Resilience (A-)
353 - - [x] **storage.rs `delete_prefix`** — added warning log when the default no-op is called.
354 -
355 - ### Frontend (A-)
356 - - [x] **Documentation pass** — added `//!` module docs to 5 internal API files (100% coverage for files >100 LOC). Added `///` struct docs to ~90 template structs across partials.rs, public.rs, and dashboard.rs.
357 - - [x] **email/notifications.rs** — fixed stale doc comment on line 442 (was "Send an alert email" above `send_tip_notification`).
358 - - [x] **checkout.rs tip truncation** — updated from 280 to 500 chars (Stripe metadata limit).
359 -
360 - ### Dependencies (B+)
361 - - [x] **Bump rand to 0.9.x** — bumped from 0.8.5. One API change: `distributions` -> `distr` (sandbox.rs).
362 - - [ ] **CONCURRENTLY index strategy** — no migrations use `CREATE INDEX CONCURRENTLY`. Plan for this before tables grow large (transactions, items, users).
363 - - [x] **.gitignore** — added secret-file pattern exclusions (`.pem`, `.key`, `.p8`, `.p12`, `.pfx`, `credentials.json`, `service-account.json`).
364 -
365 - ### Accepted (no action needed)
366 - - `git/raw.rs` `.unwrap()` on Response builders — safe (static headers), cosmetic
367 - - `promo_codes.rs` `.unwrap()` on `and_hms_opt(23,59,59)` — safe (static args)
368 - - analytics.rs 6 near-identical query blocks — correct and safe, query builder would add complexity
369 - - Inline styles (124 occurrences) — most are functional (dynamic widths, conditional visibility)
370 - - helpers.rs / pricing.rs observability B+ — pure functions, silence is appropriate
371 - - `enums.rs` size A- (1397 lines) — ~500 lines are tests, the rest is exhaustive enum definitions
372 - - `users.rs` / `items.rs` size A- — large but each function is focused, no extraction needed
373 -
374 - ---
375 -
376 - ## Sandbox Fuzz Findings (2026-04-28)
377 -
378 - Four-agent adversarial audit of sandbox feature. 12 findings: mechanical fixes applied inline, remainder tracked below.
379 -
380 - ### Fixed (mechanical)
381 - - [x] `check_not_sandbox()` added to: `add_domain`, `verify_domain`, `remove_domain` (domains.rs)
382 - - [x] `check_not_sandbox()` added to: `create_repo` (projects.rs)
383 - - [x] `check_not_sandbox()` added to: `start_import` (imports.rs)
384 - - [x] `check_not_sandbox()` added to: `claim_purchase` (guest_checkout.rs)
385 - - [x] Sandbox guard on blog publish side-effects: `send_blog_post_announcements` and `spawn_mt_thread_for_blog_post` skipped for sandbox users (blog.rs)
386 - - [x] `is_sandbox` check on RSS feeds: user_rss_feed, project_rss_feed, project_blog_rss return 404 for sandbox users (feeds.rs)
387 - - [x] `is_sandbox` check on item page: return 404 if item owner is sandbox (item.rs)
388 - - [x] Creator `is_sandbox` check in subscription checkout: reject before passing fake Stripe price IDs to Stripe API (checkout/subscriptions.rs)
389 -
390 - ### Remaining
391 - - [x] **IP header mismatch in sandbox cap** — unified IP extraction into `helpers::extract_client_ip()` (CF-Connecting-IP first, XFF fallback). Used by both sandbox handler and `track_session`. Ensures stored session IP matches cap query.
392 - - [x] **Race condition on per-IP cap** — PostgreSQL advisory lock (`pg_advisory_lock`) keyed on IP hash serializes concurrent sandbox creations from the same IP. Lock held from cap check through session tracking insert.
393 - - [x] **Orphaned SyncKit/OTA S3 objects** — sandbox cleanup now queries `sync_apps` for the user before CASCADE delete and cleans `{app_id}/` (blobs) and `ota/{app_id}/` (artifacts) on the SyncKit S3 bucket.
394 - - [x] **Dead sandbox file constants** — removed `SANDBOX_MAX_FILE_BYTES` and `SANDBOX_MAX_STORAGE_BYTES` (unreachable — `check_upload_allowed` rejects sandbox users at the tier-subscription check). Removed `max_file_override_bytes` from `create_sandbox_user` SQL.
395 -
396 - ### Accepted
397 - - Git repo disk cleanup on sandbox expiry — repos on disk are not cleaned by S3 cleanup. Low volume (sandbox users unlikely to create repos), and existing git disk cleanup scheduled task handles orphans. Not worth dedicated sandbox cleanup code.
398 - - Email to sandbox addresses — follower notifications could send to `sandbox_xxx@sandbox.local`. Mitigated by follows being blocked for sandbox users. Postmark rejects `.local` domains. Negligible risk.
399 -
400 - ## Code Fuzz Findings (2026-04-28)
401 -
402 - Six-agent adversarial code review. 21 findings total: 20 fixed, 1 accepted. Fixed items: command injection in build_runner (validation + shell escaping), guest checkout PWYW validation (uses pricing::for_item now), guest checkout promo code reservation ordering, build staleness timeout, project image scan status gating + storage quota decrement, CSP media-src dynamic from config, idempotency cache UTF-8 safety, scan concurrency semaphore, unreachable!() replaced, blob TOCTOU, SSE ordering, OAuth form-encoded, process::exit flush, hx_toast warning, 2FA lockout re-check, N+1 project export (batch chapters/versions/keys/promo_codes/blog_posts/bundles), N+1 bulk ownership (single ANY query), N+1 purchase export (batch title lookup).
403 -
404 - ### Accepted
405 - - Unbounded purchase export — intentional per creator trust audit (export limits removed 2026-04-27)
406 -
407 - ---
408 -
409 - ## Creator Trust Audit (2026-04-25, round 2 2026-04-26)
410 -
411 - Two rounds of creator-perspective audit. 25+ findings resolved (moved to todo_done.md). Incident post-mortems will publish as posts in the MNW Changelog blog project.
412 -
413 - ### Competitive Positioning (acknowledged, not bugs)
414 - - No free tier — deliberate tradeoff. Earn-back credit program planned.
415 - - No mobile fan app — creator apps exist, no general fan app.
416 - - No editorial discovery — search, tags, follows only. Interested in non-algorithmic discovery methods.
417 - - $10/mo minimum is biggest competitive gap vs Bandcamp/Gumroad/itch.io (all have free tiers).
418 -
419 - ---
420 -
421 - ## Creator Trust Audit (2026-04-27, round 3)
422 -
423 - Resolved (moved to todo_done.md): download budget removal, grace period duration, tax disclaimer, unlimited downloads doc, Stripe suspension doc, "original creative work" definition, post-cancellation retention, analytics "we don't track" expansion, Streaming→Everything rename, video "coming soon" labels removed, HSTS verified, git repo disk cleanup, succession plan, pricing page tier consistency, free item purchase redirect, dashboard Everything tier label.
424 -
425 - ### Remaining
426 - - [ ] **Legal/tax professional review** — prep doc at `docs/internal/legal_review_prep.md` with 41 specific questions across ToS, privacy, DMCA, payments, tax. Recommended: split engagement (internet attorney 3h + tax professional 1-2h)
427 -
428 - ## Creator Trust Audit (2026-04-27, round 6)
429 -
430 - ### Resolved
431 - - [x] Stripe availability note on creators.html page (link to stripe.com/global)
432 - - [x] Export limits removed: LIMIT clauses removed from sales (was 50k), followers (was 10k), subscribers (was 10k) export queries; file count cap (was 500) removed from content ZIP export (2GB memory safety cap retained)
433 - - [x] Video added to item type table in getting-started.md
434 - - [x] GDPR: SCC evaluation note + 30-day DSR response commitment added to privacy-policy.md [NEEDS LEGAL REVIEW]
435 - - [x] Stripe rejection path documented in payouts.md (honest: no alternative processor yet, actively exploring)
436 - - [x] Bandwidth policy already covered in tiers.md line 14
437 -
438 - ### Remaining
439 - - [ ] **Incident notification system** — Let creators opt into status alerts (email or webhook) when platform status changes. Monitoring infra is solid (PoM + internal monitor both detect issues); missing piece is proactive notification to creators. Implementation: subscribe endpoint on /health, store preferences in DB, trigger email on status transition (Operational -> Degraded/Error and recovery). Could reuse existing email infrastructure (Postmark).
440 -
441 - ## Creator Trust Audit (2026-04-28, round 7)
442 -
443 - ### Resolved (docs)
444 - - [x] **ToS general change notice bumped to 90 days** — Was 30 days, now matches pricing/privacy notice periods (terms-of-service.md)
445 - - [x] **Data retention reconciled** — moderation.md now says 30 days (matching privacy-policy.md), with explicit exceptions for unethical content (immediate removal) and ban evasion records (2 years). Added unlisting as intermediate action.
446 - - [x] **GDPR SCCs drafted** — privacy-policy.md international transfers section rewritten with SCC commitment [NEEDS LEGAL REVIEW]
447 - - [x] **Buyer notification gap documented** — guarantees.md now notes that buyer notification email is not yet implemented, with [NEEDS LEGAL REVIEW] on template
448 - - [x] **Free trial surfaced** — Added "Free trials available" link on landing page hero. Updated creators.html to mention free trial (2-6 weeks, no credit card) before sandbox.
449 - - [x] **Tax/VAT guidance added** — New "VAT, GST, and Sales Tax" section in payments.md covering creator obligations, Stripe Tax, MoR status. Cross-linked from pricing.md See Also.
450 - - [x] **Stale competition.md deleted** — Internal doc had 5+ shipped features still marked "Planned". Removed entirely rather than updating (public docs are source of truth).
451 - - [x] **Creator count template variable verified** — `{{ total_creators }}` is populated from DB via `count_active_creators()`. Not a bug.
452 - - [x] **Rejection info** — Will be included in rejection email itself, no separate doc needed.
453 -
454 - ### Remaining
455 - - [x] **Buyer notification email** — Email sent to all buyers when a creator deletes their account. Fires from `delete_account()` via `tokio::spawn`. Query: `get_all_buyers_for_seller()` (bypasses contact sharing since this is a platform notification). Template: `send_creator_departure_notification()` in notifications.rs.
456 - - [ ] **GDPR SCC execution** — Confirm SCCs are in place with Hetzner, AWS (S3), Stripe, Postmark. Part of legal review engagement.
457 - - [ ] **Independent appeals review** — Planned guarantee (guarantees.md). Requires second person. Track which admin made original decision, enforce different reviewer for appeals.
458 - - [ ] **COPPA/GDPR child consent** — Fan accounts allow 13+. EU sets digital consent at 16 in some member states. No parental consent mechanism exists. Part of legal review.
459 - - [ ] **Indemnification clause** — ToS lacks mutual indemnification. Flagged in legal_review_prep.md. Part of legal review engagement.
460 -
461 - ## Creator Trust Audit (2026-04-27, round 4)
462 -
463 - Resolved mechanically: fan-plus.md "not yet available" removed (feature is live). how-we-work.md video "not yet available" removed (video upload/playback works). roadmap.md embeds + video moved from Direction to What's Built. Vaporware table in todo-creator-trust-audit.md updated.
464 -
465 - ## Creator Trust Audit (2026-04-27, round 5)
466 -
467 - Verified correct (audit false positives): IP retention cleanup IS implemented (scheduler.rs:932-982, two daily jobs + streaming session cleanup). HSTS IS implemented (Caddyfile, all 5 server blocks). Pricing calculator already shows breakeven note and 9-competitor comparison.
468 -
469 - ### Remaining
470 - - [x] **Moderation warning system**: Renamed "Warning" to "Direct Message" across moderation.md, acceptable-use.md, code-of-conduct.md, copyright.md, and acceptable-use.html. Removed claims of formal warning records on account history. Now accurately describes what happens: an email explaining the issue, no formal tracking. Formal warning infrastructure can be added later when team grows.
471 -
472 - ### Docs — needs content decisions
473 - - [x] **Tax documentation**: Already covered in payouts.md (lines 33-53) — US 1099-K, non-US guidance, Stripe links, "not tax advice" disclaimer. Pattern: statements + links to Stripe, avoids hardcoded thresholds.
Lines truncated
@@ -0,0 +1,5 @@
1 + -- Track per-device pull cursor for intelligent sync log compaction.
2 + -- This allows pruning entries that ALL devices have already pulled,
3 + -- rather than relying solely on age-based retention (90 days).
4 +
5 + ALTER TABLE sync_devices ADD COLUMN last_pulled_seq BIGINT NOT NULL DEFAULT 0;
@@ -0,0 +1,19 @@
1 + -- Moderation action history: append-only record of all moderation events.
2 + -- Provides transparency (user can see their history) and audit trail (admin attribution).
3 +
4 + CREATE TABLE moderation_actions (
5 + id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6 + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
7 + admin_id UUID NOT NULL REFERENCES users(id),
8 + action_type VARCHAR(20) NOT NULL
9 + CHECK (action_type IN ('warning', 'content_removal', 'suspension', 'termination')),
10 + reason TEXT NOT NULL,
11 + -- Optional reference to specific content (item ID for content_removal)
12 + content_ref VARCHAR(255),
13 + -- NULL while action is active; set when resolved (warning acknowledged, suspension lifted, etc.)
14 + resolved_at TIMESTAMPTZ,
15 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
16 + );
17 +
18 + CREATE INDEX idx_moderation_actions_user ON moderation_actions(user_id, created_at DESC);
19 + CREATE INDEX idx_moderation_actions_active ON moderation_actions(user_id) WHERE resolved_at IS NULL;
@@ -52,7 +52,7 @@ Once approved as a creator, connect Stripe to receive fan payments:
52 52 3. Follow the Stripe onboarding flow
53 53 4. Complete identity verification (Stripe requirement)
54 54
55 - Payments go directly to your Stripe account. We never hold or touch your revenue.
55 + Payments go directly to your Stripe account. We never hold or touch your revenue. For details on payout timing, schedules, and currency, see [Payouts](./payouts.md).
56 56
57 57 ## Create Your First Project
58 58
@@ -160,8 +160,24 @@ After your first publish, here's what to focus on:
160 160 3. **Share your link.** Post your profile URL, project URL, or direct purchase link (`/buy/{item_id}`) wherever your audience is. Direct purchase links are minimal, focused pages optimized for social media and link-in-bio — fans can buy in one step without an account. You can also [point your own domain](./custom-domains.md) at your profile.
161 161 4. **Set up RSS cross-posting.** Connect your RSS feed to social media or newsletter tools. See [RSS](./rss.md).
162 162 5. **Fill in metadata.** Good titles, descriptions, tags, and cover art make your content discoverable and shareable. See [Metadata](./metadata.md). Per-file size limits and supported formats depend on your tier — see [Pricing Tiers](./tiers.md) for specifics.
163 - 6. **Understand how fans find you.** Your published work appears on the [Discover page](/discover), where fans can search, filter by tag, and browse by content type. There's no algorithm — visibility comes from good metadata and tags. See [Discovery](./discovery.md).
164 - 6. **Join the forum.** Say hello at [forums.makenot.work](https://forums.makenot.work). It's where platform feedback, feature requests, and creator-to-creator discussion happen.
163 + 6. **Check your analytics.** Your dashboard shows revenue, play counts, download counts, and per-project breakdowns. See [Analytics](./analytics.md).
164 + 7. **Understand how fans find you.** Your published work appears on the [Discover page](/discover), where fans can search, filter by tag, and browse by content type. There's no algorithm — visibility comes from good metadata and tags. See [Discovery](./discovery.md).
165 + 8. **Join the forum.** Say hello at [forums.makenot.work](https://forums.makenot.work). It's where platform feedback, feature requests, and creator-to-creator discussion happen.
166 +
167 + ## Building Your Audience
168 +
169 + Makenot.work is a selling tool, not a discovery engine. There is no algorithm that promotes content, no trending list, and no pay-to-rank. This is by design — it means you control your relationship with your audience directly, without platform interference.
170 +
171 + **How fans find your work:**
172 +
173 + - **Discover page.** All published items appear on [/discover](/discover), searchable by tag, content type, and keyword. Good metadata helps here.
174 + - **Direct links.** Share your profile (`/u/yourname`), project pages, or direct purchase links (`/buy/{item_id}`) on social media, your website, and link-in-bio tools. Purchase links are minimal one-step checkout pages optimized for sharing.
175 + - **RSS feeds.** Your projects generate RSS feeds that fans can subscribe to and that you can cross-post to social media or newsletter tools.
176 + - **Embeds.** Embed buy buttons, product cards, or audio players on your own website. See [Embeds](./embeds.md).
177 + - **Mailing lists.** Every project has a built-in mailing list. Fans get notified when you publish. You can send broadcasts to your followers. See [Mailing Lists](./mailing-lists.md).
178 + - **Follows and feeds.** Fans who follow your projects see new releases in their personal feed.
179 +
180 + **What this means for you:** Bring your existing audience. Post your links where your fans already are. The platform handles the selling, hosting, delivery, and payments — but the marketing is yours.
165 181
166 182 ## See Also
167 183
@@ -7,7 +7,7 @@ Choose the tier that matches your content. Every tier includes all features from
7 7 | **Basic** | $10 | Text, blogs, newsletters | 10MB | 50GB |
8 8 | **Small Files** | $20 | Audio, plugins, small software | 500MB | 250GB |
9 9 | **Big Files** | $30 | Video, games, large software | 20GB | 500GB |
10 - | **Everything** | $60 | Live streaming, all features, current and future | 20GB | 500GB |
10 + | **Everything** | $60 | All features, current and future (live streaming coming soon) | 20GB | 500GB |
11 11
12 12 All tiers include: 0% platform fee on fan payments, custom profile, project organization, data export, subscriptions, RSS, [analytics](./analytics.md), 2FA/passkeys.
13 13
@@ -88,27 +88,29 @@ For game developers, educators, course creators, and anyone producing large cont
88 88
89 89 ## Everything — $60/month
90 90
91 - For live streamers and creators who want every feature the platform offers, now and in the future.
91 + For creators who want every feature the platform offers, now and in the future.
92 92
93 93 ### What You Get (in addition to Big Files)
94 94
95 - - **Live streaming** — stream to your audience from OBS or any RTMP/SRT software. 0% platform fee on donations and tips during streams.
96 - - **20 hours/month of streaming included** — covers a weekly 4-hour stream comfortably
97 - - **Additional streaming at cost** — $0.10/hour beyond included hours (our actual infrastructure cost, no markup)
98 - - **VOD archival** — past streams stored and accessible to subscribers
99 - - **0% fee on stream donations** — fans tip you during streams, you keep everything minus Stripe's ~3% processing
95 + - **Live streaming (coming soon)** — stream to your audience from OBS or any RTMP/SRT software. 0% platform fee on donations and tips during streams. 20 hours/month included, $0.10/hour beyond that.
100 96 - Every current feature included in lower tiers
101 97 - Embeddable players and widgets, license keys, promo codes, subscriptions
102 - - First access to new features as they ship (adaptive transcoding, and anything else on the [roadmap](../about/roadmap.md))
98 + - First access to new features as they ship (adaptive transcoding, live streaming, and anything else on the [roadmap](../about/roadmap.md))
103 99 - Priority for per-file size increases beyond 20GB
104 100
105 101 The Everything tier is a commitment: as the platform grows, this tier always includes the full feature set. You won't need to upgrade again.
106 102
107 - ### Streaming Usage
103 + ### Live Streaming (Coming Soon)
108 104
109 - Your $60/month includes 20 hours of live streaming per month. Beyond that, additional streaming is billed at **$0.10 per hour** — this is our actual infrastructure cost with no markup. Your dashboard shows hours used and remaining in the current billing period.
105 + Live streaming is on the roadmap but not yet available. When it ships, the Everything tier will include:
110 106
111 - For context: a creator streaming 4 hours per week uses 16-20 hours/month, well within the included amount. A creator streaming daily for 2 hours uses ~60 hours/month and would pay ~$4 in overage.
107 + - Stream to your audience from OBS or any RTMP/SRT software
108 + - 20 hours/month of streaming included (covers a weekly 4-hour stream)
109 + - Additional streaming at cost: $0.10/hour beyond included hours (our actual infrastructure cost, no markup)
110 + - VOD archival: past streams stored and accessible to subscribers
111 + - 0% fee on stream donations: fans tip you during streams, you keep everything minus Stripe's ~3% processing
112 +
113 + If you subscribe to the Everything tier today, you get all current Big Files features plus priority access to streaming and every other new feature as it launches.
112 114
113 115 ### Storage
114 116
@@ -48,6 +48,9 @@ You do. A chargeback happens when a buyer disputes a charge with their bank. Sin
48 48 ### How fast do payouts arrive?
49 49 The payment processor handles payouts: 2 business days typical in the US, short hold for your first payout, instant payouts available after an initial period (1% fee).
50 50
51 + ### What if Stripe suspends or closes my account?
52 + Your content and data on Makenot.work are unaffected — you can still log in, manage your projects, and export everything. However, you won't be able to receive payments until the Stripe issue is resolved. Stripe handles disputes directly with you (they're the payment processor, not us). If Stripe permanently closes your account, we don't currently offer an alternative payment processor. You'd need to export your data and sell elsewhere. We're monitoring alternative processors but have no timeline. See [Payouts](../guide/payouts.md) for more detail.
53 +
51 54 ### Can I sell adult/NSFW content?
52 55 Not on Makenot.work. We intend to launch a separate platform for adult creators with identical commitments when infrastructure is ready.
53 56
@@ -93,6 +96,9 @@ Source code is public — every claim on this site is verifiable by reading it.
93 96 ### What stops you from changing the terms?
94 97 The code is public, the documentation is versioned, and you control your data at all times. If the terms change in a way you don't like, you can export everything and leave in minutes. The [SLA](../about/guarantees.md) guarantees that.
95 98
99 + ### What jurisdiction governs disputes?
100 + Colorado (US) law, resolved in Colorado courts. If you're outside the US, be aware that any legal dispute would take place under US law in a US court. We prefer to resolve disagreements directly before it comes to that — reach out to [support](./contact.md) first.
101 +
96 102 ### What data do you collect?
97 103 Account info you provide, content you upload, transactions you conduct. No browsing profiles, no behavioral tracking. Verifiable in the source code.
98 104
@@ -79,6 +79,10 @@ Automated, bulk, or vague takedown requests will be deprioritized or rejected. W
79 79
80 80 We terminate accounts of repeat infringers as required by law. "Repeat infringer" means someone who has had multiple valid claims upheld after review—not someone who has received multiple frivolous claims.
81 81
82 + ## Known Limitations
83 +
84 + **Free content CDN URLs do not expire.** When a CDN is configured, download URLs for free items are static and permanent. Someone with the URL can download without visiting the platform, bypassing download count tracking. This does not affect paid content (which always uses time-limited, authenticated URLs). We consider this acceptable because free content is already publicly accessible to anyone — the URL just skips the middleman. Adding signed CDN URLs would add complexity for no revenue or security benefit.
85 +
82 86 ## See Also
83 87
84 88 - [Terms of Service](../legal/terms-of-service.md) — Full legal terms including content policies
@@ -9,7 +9,7 @@ This isn't a feature—it's a core principle.
9 9 ## What's Included
10 10
11 11 ### Content
12 - - All uploaded files in original quality
12 + - All uploaded files in original quality (ZIP export has a 2GB size cap per batch; export by project if your catalog is larger)
13 13 - Cover art and images (when uploaded)
14 14
15 15 ### Metadata
@@ -39,6 +39,7 @@ pub const SYNCKIT_PUSH_MAX_CHANGES: usize = 500;
39 39 pub const SYNCKIT_PULL_PAGE_SIZE: i64 = 500;
40 40 pub const SYNCKIT_API_KEY_LENGTH: usize = 32; // 32 bytes = 64 hex chars
41 41 pub const SYNC_LOG_RETAIN_DAYS: i64 = 90;
42 + pub const SYNC_LOG_COMPACT_MIN_AGE_DAYS: i64 = 7; // Safety margin for cursor-based compaction
42 43 pub const SYNCKIT_MAX_BLOB_SIZE_BYTES: i64 = 500 * 1024 * 1024; // 500 MB
43 44 pub const SYNCKIT_BLOB_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
44 45 pub const SYNCKIT_MAX_SSE_CONNECTIONS_PER_USER: usize = 10;
@@ -41,7 +41,11 @@ impl std::fmt::Display for TimeRange {
41 41
42 42 impl TimeRange {
43 43 /// SQL interval string for the current period, or `None` for All.
44 - fn interval_sql(&self) -> Option<&str> {
44 + ///
45 + /// SAFETY: These values are interpolated into SQL via format!. They MUST be
46 + /// compile-time constants with no user input. The exhaustive match ensures
47 + /// new variants require explicit SQL strings.
48 + fn interval_sql(&self) -> Option<&'static str> {
45 49 match self {
46 50 Self::Days7 => Some("7 days"),
47 51 Self::Days30 => Some("30 days"),
@@ -51,7 +55,9 @@ impl TimeRange {
51 55 }
52 56
53 57 /// SQL date_trunc bucket size: day for short ranges, week for 90d, month for All.
54 - fn bucket_sql(&self) -> &str {
58 + ///
59 + /// SAFETY: Interpolated into SQL via format!. Must be compile-time constants.
60 + fn bucket_sql(&self) -> &'static str {
55 61 match self {
56 62 Self::Days7 | Self::Days30 => "day",
57 63 Self::Days90 => "week",
@@ -5,7 +5,7 @@ use sqlx::PgPool;
5 5
6 6 use super::models::*;
7 7 use super::validated_types::Slug;
8 - use super::{BlogPostId, ProjectId, UserId};
8 + use super::{BlogPostId, MtThreadId, ProjectId, UserId};
9 9 use crate::error::Result;
10 10
11 11 /// Insert a new blog post and return the created row.
@@ -225,7 +225,7 @@ pub async fn delete_blog_post(pool: &PgPool, id: BlogPostId) -> Result<()> {
225 225 pub async fn set_mt_thread_id(
226 226 pool: &PgPool,
227 227 blog_post_id: BlogPostId,
228 - thread_id: uuid::Uuid,
228 + thread_id: MtThreadId,
229 229 ) -> Result<()> {
230 230 sqlx::query("UPDATE blog_posts SET mt_thread_id = $2 WHERE id = $1")
231 231 .bind(blog_post_id)
@@ -625,6 +625,33 @@ pub async fn check_presign_allowed(
625 625 Ok(())
626 626 }
627 627
628 + /// Get total known file sizes for a user (versions + content insertions).
629 + /// Used by the account deletion form to show how much data will be removed.
630 + #[tracing::instrument(skip_all)]
631 + pub async fn get_user_content_size(pool: &PgPool, user_id: UserId) -> Result<i64> {
632 + let version_size: i64 = sqlx::query_scalar(
633 + r#"
634 + SELECT COALESCE(SUM(v.file_size_bytes)::BIGINT, 0)
635 + FROM versions v
636 + JOIN items i ON v.item_id = i.id
637 + JOIN projects p ON i.project_id = p.id
638 + WHERE p.user_id = $1 AND v.s3_key IS NOT NULL
639 + "#,
640 + )
641 + .bind(user_id)
642 + .fetch_one(pool)
643 + .await?;
644 +
645 + let insertion_size: i64 = sqlx::query_scalar(
646 + "SELECT COALESCE(SUM(file_size)::BIGINT, 0) FROM content_insertions WHERE user_id = $1",
647 + )
648 + .bind(user_id)
649 + .fetch_one(pool)
650 + .await?;
651 +
652 + Ok(version_size + insertion_size)
653 + }
654 +
628 655 #[cfg(test)]
629 656 mod tests {
630 657 use super::*;
@@ -896,6 +896,54 @@ impl_str_enum!(ImportJobStatus {
896 896 Failed => "failed",
897 897 });
898 898
899 + // -- Moderation action types --------------------------------------------------
900 +
901 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
902 + pub enum ModerationActionType {
903 + Warning,
904 + Suspension,
905 + Termination,
906 + ContentRemoval,
907 + }
908 +
909 + impl_str_enum!(ModerationActionType {
910 + Warning => "warning",
911 + Suspension => "suspension",
912 + Termination => "termination",
913 + ContentRemoval => "content_removal",
914 + });
915 +
916 + // -- Checkout types (Stripe metadata) -----------------------------------------
917 +
918 + /// Discriminator for checkout session types stored in Stripe metadata.
919 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
920 + pub enum CheckoutType {
921 + Guest,
922 + Subscription,
923 + Tip,
924 + FanPlus,
925 + CreatorTier,
926 + }
927 +
928 + impl_str_enum!(CheckoutType {
929 + Guest => "guest",
930 + Subscription => "subscription",
931 + Tip => "tip",
932 + FanPlus => "fan_plus",
933 + CreatorTier => "creator_tier",
934 + });
935 +
936 + impl ModerationActionType {
937 + pub fn label(&self) -> &'static str {
938 + match self {
939 + Self::Warning => "Warning",
940 + Self::Suspension => "Suspension",
941 + Self::Termination => "Termination",
942 + Self::ContentRemoval => "Content Removal",
943 + }
944 + }
945 + }
946 +
899 947 #[cfg(test)]
900 948 mod tests {
901 949 use super::*;
@@ -1394,4 +1442,27 @@ mod tests {
1394 1442 assert_eq!("failed".parse::<ImportJobStatus>().unwrap(), ImportJobStatus::Failed);
1395 1443 assert!("bogus".parse::<ImportJobStatus>().is_err());
1396 1444 }
1445 +
1446 + #[test]
1447 + fn checkout_type_round_trip() {
1448 + assert_eq!(CheckoutType::Guest.to_string(), "guest");
1449 + assert_eq!(CheckoutType::Subscription.to_string(), "subscription");
1450 + assert_eq!(CheckoutType::Tip.to_string(), "tip");
1451 + assert_eq!(CheckoutType::FanPlus.to_string(), "fan_plus");
1452 + assert_eq!(CheckoutType::CreatorTier.to_string(), "creator_tier");
1453 + assert_eq!("guest".parse::<CheckoutType>().unwrap(), CheckoutType::Guest);
1454 + assert_eq!("fan_plus".parse::<CheckoutType>().unwrap(), CheckoutType::FanPlus);
1455 + assert!("bogus".parse::<CheckoutType>().is_err());
1456 + }
1457 +
1458 + #[test]
1459 + fn moderation_action_type_round_trip() {
1460 + assert_eq!(ModerationActionType::Warning.to_string(), "warning");
1461 + assert_eq!(ModerationActionType::Suspension.to_string(), "suspension");
1462 + assert_eq!(ModerationActionType::Termination.to_string(), "termination");
1463 + assert_eq!(ModerationActionType::ContentRemoval.to_string(), "content_removal");
1464 + assert_eq!("warning".parse::<ModerationActionType>().unwrap(), ModerationActionType::Warning);
1465 + assert_eq!("content_removal".parse::<ModerationActionType>().unwrap(), ModerationActionType::ContentRemoval);
1466 + assert!("bogus".parse::<ModerationActionType>().is_err());
1467 + }
1397 1468 }
@@ -182,6 +182,10 @@ define_pg_uuid_id!(
182 182 TipId,
183 183 ProjectMemberId,
184 184 RevenueSplitId,
185 + ModerationActionId,
186 + MtThreadId,
187 + ClaimToken,
188 + DownloadToken,
185 189 );
186 190
187 191 #[cfg(test)]
@@ -4,7 +4,7 @@ use sqlx::PgPool;
4 4
5 5 use super::enums::{AiTier, ItemType};
6 6 use super::models::*;
7 - use super::{ItemId, ProjectId, UserId};
7 + use super::{ItemId, MtThreadId, PriceCents, ProjectId, UserId};
8 8 use crate::error::Result;
9 9
10 10 /// Insert a new item into a project and return the created row.
@@ -17,7 +17,7 @@ pub async fn create_item(
17 17 project_id: ProjectId,
18 18 title: &str,
19 19 description: Option<&str>,
20 - price_cents: i32,
20 + price_cents: PriceCents,
21 21 item_type: ItemType,
22 22 ai_tier: AiTier,
23 23 ai_disclosure: Option<&str>,
@@ -225,11 +225,11 @@ pub async fn update_item(
225 225 user_id: UserId,
226 226 title: Option<&str>,
227 227 description: Option<&str>,
228 - price_cents: Option<i32>,
228 + price_cents: Option<PriceCents>,
229 229 item_type: Option<ItemType>,
230 230 is_public: Option<bool>,
231 231 pwyw_enabled: Option<bool>,
232 - pwyw_min_cents: Option<i32>,
232 + pwyw_min_cents: Option<PriceCents>,
233 233 publish_at: Option<Option<chrono::DateTime<chrono::Utc>>>,
234 234 web_only: Option<bool>,
235 235 ai_tier: Option<AiTier>,
@@ -539,7 +539,7 @@ pub async fn mark_release_announced(pool: &PgPool, item_id: ItemId) -> Result<bo
539 539 pub async fn set_mt_thread_id(
540 540 pool: &PgPool,
541 541 item_id: ItemId,
542 - thread_id: uuid::Uuid,
542 + thread_id: MtThreadId,
543 543 ) -> Result<()> {
544 544 sqlx::query("UPDATE items SET mt_thread_id = $2 WHERE id = $1")
545 545 .bind(item_id)
@@ -1087,3 +1087,15 @@ pub async fn admin_restore_item(
1087 1087
1088 1088 Ok(item)
1089 1089 }
1090 +
1091 + /// Count public, listed items (for landing page stats).
1092 + #[tracing::instrument(skip_all)]
1093 + pub async fn count_public_listed(pool: &PgPool) -> Result<i64> {
1094 + let count = sqlx::query_scalar::<_, i64>(
1095 + "SELECT COUNT(*) FROM items WHERE is_public = true AND listed = true",
1096 + )
1097 + .fetch_one(pool)
1098 + .await?;
1099 +
1100 + Ok(count)
1101 + }