max / makenotwork
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 |
| @@ -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 | + | } |