Skip to main content

max / makenotwork

Server hardening, docs updates, new migrations, SyncKit improvements - Migrations 089-093: key rotation, fuzz fixes, notify status, media scan status, creator pause - SyncKit client: key rotation support, sync improvements - Server: CSRF, rate limit, monitor, payment, session improvements - Route hardening across API, storage, Stripe, dashboard - Doc fuzz updates across site-docs and internal docs - Business docs: app sync pricing, expenses, partnership targets - New guides: embeds, feed, import, media library, password reset - Creator pause feature support - Todo updates across server, MT, PoM, SyncKit
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-04 03:01 UTC
Commit: 39e6303e1f170b3b2da6a9fec3ed9b5f5d09af1b
Parent: 4350332
121 files changed, +4914 insertions, -496 deletions
@@ -3385,7 +3385,7 @@
3385 3385
3386 3386 [[package]]
3387 3387 name = "makenotwork"
3388 - version = "0.4.8"
3388 + version = "0.4.9"
3389 3389 dependencies = [
3390 3390 "anyhow",
3391 3391 "argon2",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.4.8"
3 + version = "0.4.9"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -1,7 +1,7 @@
1 1 # mnw-cli TODO
2 2
3 3 ## Status
4 - Done: Phases 1-8, Git proxy A-D (except D5), UX audit fixes (8/8). Active: None. Next: D5 DNS, PoM health check, deploy.
4 + Done: Phases 1-8, Git proxy A-D (incl D5 DNS), UX audit fixes (8/8). Deployed 2026-05-03. Active: None. Next: PoM health check, astra test.
5 5
6 6 ---
7 7
@@ -26,7 +26,7 @@
26 26 - [x] D2: Update mnw-cli .env (`SSH_PORT=22`, `GIT_SUDO_USER=git`) — done 2026-04-22
27 27 - [x] D3: Sudoers rule (`/etc/sudoers.d/mnw-cli-git` — git-upload-pack, git-receive-pack, git-upload-archive, git, tee, chmod) — done 2026-04-22, extended 2026-05-03
28 28 - [x] D4: Firewall — removed 2222/tcp, added 2200/tcp — done 2026-04-22
29 - - [ ] D5: DNS — `cli.makenot.work` A record -> `5.78.144.244`, proxy OFF (needs Cloudflare dashboard)
29 + - [x] D5: DNS — `cli.makenot.work` A record -> `5.78.144.244`, proxy OFF — done 2026-05-03
30 30 - [x] D6: Restart sequence verified — admin SSH on 2200, mnw-cli on 22, both running — done 2026-04-22
31 31
32 32 ## Deploy
@@ -19,11 +19,26 @@
19 19 - [ ] Crashes (crash reports from DS2)
20 20 - [ ] Feedback (user feedback from DS3)
21 21
22 + ### Fan+ Feature Gating
23 + - [ ] Signatures (text + image, rendered on every post) — Fan+ only
24 + - [ ] Custom / larger profile images — Fan+ only (free accounts get generated avatar)
25 + - [ ] Image and video embeds in posts — Fan+ only (free accounts post text only)
26 + - [ ] Creator auto-grant: creators get all Fan+ forum perks in own communities (no + badge)
27 + - [ ] + badge rendering in post author display
28 +
22 29 ### Private Communities (Fan+)
23 30 - [ ] Community visibility flag (public/private)
24 31 - [ ] Membership gating: restrict join to Fan+ subscribers or item buyers
25 32 - [ ] Hidden from public listing, accessible only via direct link or MNW project page
26 33
34 + ### Community Moderation Enforcement
35 + - [ ] Restricted state: disable new thread creation for non-moderators
36 + - [ ] Frozen state: community goes read-only, mods can still take mod actions to unfreeze
37 + - [ ] Clean slate mechanism: clear all threads/posts, preserve settings/categories, post system notice
38 + - [ ] Archived state with reactivation path
39 + - [ ] PoM integration: flag age monitoring, flag-to-action ratio alerts
40 + - [ ] Document moderation policy publicly (see `docs/internal/moderation_policy.md` for internal version)
41 +
27 42 ### Notification Integration
28 43 - [ ] Push mentions, replies, endorsements, flags to MNW notifications API
29 44 - [ ] Read state synced with MNW notification center
@@ -190,3 +190,35 @@
190 190 # Check cron is scheduled
191 191 sudo crontab -u makenotwork -l
192 192 ```
193 +
194 + ---
195 +
196 + ## Monthly Restore Test
197 +
198 + Run once per month to verify backups are actually restorable. Use the offsite copy on astra to avoid touching production.
199 +
200 + ```bash
201 + # On astra — restore latest backup to a scratch database
202 + LATEST=$(ls -t /opt/backups/mnw/makenotwork-*.sql.gz | head -1)
203 + sudo -u postgres createdb mnw_restore_test -O postgres
204 + gunzip -c "$LATEST" | psql -U postgres -d mnw_restore_test -q
205 +
206 + # Verify row counts (should be non-zero)
207 + psql -U postgres -d mnw_restore_test -c "SELECT 'users' AS t, COUNT(*) FROM users UNION ALL SELECT 'projects', COUNT(*) FROM projects UNION ALL SELECT 'items', COUNT(*) FROM items UNION ALL SELECT 'transactions', COUNT(*) FROM transactions;"
208 +
209 + # Verify migration state
210 + psql -U postgres -d mnw_restore_test -c "SELECT COUNT(*) AS migrations FROM _sqlx_migrations;"
211 +
212 + # Clean up
213 + sudo -u postgres dropdb mnw_restore_test
214 +
215 + # Log result
216 + echo "$(date -u +%Y-%m-%d) restore-test OK: $LATEST" >> /opt/backups/mnw/restore-test.log
217 + ```
218 +
219 + If any step fails, investigate immediately — a backup that can't be restored isn't a backup.
220 +
221 + Schedule via cron on astra (first of each month):
222 + ```
223 + 0 4 1 * * /opt/backups/mnw/test-restore.sh >> /opt/backups/mnw/restore-test.log 2>&1
224 + ```
@@ -1,31 +1,31 @@
1 1 # MakeNotWork -- Audit Review
2 2
3 - **Last audited:** 2026-05-01 (Run 18, MNW server only)
4 - **Previous audit:** 2026-04-30 (Run 17, cross-project)
3 + **Last audited:** 2026-05-02 (Run 19, MNW server + doc fuzz)
4 + **Previous audit:** 2026-05-01 (Run 18, MNW server only)
5 5
6 6 ## Overall Grade: A
7 7
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).
8 + Run 19: 1,923 tests passing (1,213 unit + 679 integration + 28 doc + 3 load; 10 ignored), 0 failed. 0 clippy warnings. v0.4.7. ~81,384 LOC. 2 cold spots (0 bugs, 2 minor). Combined with doc fuzz for creator email readiness assessment.
9 9
10 10 ## Scorecard
11 11
12 12 | Dimension | Grade | Notes |
13 13 |-----------|:-----:|-------|
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 |
14 + | Code Quality | A | unwrap() in git/raw.rs fixed. helpers.rs split into formatting/crypto/rate_limit (395 lines from 1,268) |
15 + | Architecture | A | Inline SQL in route handlers still present (4 locations) but minor |
16 + | Testing | A | 1,923 tests, 0 failures. proptest active. Lockout test fixed (auth rate limit fast-tests override) |
17 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 |
18 + | Performance | A | analytics.rs deduplicated (623->468 LOC). hash_lookup uses static reqwest::Client |
19 + | Documentation | A- | Module-level //! on every file. No README.md (CONTRIBUTING.md partially fills role) |
20 + | Dependencies | A- | Most deps current. tokio (1.49->1.50), uuid (1.20->1.22), chrono (0.4.43->0.4.44) pins stale |
21 + | Frontend | A | Askama auto-escape, all `\|safe` uses verified safe, no raw innerHTML. No CSP header (inline onclick pattern) |
22 + | Type Safety | A | 50+ UUID newtypes, validated string types, Cents monetary newtype. moderation.rs still uses raw Uuid |
23 + | Observability | A | Comprehensive #[instrument] coverage. Structured logging throughout |
24 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 |
25 + | Resilience | A | Graceful shutdown with hard deadline. One missing timeout: payments/connect.rs:198 raw reqwest call |
26 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 |
27 + | Migration Safety | A | 87 additive migrations, IF EXISTS on drops, CHECK constraints |
28 + | Codebase Size | A | 81K LOC well-organized. helpers.rs split complete. analytics.rs deduplicated |
29 29
30 30 ## Module Heatmap
31 31
@@ -95,18 +95,22 @@
95 95
96 96 ### Cold Spots
97 97
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.
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. (Carried from Run 18)
99 + 2. **payments/connect.rs resilience (B):** Missing timeout on raw reqwest call to Stripe resume-subscription API at line 198. Only unprotected outbound HTTP call in the codebase.
100 +
101 + ### Resolved Cold Spots (from Run 18)
102 + - ~~routes/git/raw.rs unwrap()~~ -- Fixed
103 + - ~~db/analytics.rs duplication~~ -- Fixed (Scope enum extraction)
104 + - ~~wam_client.rs testing~~ -- Fixed (5 tests added)
103 105
104 106 ## Mandatory Surprise
105 107
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'`.
108 + **Hand-rolled Stripe v2 webhook signature verification.** `payments/webhooks.rs:184-228` implements v2 webhook signature verification from scratch because the `stripe` crate only supports v1. The implementation correctly parses `t=,v1=` headers, rejects timestamps >300s in either direction (replay + clock skew), and uses `hmac::Hmac::verify_slice()` which is constant-time via the `subtle` crate. Both past and future timestamp bounds are checked. Well-tested with 26 unit tests including edge cases (stale, future, wrong secret, within tolerance).
107 109
108 110 ### Previous Surprises
109 111
112 + **Run 18:** Sandbox tier mismatch bug (SmallFiles vs small_files). Fixed.
113 +
110 114 **Run 17:** TOCTOU-safe slug generation with retry loop + advisory lock pattern for sandbox IP cap.
111 115
112 116 **Run 15:** Session touch cache — DashMap with 30s TTL avoids N+1 session queries.
@@ -140,20 +144,15 @@
140 144
141 145 ## Action Items
142 146
143 - ### Run 18 (2026-05-01)
147 + ### Run 19 (2026-05-02)
144 148
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
149 + 51. **[MEDIUM]** Add timeout to `payments/connect.rs:198` raw reqwest call to Stripe resume-subscription API
150 + 52. **[MEDIUM]** Add `ModerationActionId` newtype and `ModerationActionType` enum to `db/moderation.rs` (carried from Run 18 #41)
151 + 53. **[LOW]** Add README.md to server/ with setup instructions and architecture overview link
152 + 54. **[LOW]** Bump dependency pins: tokio 1.49->1.50, uuid 1.20->1.22, chrono 0.4.43->0.4.44, yara-x 1.13->1.14, anyhow 1.0.101->1.0.102
153 + 55. **[DEFERRED]** Extract inline SQL from route handlers to db/ layer (4 locations, carried from Run 18 #40)
154 + 56. **[DEFERRED]** Remove `async-trait` in favor of Rust 2024 native async traits (carried from Run 18 #50)
155 + 57. **[DEFERRED]** Migrate inline `onclick` handlers to `addEventListener` to enable strict CSP
157 156
158 157 ### Open (blocked on upstream)
159 158 23. Monitor aws-sdk-s3 for lru fix (RUSTSEC-2026-0002)
@@ -162,19 +161,27 @@
162 161 33. bincode unmaintained (RUSTSEC-2025-0141) — upstream via syntect/yara-x, warning only
163 162
164 163 ### Previously resolved
165 - All items 1-22 and 31-38 from previous audits verified intact.
164 + All items 1-22, 31-38, and most Run 18 items verified intact.
166 165
167 - ## Previous Action Item Verification
166 + ## Previous Action Item Verification (Run 18)
168 167
169 168 | # | Item | Status |
170 169 |---|------|--------|
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 |
170 + | 39 | Sandbox tier bug (`'SmallFiles'`->`'small_files'`) | Fixed |
171 + | 40 | Extract inline SQL from route handlers | Unfixed (carried as #55) |
172 + | 41 | Moderation typed IDs | Unfixed (carried as #52) |
173 + | 42 | `.unwrap()` in git/raw.rs | Fixed |
174 + | 43 | Missing `#[instrument]` on public_projects | Fixed |
175 + | 44 | `.unwrap()` in helpers.rs | Fixed (helpers.rs split) |
176 + | 45 | `.unwrap()` in monitor.rs | Fixed |
177 + | 46 | wam_client.rs unit tests | Fixed (5 tests added) |
178 + | 47 | CSV import cents/dollars format | Fixed (heuristic removed) |
179 + | 48 | Split helpers.rs | Fixed (1,268->395 lines: formatting.rs, crypto.rs, rate_limit.rs) |
180 + | 49 | Analytics.rs deduplication | Fixed (623->468 lines via Scope enum) |
181 + | 50 | Remove async-trait | Unfixed (carried as #56) |
182 + | 23-25, 33 | Upstream dep advisories | Unfixed (chronic, upstream-blocked) |
176 183
177 - No regressions found. Items 23-25, 33 remain open across 3+ consecutive audits (chronic, but upstream-blocked).
184 + 10 of 12 Run 18 items fixed. 2 carried forward. No regressions.
178 185
179 186 ## Metrics Over Time
180 187
@@ -197,6 +204,7 @@
197 204 | 2026-04-22 (Run 15 corrected) | ~67,442 | -- | 1,359 | ~20 | 0 | 1 | A |
198 205 | 2026-04-30 (Run 17) | ~79,334 | -- | 1,861 | ~15.0 | 0 | 0 | A |
199 206 | 2026-05-01 (Run 18) | ~80,470 | -- | 1,933 (34 int. fail) | ~15.1 | 0 | 5 | A |
207 + | 2026-05-02 (Run 19) | ~81,384 | -- | 1,923 (0 fail) | ~23.6 | 0 | 2 | A |
200 208
201 209 ---
202 210