Skip to main content

max / makenotwork

v0.5.16: Fix SyncKit device registration, doc fuzz cleanup, code fuzz fixes SyncKit fixes: - Return full Device response from register_device (was missing app_id, user_id, platform, last_seen_at — caused client deserialization failure) - Redirect app checkout success to /checkout/complete (public page) instead of /dashboard (requires auth, caused 401 for app-initiated flows) - Add /checkout/complete lightweight public page Doc fuzz (15 precision edits to public docs): - Remove vague language (hedging, passive voice, undefined terms) - Fix terminology: "creator tier subscription" not "membership" in tiers.md - Align earn-back date to "no later than January 1, 2027" - Specify platforms for OG player embeds, Stripe fee ranges - Clarify guarantees continuity timeline Deduplication: - MT CONTRIBUTING: replace OAuth flow duplicate with architecture.md cross-ref - Add SyncKit canonical source cross-ref in ecosystem.md - Add test count cross-ref in README.md Cleanup: - Delete 10 stale one-time audit snapshots (code_review, code_flaws, flaws, codesize_audit, concurrency_audit, payload_audit, todo-creator-trust-audit) - Migrate 5 open trust audit items to server/docs/todo.md - Move synckit_pricing.md to internal/business/ Code fuzz fixes (Run 28): - SyncKit JWT invalidation on password change (migration 112) - SSH key lookup checks suspension - Guest checkout + cart checkout transaction safety - Cents encode_by_ref overflow, S3 key NULL handling - Owner authorization on 9 item update functions - Health dashboard deadlock fix, scheduler improvements - Rate limiting, webhook idempotency, broadcast validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-12 16:20 UTC
Commit: 510fd4d65e7e0e3efe12620844aa11eec30501d5
Parent: c2453c8
125 files changed, +4187 insertions, -2010 deletions
@@ -860,7 +860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
860 860 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
861 861 dependencies = [
862 862 "libc",
863 - "windows-sys 0.52.0",
863 + "windows-sys 0.59.0",
864 864 ]
865 865
866 866 [[package]]
@@ -2891,7 +2891,7 @@ dependencies = [
2891 2891 "errno",
2892 2892 "libc",
2893 2893 "linux-raw-sys",
2894 - "windows-sys 0.52.0",
2894 + "windows-sys 0.59.0",
2895 2895 ]
2896 2896
2897 2897 [[package]]
@@ -2920,9 +2920,9 @@ dependencies = [
2920 2920
2921 2921 [[package]]
2922 2922 name = "rustls-webpki"
2923 - version = "0.103.10"
2923 + version = "0.103.13"
2924 2924 source = "registry+https://github.com/rust-lang/crates.io-index"
2925 - checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
2925 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
2926 2926 dependencies = [
2927 2927 "ring",
2928 2928 "rustls-pki-types",
@@ -1,118 +0,0 @@
1 - # mnw-cli — Code Review
2 -
3 - **Date:** 2026-04-12
4 - **Version:** 0.1.0 (pre-deployment)
5 - **Reviewer:** Claude (Opus 4.6)
6 - **Scope:** Full codebase review — all Rust source, deploy config, tests, docs
7 -
8 - ## Summary
9 -
10 - mnw-cli is an SSH-first CLI server (~6,870 LOC Rust, ~8,600 total) for the MNW creator platform. SSH-based auth eliminates API key management. Users connect via standard SSH clients for an interactive TUI, non-interactive commands, SFTP file uploads, or git operations. Built on russh + ratatui + reqwest. 46 unit tests, 0 clippy warnings.
11 -
12 - **Overall: A** — clean architecture, strong security posture. All original findings resolved: UTF-8 panic fixed, price parsing fixed, 18 clippy warnings fixed, tui/mod.rs split, 36 tests added, exit status added, confirmation dialogs for all destructive operations.
13 -
14 - ---
15 -
16 - ## Findings
17 -
18 - ### [MEDIUM] `tui/mod.rs` at 2,211 lines — well over 500-line guideline
19 -
20 - This file contains the event loop, all 9 screen input handlers, 9 data loaders, the multi-step publish flow, and utility functions. All branching logic. Should be split into at minimum:
21 - - `tui/input.rs` — screen-specific input handlers (~800 lines)
22 - - `tui/loading.rs` — data loading functions (~300 lines)
23 - - `tui/publish.rs` — the publish flow (~100 lines)
24 -
25 - ### [MEDIUM] `truncate()` in commands.rs panics on multi-byte UTF-8
26 -
27 - ```rust
28 - fn truncate(s: &str, max_len: usize) -> &str {
29 - if s.len() <= max_len { s } else { &s[..max_len] }
30 - }
31 - ```
32 -
33 - Slicing at a byte offset panics if `max_len` falls within a multi-byte character. Any project/blog title with accented letters, CJK, or emoji could trigger this. Fix: use `s.floor_char_boundary(max_len)` (stable since Rust 1.82).
34 -
35 - ### [MEDIUM] 18 clippy warnings
36 -
37 - Includes: collapsible if-statements (7), `&PathBuf` instead of `&Path` (2), useless `format!` (1), useless `.into()` (1), too many function arguments (2), clamp-like pattern (1), trim-before-split_whitespace (1), unused struct fields (2). Should be cleaned up before deployment.
38 -
39 - ### [MEDIUM] `parse_price("5.5")` returns 505 cents ($5.05), not 550 ($5.50)
40 -
41 - The function takes at most 2 chars from the cents portion: `cents.get(..2)` on `"5"` yields `5`, so `5 * 100 + 5 = 505`. A user typing "5.5" almost certainly means $5.50. Fix: pad single-digit cents with a trailing zero.
42 -
43 - ### [LOW] `russh` 0.58.1 was yanked
44 -
45 - Resolved by `cargo update` downgrading to 0.58.0. Version 0.60.0 is available but is a breaking change. Remaining advisories (rsa, rand) are transitive with no direct fix.
46 -
47 - ### [LOW] No exit status on non-interactive commands
48 -
49 - `exec_request` in handler.rs spawns command execution but never sends `exit_status_request` to the SSH channel. SSH clients can't distinguish success from failure. The git proxy path correctly sends exit status, but the command path does not.
50 -
51 - ### [LOW] No confirmation for destructive operations
52 -
53 - Delete item, delete blog post, delete promo code, and revoke license key all execute immediately on a single keypress (`d` or `x`). No confirmation dialog. One accidental keypress permanently deletes.
54 -
55 - ### [LOW] `api.rs` at 897 lines
56 -
57 - Contains 18 data types and 23 API methods. The types (structs + enums) are flat data definitions (~400 lines, exempt from the 500-line rule), so the branching logic is within limits. But if more endpoints are added, consider splitting types into `api/types.rs`.
58 -
59 - ### [LOW] Missing unit tests for pure functions
60 -
61 - `staging.rs` has 6 testable pure functions (`sanitize_filename`, `classify_extension`, `derive_title`, `is_allowed_extension`, `format_bytes`, `cleanup_stale` logic). `format.rs` has 5 pure formatters. `commands.rs` has `truncate` and price parsing. None have tests. The git proxy has 10 tests — the same pattern should be applied to these modules.
62 -
63 - ### [INFO] `i64` to `u64` casts in render code
64 -
65 - `storage_used_bytes as u64` and `max_storage_bytes as u64` in upload.rs, settings.rs, item.rs, and analytics.rs. If the server returns negative values (bug), these wrap silently. Extremely unlikely but could use `.try_into().unwrap_or(0)`.
66 -
67 - ### [INFO] Non-interactive promo creation only supports percentage discounts
68 -
69 - Both CLI (`promo create CODE PCT`) and TUI always send `"percentage"` discount type. The API supports fixed-amount discounts but neither interface exposes it. Minor — percentage is the common case.
70 -
71 - ---
72 -
73 - ## Strengths
74 -
75 - - **SSH-first auth is elegant.** Eliminates API key management entirely. Users authenticate with their existing SSH keys. Public key fingerprint looked up against MNW server.
76 - - **Clean per-connection isolation.** No shared mutable state between connections. Each connection gets its own `MnwHandler` with independent API client, staging handle, and TUI state.
77 - - **SFTP implementation is thorough.** Tier checking, extension validation, per-user quota enforcement (checked on both `open` and `write`), path traversal prevention, virtual filesystem abstraction.
78 - - **Git proxy is well-tested.** 10 unit tests covering command parsing and path traversal prevention. Subprocess management with proper stdin/stdout/stderr piping.
79 - - **Security hardening.** systemd service has ProtectSystem=strict, PrivateTmp, NoNewPrivileges, etc. Path traversal blocked in both SFTP and git. Suspended users rejected at auth.
80 - - **Prior audit findings all resolved.** The 5 issues from cleanup.md (memory leak, silent errors, silent deletions) are all fixed in the current code.
81 - - **Good documentation.** README, architecture.md, todo.md, and cleanup.md all present and current.
82 -
83 - ## Security Checklist
84 -
85 - | Check | Status |
86 - |-------|--------|
87 - | Auth bypass | Pass — SSH public key auth, fingerprint verified against MNW API |
88 - | Path traversal (SFTP) | Pass — filename sanitization, no directory creation |
89 - | Path traversal (git) | Pass — repo path parsed, `..` rejected, nested paths rejected |
90 - | Suspended user access | Pass — rejected at `auth_publickey_offered` |
91 - | Tier enforcement (SFTP) | Pass — Basic tier rejected at `open`, quota checked per write |
92 - | Service token exposure | Pass — loaded from env, not logged |
93 - | Command injection (git) | Pass — command and repo path parsed separately, no shell interpolation |
94 -
95 - ## Metrics
96 -
97 - | Metric | Value |
98 - |--------|-------|
99 - | Rust source LOC | ~6,870 |
100 - | Total LOC (all files) | ~8,600 |
101 - | Source files | 24 |
102 - | Unit tests | 46 |
103 - | Integration test assertions | ~61 (bash script) |
104 - | Clippy warnings | 0 |
105 - | Dependency advisories | 0 yanked, 3 allowed transitive |
106 - | TUI screens | 9 |
107 - | Non-interactive commands | 8 |
108 - | API client methods | 23 |
109 -
110 - ## Action Items
111 -
112 - 1. ~~**[MEDIUM]** Split `tui/mod.rs` (2,211 lines) into input handlers, data loaders, and publish flow~~ — Done. mod.rs (767), input.rs (1,198), loading.rs (265).
113 - 2. ~~**[MEDIUM]** Fix `truncate()` UTF-8 panic — use `floor_char_boundary`~~ — Done.
114 - 3. ~~**[MEDIUM]** Fix all 18 clippy warnings~~ — Done. 0 warnings.
115 - 4. ~~**[MEDIUM]** Fix `parse_price("5.5")` — pad single-digit cents~~ — Done. Single-digit cents multiplied by 10.
116 - 5. ~~**[LOW]** Send exit status on non-interactive command completion~~ — Done. Added `exit_status_request(0)` + `eof` before close.
117 - 6. ~~**[LOW]** Add confirmation dialogs for destructive operations (delete, revoke)~~ — Done. Double-press confirmation on all 4 destructive actions (delete item, blog post, promo code; revoke license key).
118 - 7. ~~**[LOW]** Add unit tests for pure functions in staging.rs, format.rs, commands.rs~~ — Done. 36 new tests (10 → 46 total).
@@ -54,13 +54,7 @@ Library crates (`mt-core`, `mt-db`) contain no web framework types. Routes and t
54 54
55 55 ## MNW OAuth Integration
56 56
57 - Multithreaded delegates all authentication to MNW via OAuth 2.0 with PKCE. There are no local passwords or signup forms.
58 -
59 - **Flow:**
60 - 1. `/auth/login` generates PKCE verifier + challenge, stores verifier in session, redirects to MNW `/oauth/authorize`
61 - 2. MNW authenticates the user, redirects back to `/auth/callback` with an authorization code
62 - 3. Callback exchanges code for token (with PKCE verifier), fetches userinfo, upserts local user via `ON CONFLICT` upsert
63 - 4. Session is created with `user_id`, `username`, `display_name`; session ID is cycled
57 + Multithreaded delegates all authentication to MNW via OAuth 2.0 with PKCE. There are no local passwords or signup forms. See [architecture.md § Authentication](docs/architecture.md#4-authentication) for the full flow (PKCE parameters, state nonce validation, retry behavior, session cycling).
64 58
65 59 **Extractors:**
66 60 - `MaybeUser(Option<SessionUser>)` — optional auth, infallible (never rejects)
@@ -131,8 +131,8 @@ dependencies = [
131 131 "aws-sdk-ssooidc",
132 132 "aws-sdk-sts",
133 133 "aws-smithy-async",
134 - "aws-smithy-http 0.63.6",
135 - "aws-smithy-json 0.62.5",
134 + "aws-smithy-http",
135 + "aws-smithy-json",
136 136 "aws-smithy-runtime",
137 137 "aws-smithy-runtime-api",
138 138 "aws-smithy-types",
@@ -141,7 +141,7 @@ dependencies = [
141 141 "fastrand",
142 142 "hex",
143 143 "http 1.4.0",
144 - "sha1",
144 + "sha1 0.10.6",
145 145 "time",
146 146 "tokio",
147 147 "tracing",
@@ -185,15 +185,15 @@ dependencies = [
185 185
186 186 [[package]]
187 187 name = "aws-runtime"
188 - version = "1.7.2"
188 + version = "1.7.3"
189 189 source = "registry+https://github.com/rust-lang/crates.io-index"
190 - checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17"
190 + checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864"
191 191 dependencies = [
192 192 "aws-credential-types",
193 193 "aws-sigv4",
194 194 "aws-smithy-async",
195 195 "aws-smithy-eventstream",
196 - "aws-smithy-http 0.63.6",
196 + "aws-smithy-http",
197 197 "aws-smithy-runtime",
198 198 "aws-smithy-runtime-api",
199 199 "aws-smithy-types",
@@ -213,9 +213,9 @@ dependencies = [
213 213
214 214 [[package]]
215 215 name = "aws-sdk-s3"
216 - version = "1.119.0"
216 + version = "1.132.0"
217 217 source = "registry+https://github.com/rust-lang/crates.io-index"
218 - checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c"
218 + checksum = "5575840a3a6b11f6011463ebe359320dfe5b67babb5e9b06fed6ddf809a9ab40"
219 219 dependencies = [
220 220 "aws-credential-types",
221 221 "aws-runtime",
@@ -223,8 +223,9 @@ dependencies = [
223 223 "aws-smithy-async",
224 224 "aws-smithy-checksums",
225 225 "aws-smithy-eventstream",
226 - "aws-smithy-http 0.62.6",
227 - "aws-smithy-json 0.61.9",
226 + "aws-smithy-http",
227 + "aws-smithy-json",
228 + "aws-smithy-observability",
228 229 "aws-smithy-runtime",
229 230 "aws-smithy-runtime-api",
230 231 "aws-smithy-types",
@@ -233,14 +234,14 @@ dependencies = [
233 234 "bytes",
234 235 "fastrand",
235 236 "hex",
236 - "hmac",
237 + "hmac 0.13.0",
237 238 "http 0.2.12",
238 239 "http 1.4.0",
239 - "http-body 0.4.6",
240 + "http-body 1.0.1",
240 241 "lru",
241 242 "percent-encoding",
242 243 "regex-lite",
243 - "sha2",
244 + "sha2 0.11.0",
244 245 "tracing",
245 246 "url",
246 247 ]
@@ -254,8 +255,8 @@ dependencies = [
254 255 "aws-credential-types",
255 256 "aws-runtime",
256 257 "aws-smithy-async",
257 - "aws-smithy-http 0.63.6",
258 - "aws-smithy-json 0.62.5",
258 + "aws-smithy-http",
259 + "aws-smithy-json",
259 260 "aws-smithy-observability",
260 261 "aws-smithy-runtime",
261 262 "aws-smithy-runtime-api",
@@ -278,8 +279,8 @@ dependencies = [
278 279 "aws-credential-types",
279 280 "aws-runtime",
280 281 "aws-smithy-async",
281 - "aws-smithy-http 0.63.6",
282 - "aws-smithy-json 0.62.5",
282 + "aws-smithy-http",
283 + "aws-smithy-json",
283 284 "aws-smithy-observability",
284 285 "aws-smithy-runtime",
285 286 "aws-smithy-runtime-api",
@@ -302,8 +303,8 @@ dependencies = [
302 303 "aws-credential-types",
303 304 "aws-runtime",
304 305 "aws-smithy-async",
305 - "aws-smithy-http 0.63.6",
306 - "aws-smithy-json 0.62.5",
306 + "aws-smithy-http",
307 + "aws-smithy-json",
307 308 "aws-smithy-observability",
308 309 "aws-smithy-query",
309 310 "aws-smithy-runtime",
@@ -320,26 +321,26 @@ dependencies = [
320 321
321 322 [[package]]
322 323 name = "aws-sigv4"
323 - version = "1.4.2"
324 + version = "1.4.3"
324 325 source = "registry+https://github.com/rust-lang/crates.io-index"
325 - checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4"
326 + checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a"
326 327 dependencies = [
327 328 "aws-credential-types",
328 329 "aws-smithy-eventstream",
329 - "aws-smithy-http 0.63.6",
330 + "aws-smithy-http",
330 331 "aws-smithy-runtime-api",
331 332 "aws-smithy-types",
332 333 "bytes",
333 334 "crypto-bigint 0.5.5",
334 335 "form_urlencoded",
335 336 "hex",
336 - "hmac",
337 + "hmac 0.13.0",
337 338 "http 0.2.12",
338 339 "http 1.4.0",
339 340 "p256",
340 341 "percent-encoding",
341 342 "ring",
342 - "sha2",
343 + "sha2 0.11.0",
343 344 "subtle",
344 345 "time",
345 346 "tracing",
@@ -359,21 +360,22 @@ dependencies = [
359 360
360 361 [[package]]
361 362 name = "aws-smithy-checksums"
362 - version = "0.63.12"
363 + version = "0.64.7"
363 364 source = "registry+https://github.com/rust-lang/crates.io-index"
364 - checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae"
365 + checksum = "10efbbcec1e044b81600e2fc562a391951d291152d95b482d5b7e7132299d762"
365 366 dependencies = [
366 - "aws-smithy-http 0.62.6",
367 + "aws-smithy-http",
367 368 "aws-smithy-types",
368 369 "bytes",
369 370 "crc-fast",
370 371 "hex",
371 - "http 0.2.12",
372 - "http-body 0.4.6",
373 - "md-5",
372 + "http 1.4.0",
373 + "http-body 1.0.1",
374 + "http-body-util",
375 + "md-5 0.11.0",
374 376 "pin-project-lite",
375 - "sha1",
376 - "sha2",
377 + "sha1 0.11.0",
378 + "sha2 0.11.0",
377 379 "tracing",
378 380 ]
379 381
@@ -390,32 +392,11 @@ dependencies = [
390 392
391 393 [[package]]
392 394 name = "aws-smithy-http"
393 - version = "0.62.6"
394 - source = "registry+https://github.com/rust-lang/crates.io-index"
395 - checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b"
396 - dependencies = [
397 - "aws-smithy-eventstream",
398 - "aws-smithy-runtime-api",
399 - "aws-smithy-types",
400 - "bytes",
401 - "bytes-utils",
402 - "futures-core",
403 - "futures-util",
404 - "http 0.2.12",
405 - "http 1.4.0",
406 - "http-body 0.4.6",
407 - "percent-encoding",
408 - "pin-project-lite",
409 - "pin-utils",
410 - "tracing",
411 - ]
412 -
413 - [[package]]
414 - name = "aws-smithy-http"
415 395 version = "0.63.6"
416 396 source = "registry+https://github.com/rust-lang/crates.io-index"
417 397 checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
418 398 dependencies = [
399 + "aws-smithy-eventstream",
419 400 "aws-smithy-runtime-api",
420 401 "aws-smithy-types",
421 402 "bytes",
@@ -463,15 +444,6 @@ dependencies = [
463 444
464 445 [[package]]
465 446 name = "aws-smithy-json"
466 - version = "0.61.9"
467 - source = "registry+https://github.com/rust-lang/crates.io-index"
468 - checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551"
469 - dependencies = [
470 - "aws-smithy-types",
471 - ]
472 -
473 - [[package]]
474 - name = "aws-smithy-json"
475 447 version = "0.62.5"
476 448 source = "registry+https://github.com/rust-lang/crates.io-index"
477 449 checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a"
@@ -500,12 +472,12 @@ dependencies = [
500 472
501 473 [[package]]
502 474 name = "aws-smithy-runtime"
503 - version = "1.10.3"
475 + version = "1.11.1"
504 476 source = "registry+https://github.com/rust-lang/crates.io-index"
505 - checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110"
477 + checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5"
506 478 dependencies = [
507 479 "aws-smithy-async",
508 - "aws-smithy-http 0.63.6",
480 + "aws-smithy-http",
509 481 "aws-smithy-http-client",
510 482 "aws-smithy-observability",
511 483 "aws-smithy-runtime-api",
@@ -525,11 +497,12 @@ dependencies = [
525 497
526 498 [[package]]
527 499 name = "aws-smithy-runtime-api"
528 - version = "1.11.6"
500 + version = "1.12.0"
529 501 source = "registry+https://github.com/rust-lang/crates.io-index"
530 - checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6"
502 + checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e"
531 503 dependencies = [
532 504 "aws-smithy-async",
505 + "aws-smithy-runtime-api-macros",
533 506 "aws-smithy-types",
534 507 "bytes",
535 508 "http 0.2.12",
@@ -541,6 +514,17 @@ dependencies = [
541 514 ]
542 515
543 516 [[package]]
517 + name = "aws-smithy-runtime-api-macros"
518 + version = "1.0.0"
519 + source = "registry+https://github.com/rust-lang/crates.io-index"
520 + checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7"
521 + dependencies = [
522 + "proc-macro2",
523 + "quote",
524 + "syn",
525 + ]
526 +
527 + [[package]]
544 528 name = "aws-smithy-types"
545 529 version = "1.4.7"
546 530 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -577,9 +561,9 @@ dependencies = [
577 561
578 562 [[package]]
579 563 name = "aws-types"
580 - version = "1.3.14"
564 + version = "1.3.15"
581 565 source = "registry+https://github.com/rust-lang/crates.io-index"
582 - checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9"
566 + checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac"
583 567 dependencies = [
584 568 "aws-credential-types",
585 569 "aws-smithy-async",
@@ -616,7 +600,7 @@ dependencies = [
616 600 "serde_json",
617 601 "serde_path_to_error",
618 602 "serde_urlencoded",
619 - "sha1",
603 + "sha1 0.10.6",
620 604 "sync_wrapper",
621 605 "tokio",
622 606 "tokio-tungstenite",
@@ -701,6 +685,15 @@ dependencies = [
701 685 ]
702 686
703 687 [[package]]
688 + name = "block-buffer"
689 + version = "0.12.0"
690 + source = "registry+https://github.com/rust-lang/crates.io-index"
691 + checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
692 + dependencies = [
693 + "hybrid-array",
694 + ]
695 +
696 + [[package]]
704 697 name = "bumpalo"
705 698 version = "3.20.2"
706 699 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -776,6 +769,12 @@ dependencies = [
776 769 ]
777 770
778 771 [[package]]
772 + name = "cmov"
773 + version = "0.5.3"
774 + source = "registry+https://github.com/rust-lang/crates.io-index"
775 + checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
776 +
777 + [[package]]
779 778 name = "concurrent-queue"
780 779 version = "2.5.0"
781 780 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -791,6 +790,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
791 790 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
792 791
793 792 [[package]]
793 + name = "const-oid"
794 + version = "0.10.2"
795 + source = "registry+https://github.com/rust-lang/crates.io-index"
796 + checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
797 +
798 + [[package]]
794 799 name = "cookie"
795 800 version = "0.18.1"
796 801 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -827,10 +832,19 @@ dependencies = [
827 832 ]
828 833
829 834 [[package]]
835 + name = "cpufeatures"
836 + version = "0.3.0"
837 + source = "registry+https://github.com/rust-lang/crates.io-index"
838 + checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
839 + dependencies = [
840 + "libc",
841 + ]
842 +
843 + [[package]]
830 844 name = "crc"
831 - version = "3.4.0"
845 + version = "3.3.0"
832 846 source = "registry+https://github.com/rust-lang/crates.io-index"
833 - checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
847 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
834 848 dependencies = [
835 849 "crc-catalog",
836 850 ]
@@ -843,15 +857,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
843 857
844 858 [[package]]
845 859 name = "crc-fast"
846 - version = "1.6.0"
860 + version = "1.9.0"
847 861 source = "registry+https://github.com/rust-lang/crates.io-index"
848 - checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3"
862 + checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d"
849 863 dependencies = [
850 864 "crc",
851 - "digest",
852 - "rand 0.9.2",
853 - "regex",
865 + "digest 0.10.7",
854 866 "rustversion",
867 + "spin 0.10.0",
855 868 ]
856 869
857 870 [[package]]
@@ -911,6 +924,15 @@ dependencies = [
911 924 ]
912 925
913 926 [[package]]
927 + name = "crypto-common"
928 + version = "0.2.1"
929 + source = "registry+https://github.com/rust-lang/crates.io-index"
930 + checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
931 + dependencies = [
932 + "hybrid-array",
933 + ]
934 +
935 + [[package]]
914 936 name = "cssparser"
915 937 version = "0.35.0"
916 938 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -934,6 +956,15 @@ dependencies = [
934 956 ]
935 957
936 958 [[package]]
959 + name = "ctutils"
960 + version = "0.4.2"
961 + source = "registry+https://github.com/rust-lang/crates.io-index"
962 + checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
963 + dependencies = [
964 + "cmov",
965 + ]
966 +
967 + [[package]]
937 968 name = "dashmap"
938 969 version = "6.1.0"
939 970 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -959,7 +990,7 @@ version = "0.6.1"
959 990 source = "registry+https://github.com/rust-lang/crates.io-index"
960 991 checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
961 992 dependencies = [
962 - "const-oid",
993 + "const-oid 0.9.6",
963 994 "zeroize",
964 995 ]
965 996
@@ -969,7 +1000,7 @@ version = "0.7.10"
969 1000 source = "registry+https://github.com/rust-lang/crates.io-index"
970 1001 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
971 1002 dependencies = [
972 - "const-oid",
1003 + "const-oid 0.9.6",
973 1004 "pem-rfc7468",
974 1005 "zeroize",
975 1006 ]
@@ -990,13 +1021,25 @@ version = "0.10.7"
990 1021 source = "registry+https://github.com/rust-lang/crates.io-index"
991 1022 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
992 1023 dependencies = [
993 - "block-buffer",
994 - "const-oid",
995 - "crypto-common",
1024 + "block-buffer 0.10.4",
1025 + "const-oid 0.9.6",
1026 + "crypto-common 0.1.7",
996 1027 "subtle",
997 1028 ]
998 1029
999 1030 [[package]]
1031 + name = "digest"
1032 + version = "0.11.3"
1033 + source = "registry+https://github.com/rust-lang/crates.io-index"
1034 + checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
1035 + dependencies = [
1036 + "block-buffer 0.12.0",
1037 + "const-oid 0.10.2",
1038 + "crypto-common 0.2.1",
1039 + "ctutils",
1040 + ]
1041 +
1042 + [[package]]
1000 1043 name = "displaydoc"
1001 1044 version = "0.2.5"
1002 1045 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1075,7 +1118,7 @@ dependencies = [
1075 1118 "base16ct",
1076 1119 "crypto-bigint 0.4.9",
1077 1120 "der 0.6.1",
1078 - "digest",
1121 + "digest 0.10.7",
1079 1122 "ff",
1080 1123 "generic-array",
1081 1124 "group",
@@ -1163,7 +1206,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
1163 1206 dependencies = [
1164 1207 "futures-core",
1165 1208 "futures-sink",
1166 - "spin",
1209 + "spin 0.9.8",
1167 1210 ]
1168 1211
1169 1212 [[package]]
@@ -1179,6 +1222,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1179 1222 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
1180 1223
1181 1224 [[package]]
1225 + name = "foldhash"
1226 + version = "0.2.0"
1227 + source = "registry+https://github.com/rust-lang/crates.io-index"
1228 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
1229 +
1230 + [[package]]
1182 1231 name = "form_urlencoded"
1183 1232 version = "1.2.2"
1184 1233 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1461,7 +1510,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1461 1510 dependencies = [
1462 1511 "allocator-api2",
1463 1512 "equivalent",
1464 - "foldhash",
1513 + "foldhash 0.1.5",
1465 1514 ]
1466 1515
1467 1516 [[package]]
@@ -1469,6 +1518,11 @@ name = "hashbrown"
1469 1518 version = "0.16.1"
1470 1519 source = "registry+https://github.com/rust-lang/crates.io-index"
1471 1520 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1521 + dependencies = [
1522 + "allocator-api2",
1523 + "equivalent",
1524 + "foldhash 0.2.0",
1525 + ]
1472 1526
1473 1527 [[package]]
1474 1528 name = "hashlink"
@@ -1497,7 +1551,7 @@ version = "0.12.4"
1497 1551 source = "registry+https://github.com/rust-lang/crates.io-index"
1498 1552 checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
1499 1553 dependencies = [
1500 - "hmac",
1554 + "hmac 0.12.1",
1501 1555 ]
1502 1556
1503 1557 [[package]]
@@ -1506,7 +1560,16 @@ version = "0.12.1"
1506 1560 source = "registry+https://github.com/rust-lang/crates.io-index"
1507 1561 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
1508 1562 dependencies = [
1509 - "digest",
1563 + "digest 0.10.7",
1564 + ]
1565 +
1566 + [[package]]
1567 + name = "hmac"
1568 + version = "0.13.0"
1569 + source = "registry+https://github.com/rust-lang/crates.io-index"
1570 + checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
1571 + dependencies = [
Lines truncated
@@ -1,129 +0,0 @@
1 - # Multithreaded — Code Review
2 -
3 - **Date:** 2026-04-12
4 - **Version:** 0.3.2
5 - **Reviewer:** Claude (Opus 4.6)
6 - **Scope:** Full codebase review — all Rust source, SQL migrations, templates, CSS, JS, deploy config, tests
7 -
8 - ## Summary
9 -
10 - Multithreaded is a forum application (~8,800 LOC Rust, ~20,800 total) built on Axum/PostgreSQL with MNW OAuth integration. 3-crate workspace (mt-core, mt-db, main app). 21 migrations, 21 HTML templates, 510 lines JS, 1,761 lines CSS. 225+ tests (35 unit + 190 integration). 0 clippy warnings. Security posture is strong. Code is clean and well-organized.
11 -
12 - **Overall: A** — consistent with the standing audit grade. No new vulnerabilities found. Several minor structural items noted below.
13 -
14 - ---
15 -
16 - ## Findings
17 -
18 - ### [MEDIUM] Dependency advisories — 4 active
19 -
20 - `cargo audit` reports 4 vulnerabilities:
21 -
22 - | Crate | Advisory | Severity | Fix |
23 - |-------|----------|----------|-----|
24 - | aws-lc-sys 0.38.0 | RUSTSEC-2026-0044 | HIGH | `cargo update -p aws-lc-sys` (>=0.39.0) |
25 - | aws-lc-sys 0.38.0 | RUSTSEC-2026-0048 | HIGH 7.4 | Same |
26 - | rustls-webpki 0.103.9 | RUSTSEC-2026-0049 | — | `cargo update -p rustls-webpki` (>=0.103.10) |
27 - | rand 0.8.5 + 0.9.2 | RUSTSEC-2026-0097 | — | Unsoundness with custom loggers (not applicable here, but upgrade when deps allow) |
28 -
29 - Additionally 3 allowed warnings (rsa, lru — transitive, no direct fix available).
30 -
31 - aws-lc-sys and rustls-webpki are straightforward `cargo update` fixes. The rand advisory (RUSTSEC-2026-0097) affects `rand::rng()` with custom loggers — MT doesn't use custom loggers, so no practical impact, but both 0.8.5 (direct dep) and 0.9.2 (transitive via axum/governor) are flagged. Consider bumping the direct `rand` dep from 0.8 to 0.9 when convenient.
32 -
33 - ### [MEDIUM] Three route files exceed 500-line guideline
34 -
35 - Per project conventions, files with >500 lines of branching logic should be split.
36 -
37 - | File | Lines | Content |
38 - |------|-------|---------|
39 - | `src/routes/mod.rs` | 578 | Route tree, form types, 15+ helper functions |
40 - | `src/routes/forum/views.rs` | 628 | 11 view handlers |
41 - | `src/routes/forum/actions.rs` | 600 | 10 action handlers |
42 -
43 - `mod.rs` could split helpers into `routes/helpers.rs` (validation, permission checks, markdown rendering) and keep forms + route tree in `mod.rs`. The forum files could split by page area (thread views vs. listing views, thread actions vs. post actions).
44 -
45 - `queries.rs` (1,503) and `mutations.rs` (879) are exempt — flat lists of SQL query functions with no branching logic.
46 -
47 - ### [LOW] Pagination partial not used consistently
48 -
49 - `templates/pages/forum_directory.html` (lines 40-49) inlines pagination markup instead of using `{% include "partials/pagination.html" %}`. Only `thread.html` uses the partial. If the pagination design changes, it needs updating in two places.
50 -
51 - ### [LOW] CSS: `.form-inline-row` has contradictory display properties
52 -
53 - ```css
54 - .form-inline-row { display: inline; gap: 0.25rem; flex-direction: row; align-items: center; }
55 - ```
56 -
57 - `gap`, `flex-direction`, and `align-items` are flex/grid properties — they have no effect with `display: inline`. Used on 2 admin forms. Should be `display: inline-flex`.
58 -
59 - ### [LOW] No `Cache-Control` on HTML responses
60 -
61 - Static files get caching from tower-http `ServeDir`. Image proxy sets `max-age=31536000`. But regular HTML pages have no explicit cache control headers. Adding `private, no-cache` for authenticated pages prevents stale content after logout/session change.
62 -
63 - ### [INFO] Test harness duplication
64 -
65 - `tests/harness/mod.rs`: `new()` and `new_with_admin()` share ~40 lines of identical setup code. `tests/harness/client.rs`: `send_raw` and `send_raw_with_token` duplicate ~60 lines. Not a correctness issue, but a maintenance burden.
66 -
67 - ### [INFO] `MaybeUser` silently swallows session errors
68 -
69 - In `auth.rs`, the `MaybeUser` extractor returns `MaybeUser(None)` on any session read error. This is intentionally infallible, but session store failures silently deauthenticate users without logging at the extraction point. A `tracing::warn!` on the error path would help debugging.
70 -
71 - ### [INFO] No index on `posts.removed_at`
72 -
73 - The moderation page queries removed posts and thread views filter by removal status. At scale, a partial index `WHERE removed_at IS NOT NULL` on `posts` would help. Current data volumes don't require it.
74 -
75 - ---
76 -
77 - ## Strengths
78 -
79 - - **Security is defense-in-depth.** CSRF (constant-time comparison, auto-injected), SSRF (comprehensive private IP blocking on link previews), XSS (docengine sanitization + Askama autoescaping), SQL injection (all parameterized), rate limiting (per-IP + per-user), HMAC internal API with replay protection, EXIF stripping on uploads, CSP headers. 11 dedicated XSS tests.
80 - - **Immutable post model.** Posts cannot be edited or deleted by users — only corrected via footnotes or mod-removed (content preserved). Philosophically consistent and well-executed.
81 - - **Test infrastructure is excellent.** Per-test database isolation. Cookie-aware HTTP client with auto CSRF extraction. Full Axum app in-process. 22 workflow test files covering every feature area.
82 - - **Clean 3-crate separation.** mt-core has zero deps beyond chrono. mt-db has no web framework deps. Main crate handles all HTTP concerns. No circular dependencies.
83 - - **Production hardening.** Systemd service with 18 security directives (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies, etc.). Memory cap 512M. Graceful shutdown.
84 - - **Quote verification.** SHA-256 hash of quoted text verified server-side before rendering attribution. Prevents fabricated quotes.
85 - - **Observability.** 86+ `#[instrument(skip_all)]` annotations. Structured logging via tracing with env filter.
86 -
87 - ## Security Checklist
88 -
89 - | Check | Status |
90 - |-------|--------|
91 - | SQL injection | Pass — all queries parameterized via SQLx |
92 - | XSS | Pass — docengine sanitization + Askama autoescaping + CSP |
93 - | CSRF | Pass — synchronizer token, constant-time comparison, all mutating routes |
94 - | SSRF | Pass — link preview validates IPs (IPv4+IPv6 private ranges blocked) |
95 - | Auth bypass | Pass — fail-closed access checks, session cycling on login |
96 - | IDOR | Pass — all resource access checks use authenticated user context |
97 - | Rate limiting | Pass — per-IP (tower-governor) + per-user (15 posts/60s) + upload rate (20/hr) |
98 - | Secrets in source | Pass — env-based config, no secrets committed (env templates have placeholders) |
99 - | Dependency advisories | Fail — 4 active (2 HIGH via aws-lc-sys, fixable with cargo update) |
100 -
101 - ## Metrics
102 -
103 - | Metric | Value |
104 - |--------|-------|
105 - | Rust source LOC | ~8,800 |
106 - | Total LOC (all files) | ~20,800 |
107 - | Rust source files | 39 |
108 - | Test files | 24 (harness + workflows) |
109 - | Test count | 225+ (35 unit + 190 integration) |
110 - | Tests/KLOC | ~26 |
111 - | Clippy warnings | 0 |
112 - | Migrations | 21 |
113 - | HTML templates | 21 |
114 - | Query functions | 55+ |
115 - | Mutation functions | 30+ |
116 - | Dependencies (direct) | 28 |
117 - | Audit advisories | 4 (2 HIGH, 2 low/info) |
118 -
119 - ## Action Items
120 -
121 - 1. ~~**[MEDIUM]** Run `cargo update -p aws-lc-sys -p rustls-webpki` to fix 3 of 4 advisories~~ — Done. aws-lc-sys 0.39.1, rustls-webpki 0.103.11.
122 - 2. ~~**[MEDIUM]** Split `routes/mod.rs` helpers into `routes/helpers.rs`~~ — Done. mod.rs 281, helpers.rs 315.
123 - 3. ~~**[LOW]** Fix `.form-inline-row` CSS to `display: inline-flex`~~ — Done.
124 - 4. ~~**[LOW]** Use pagination partial in `forum_directory.html`~~ — Done.
125 - 5. ~~**[LOW]** Add `Cache-Control: private, no-cache` to HTML responses~~ — Done.
126 - 6. ~~**[MEDIUM]** Split `forum/views.rs` and `forum/actions.rs`~~ — Done. views.rs 424, thread.rs 224, posts.rs 443, actions.rs 172.
127 - 7. **[INFO]** Transitive dep advisories (rand, rsa, lru) — no fix available, monitor upstream.
128 - 8. **[INFO]** Add partial index on `posts.removed_at` when data volume warrants it.
129 - 9. **[INFO]** Add `tracing::warn!` to `MaybeUser` extractor on session read errors.
M pom/Cargo.lock +2 -2
@@ -2093,9 +2093,9 @@ dependencies = [
2093 2093
2094 2094 [[package]]
2095 2095 name = "rustls-webpki"
2096 - version = "0.103.9"
2096 + version = "0.103.13"
2097 2097 source = "registry+https://github.com/rust-lang/crates.io-index"
2098 - checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
2098 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
2099 2099 dependencies = [
2100 2100 "aws-lc-rs",
2101 2101 "ring",
M server/Cargo.lock +108 -101
@@ -4,11 +4,11 @@ version = 4
4 4
5 5 [[package]]
6 6 name = "addr2line"
7 - version = "0.25.1"
7 + version = "0.26.1"
8 8 source = "registry+https://github.com/rust-lang/crates.io-index"
9 - checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
9 + checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a"
10 10 dependencies = [
11 - "gimli",
11 + "gimli 0.33.0",
12 12 ]
13 13
14 14 [[package]]
@@ -46,7 +46,6 @@ version = "1.1.4"
46 46 source = "registry+https://github.com/rust-lang/crates.io-index"
47 47 checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
48 48 dependencies = [
49 - "log",
50 49 "memchr",
51 50 ]
52 51
@@ -1389,46 +1388,48 @@ dependencies = [
1389 1388
1390 1389 [[package]]
1391 1390 name = "cranelift-assembler-x64"
1392 - version = "0.127.4"
1391 + version = "0.130.2"
1393 1392 source = "registry+https://github.com/rust-lang/crates.io-index"
1394 - checksum = "d6abf69c884fde2d9d4cc232a585fb18f16af3ae04c634315c84ebe158ded92d"
1393 + checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9"
1395 1394 dependencies = [
1396 1395 "cranelift-assembler-x64-meta",
1397 1396 ]
1398 1397
1399 1398 [[package]]
1400 1399 name = "cranelift-assembler-x64-meta"
1401 - version = "0.127.4"
1400 + version = "0.130.2"
1402 1401 source = "registry+https://github.com/rust-lang/crates.io-index"
1403 - checksum = "263d31fcdf83a10267e8c38b53bc8f7688dfbc331267fd8fdf5b22e0dc47a55b"
1402 + checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a"
1404 1403 dependencies = [
1405 1404 "cranelift-srcgen",
1406 1405 ]
1407 1406
1408 1407 [[package]]
1409 1408 name = "cranelift-bforest"
1410 - version = "0.127.4"
1409 + version = "0.130.2"
1411 1410 source = "registry+https://github.com/rust-lang/crates.io-index"
1412 - checksum = "d459d5377c01c4472b71029caa2df41afaf47711676aa9b12d7414f15104637b"
1411 + checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d"
1413 1412 dependencies = [
1414 1413 "cranelift-entity",
1414 + "wasmtime-internal-core",
1415 1415 ]
1416 1416
1417 1417 [[package]]
1418 1418 name = "cranelift-bitset"
1419 - version = "0.127.4"
1419 + version = "0.130.2"
1420 1420 source = "registry+https://github.com/rust-lang/crates.io-index"
1421 - checksum = "8283088d5823ba7856ab8d531b7c3654b24984748f9fd99dcf3210701fd1d065"
1421 + checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c"
1422 1422 dependencies = [
1423 1423 "serde",
1424 1424 "serde_derive",
1425 + "wasmtime-internal-core",
1425 1426 ]
1426 1427
1427 1428 [[package]]
1428 1429 name = "cranelift-codegen"
1429 - version = "0.127.4"
1430 + version = "0.130.2"
1430 1431 source = "registry+https://github.com/rust-lang/crates.io-index"
1431 - checksum = "7d3138316d8dd341d725d6ab1598750545c76ad32892837fde558edd68a01b43"
1432 + checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878"
1432 1433 dependencies = [
1433 1434 "bumpalo",
1434 1435 "cranelift-assembler-x64",
@@ -1439,8 +1440,9 @@ dependencies = [
1439 1440 "cranelift-control",
1440 1441 "cranelift-entity",
1441 1442 "cranelift-isle",
1442 - "gimli",
1443 - "hashbrown 0.15.5",
1443 + "gimli 0.33.0",
1444 + "hashbrown 0.16.1",
1445 + "libm",
1444 1446 "log",
1445 1447 "pulley-interpreter",
1446 1448 "regalloc2",
@@ -1448,14 +1450,14 @@ dependencies = [
1448 1450 "serde",
1449 1451 "smallvec",
1450 1452 "target-lexicon",
1451 - "wasmtime-internal-math",
1453 + "wasmtime-internal-core",
1452 1454 ]
1453 1455
1454 1456 [[package]]
1455 1457 name = "cranelift-codegen-meta"
1456 - version = "0.127.4"
1458 + version = "0.130.2"
1457 1459 source = "registry+https://github.com/rust-lang/crates.io-index"
1458 - checksum = "505cead19304a8dc8689e31b29038775c3f73f9d5ea7a5e33864437a1f46c6b6"
1460 + checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f"
1459 1461 dependencies = [
1460 1462 "cranelift-assembler-x64-meta",
1461 1463 "cranelift-codegen-shared",
@@ -1466,35 +1468,36 @@ dependencies = [
1466 1468
1467 1469 [[package]]
1468 1470 name = "cranelift-codegen-shared"
1469 - version = "0.127.4"
1471 + version = "0.130.2"
1470 1472 source = "registry+https://github.com/rust-lang/crates.io-index"
1471 - checksum = "ce62ba94f570644ce7de6ed05bd39ca28936665dec10a2a1f6f2c531d6add45c"
1473 + checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270"
1472 1474
1473 1475 [[package]]
1474 1476 name = "cranelift-control"
1475 - version = "0.127.4"
1477 + version = "0.130.2"
1476 1478 source = "registry+https://github.com/rust-lang/crates.io-index"
1477 - checksum = "db6cfe339689c3926412a7060ab00ef3b2b43d936b537e7a3f696121be9d0eaa"
1479 + checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9"
1478 1480 dependencies = [
1479 1481 "arbitrary",
1480 1482 ]
1481 1483
1482 1484 [[package]]
1483 1485 name = "cranelift-entity"
1484 - version = "0.127.4"
1486 + version = "0.130.2"
1485 1487 source = "registry+https://github.com/rust-lang/crates.io-index"
1486 - checksum = "625518090e912bdfe3c41464bf97ae421f6044d4ca0f5c3267dcacdb352b033d"
1488 + checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1"
1487 1489 dependencies = [
1488 1490 "cranelift-bitset",
1489 1491 "serde",
1490 1492 "serde_derive",
1493 + "wasmtime-internal-core",
1491 1494 ]
1492 1495
1493 1496 [[package]]
1494 1497 name = "cranelift-frontend"
1495 - version = "0.127.4"
1498 + version = "0.130.2"
1496 1499 source = "registry+https://github.com/rust-lang/crates.io-index"
1497 - checksum = "17f652d40ddf3afb55be64d8979d312b52b384a8cebbcde1dd1c2e32ebcd4466"
1500 + checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e"
1498 1501 dependencies = [
1499 1502 "cranelift-codegen",
1500 1503 "log",
@@ -1504,15 +1507,15 @@ dependencies = [
1504 1507
1505 1508 [[package]]
1506 1509 name = "cranelift-isle"
1507 - version = "0.127.4"
1510 + version = "0.130.2"
1508 1511 source = "registry+https://github.com/rust-lang/crates.io-index"
1509 - checksum = "9f512767e83015f4baf6e732cabca93cea82907e3ab237f826ef64f7ece75eb6"
1512 + checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c"
1510 1513
1511 1514 [[package]]
1512 1515 name = "cranelift-native"
1513 - version = "0.127.4"
1516 + version = "0.130.2"
1514 1517 source = "registry+https://github.com/rust-lang/crates.io-index"
1515 - checksum = "cb1ca6e4dca568ff988d367e4707be2362cee9782265b0a501eaf467ffd550a8"
1518 + checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853"
1516 1519 dependencies = [
1517 1520 "cranelift-codegen",
1518 1521 "libc",
@@ -1521,9 +1524,9 @@ dependencies = [
1521 1524
1522 1525 [[package]]
1523 1526 name = "cranelift-srcgen"
1524 - version = "0.127.4"
1527 + version = "0.130.2"
1525 1528 source = "registry+https://github.com/rust-lang/crates.io-index"
1526 - checksum = "97400ad8fbd3a434092fc0b486fa7784150b53187941d818b1087f3ac0a547f0"
1529 + checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d"
1527 1530
1528 1531 [[package]]
1529 1532 name = "crc"
@@ -1698,6 +1701,12 @@ dependencies = [
1698 1701 ]
1699 1702
1700 1703 [[package]]
1704 + name = "daachorse"
1705 + version = "3.0.0"
1706 + source = "registry+https://github.com/rust-lang/crates.io-index"
1707 + checksum = "9d87f75bbe32ee10609201e09e818537df81c3acb436be2b78f47cc85d139475"
1708 +
1709 + [[package]]
1701 1710 name = "darling"
1702 1711 version = "0.20.11"
1703 1712 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2426,6 +2435,18 @@ dependencies = [
2426 2435 ]
2427 2436
2428 2437 [[package]]
2438 + name = "gimli"
2439 + version = "0.33.0"
2440 + source = "registry+https://github.com/rust-lang/crates.io-index"
2441 + checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c"
2442 + dependencies = [
2443 + "fnv",
2444 + "hashbrown 0.16.1",
2445 + "indexmap",
2446 + "stable_deref_trait",
2447 + ]
2448 +
2449 + [[package]]
2429 2450 name = "git2"
2430 2451 version = "0.20.4"
2431 2452 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2584,7 +2605,6 @@ dependencies = [
2584 2605 "allocator-api2",
2585 2606 "equivalent",
2586 2607 "foldhash 0.1.5",
2587 - "serde",
2588 2608 ]
2589 2609
2590 2610 [[package]]
@@ -3843,12 +3863,12 @@ dependencies = [
3843 3863
3844 3864 [[package]]
3845 3865 name = "object"
3846 - version = "0.37.3"
3866 + version = "0.38.1"
3847 3867 source = "registry+https://github.com/rust-lang/crates.io-index"
3848 - checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
3868 + checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc"
3849 3869 dependencies = [
3850 3870 "crc32fast",
3851 - "hashbrown 0.15.5",
3871 + "hashbrown 0.16.1",
3852 3872 "indexmap",
3853 3873 "memchr",
3854 3874 ]
@@ -4409,21 +4429,21 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
4409 4429
4410 4430 [[package]]
4411 4431 name = "pulley-interpreter"
4412 - version = "40.0.4"
4432 + version = "43.0.2"
4413 4433 source = "registry+https://github.com/rust-lang/crates.io-index"
4414 - checksum = "5de307c194cf6310d486dd5595ffc329c53b4acafd54e214752c1eb2e68be3a9"
4434 + checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5"
4415 4435 dependencies = [
4416 4436 "cranelift-bitset",
4417 4437 "log",
4418 4438 "pulley-macros",
4419 - "wasmtime-internal-math",
4439 + "wasmtime-internal-core",
4420 4440 ]
4421 4441
4422 4442 [[package]]
4423 4443 name = "pulley-macros"
4424 - version = "40.0.4"
4444 + version = "43.0.2"
4425 4445 source = "registry+https://github.com/rust-lang/crates.io-index"
4426 - checksum = "99dca2747e910d10bafe911e172a1b35860268421c3ee5ddb7e16c35e0288b4a"
4446 + checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580"
4427 4447 dependencies = [
4428 4448 "proc-macro2",
4429 4449 "quote",
@@ -4648,13 +4668,13 @@ dependencies = [
4648 4668
4649 4669 [[package]]
4650 4670 name = "regalloc2"
4651 - version = "0.13.5"
4671 + version = "0.15.1"
4652 4672 source = "registry+https://github.com/rust-lang/crates.io-index"
4653 - checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa"
4673 + checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186"
4654 4674 dependencies = [
4655 4675 "allocator-api2",
4656 4676 "bumpalo",
4657 - "hashbrown 0.15.5",
4677 + "hashbrown 0.17.0",
4658 4678 "log",
4659 4679 "rustc-hash 2.1.1",
4660 4680 "smallvec",
@@ -4909,7 +4929,7 @@ dependencies = [
4909 4929 "aws-lc-rs",
4910 4930 "once_cell",
4911 4931 "rustls-pki-types",
4912 - "rustls-webpki 0.103.10",
4932 + "rustls-webpki 0.103.13",
4913 4933 "subtle",
4914 4934 "zeroize",
4915 4935 ]
@@ -4947,9 +4967,9 @@ dependencies = [
4947 4967
4948 4968 [[package]]
4949 4969 name = "rustls-webpki"
4950 - version = "0.103.10"
4970 + version = "0.103.13"
4951 4971 source = "registry+https://github.com/rust-lang/crates.io-index"
4952 - checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
4972 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
4953 4973 dependencies = [
4954 4974 "aws-lc-rs",
4955 4975 "ring",
@@ -6582,7 +6602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6582 6602 checksum = "e151599d689dac80e85c66a7cfa6ffd1b2ab79220517f9161040a87a5041aee3"
6583 6603 dependencies = [
6584 6604 "anyhow",
6585 - "gimli",
6605 + "gimli 0.32.3",
6586 6606 "id-arena",
6587 6607 "leb128",
6588 6608 "log",
@@ -6705,16 +6725,6 @@ dependencies = [
6705 6725
6706 6726 [[package]]
6707 6727 name = "wasm-encoder"
6708 - version = "0.243.0"
6709 - source = "registry+https://github.com/rust-lang/crates.io-index"
6710 - checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35"
6711 - dependencies = [
6712 - "leb128fmt",
6713 - "wasmparser 0.243.0",
6714 - ]
6715 -
6716 - [[package]]
6717 - name = "wasm-encoder"
6718 6728 version = "0.244.0"
6719 6729 source = "registry+https://github.com/rust-lang/crates.io-index"
6720 6730 checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
@@ -6747,19 +6757,6 @@ dependencies = [
6747 6757
6748 6758 [[package]]
6749 6759 name = "wasmparser"
6750 - version = "0.243.0"
6751 - source = "registry+https://github.com/rust-lang/crates.io-index"
6752 - checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d"
6753 - dependencies = [
6754 - "bitflags 2.11.0",
6755 - "hashbrown 0.15.5",
6756 - "indexmap",
6757 - "semver",
6758 - "serde",
6759 - ]
6760 -
6761 - [[package]]
6762 - name = "wasmparser"
6763 6760 version = "0.244.0"
6764 6761 source = "registry+https://github.com/rust-lang/crates.io-index"
6765 6762 checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
@@ -6785,30 +6782,27 @@ dependencies = [
6785 6782
6786 6783 [[package]]
6787 6784 name = "wasmprinter"
6788 - version = "0.243.0"
6785 + version = "0.245.1"
6789 6786 source = "registry+https://github.com/rust-lang/crates.io-index"
6790 - checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b"
6787 + checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8"
6791 6788 dependencies = [
6792 6789 "anyhow",
6793 6790 "termcolor",
6794 - "wasmparser 0.243.0",
6791 + "wasmparser 0.245.1",
6795 6792 ]
6796 6793
6797 6794 [[package]]
6798 6795 name = "wasmtime"
6799 - version = "40.0.4"
6796 + version = "43.0.2"
6800 6797 source = "registry+https://github.com/rust-lang/crates.io-index"
6801 - checksum = "0702b64d4c3fe43ae4ce229e06af06a27783e48c519e68586d180717cdd24314"
6798 + checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf"
6802 6799 dependencies = [
6803 6800 "addr2line",
6804 - "anyhow",
6805 6801 "async-trait",
6806 6802 "bitflags 2.11.0",
6807 6803 "bumpalo",
6808 6804 "cc",
6809 6805 "cfg-if",
6810 - "hashbrown 0.15.5",
6811 - "indexmap",
6812 6806 "libc",
6813 6807 "log",
6814 6808 "mach2",
@@ -6822,14 +6816,13 @@ dependencies = [
6822 6816 "serde_derive",
6823 6817 "smallvec",
6824 6818 "target-lexicon",
6825 - "wasmparser 0.243.0",
6819 + "wasmparser 0.245.1",
6826 6820 "wasmtime-environ",
6821 + "wasmtime-internal-core",
6827 6822 "wasmtime-internal-cranelift",
6828 6823 "wasmtime-internal-fiber",
6829 6824 "wasmtime-internal-jit-debug",
6830 6825 "wasmtime-internal-jit-icache-coherence",
6831 - "wasmtime-internal-math",
6832 - "wasmtime-internal-slab",
6833 6826 "wasmtime-internal-unwinder",
6834 6827 "wasmtime-internal-versioned-export-macros",
6835 6828 "windows-sys 0.61.2",
@@ -6837,41 +6830,55 @@ dependencies = [
6837 6830
6838 6831 [[package]]
6839 6832 name = "wasmtime-environ"
6840 - version = "40.0.4"
6833 + version = "43.0.2"
6841 6834 source = "registry+https://github.com/rust-lang/crates.io-index"
6842 - checksum = "3ffeb777a21965a85e4b1ce7b308c63ba130df91912096b49b95523bf3bdd2c7"
6835 + checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821"
6843 6836 dependencies = [
6844 6837 "anyhow",
6838 + "cranelift-bforest",
6845 6839 "cranelift-bitset",
6846 6840 "cranelift-entity",
6847 - "gimli",
6841 + "gimli 0.33.0",
6842 + "hashbrown 0.16.1",
6848 6843 "indexmap",
6849 6844 "log",
6850 6845 "object",
6851 6846 "postcard",
6852 6847 "serde",
6853 6848 "serde_derive",
6849 + "sha2 0.10.9",
6854 6850 "smallvec",
6855 6851 "target-lexicon",
6856 - "wasm-encoder 0.243.0",
6857 - "wasmparser 0.243.0",
6852 + "wasm-encoder 0.245.1",
6853 + "wasmparser 0.245.1",
6858 6854 "wasmprinter",
6855 + "wasmtime-internal-core",
6856 + ]
6857 +
6858 + [[package]]
6859 + name = "wasmtime-internal-core"
6860 + version = "43.0.2"
6861 + source = "registry+https://github.com/rust-lang/crates.io-index"
6862 + checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3"
6863 + dependencies = [
6864 + "hashbrown 0.16.1",
6865 + "libm",
6866 + "serde",
6859 6867 ]
6860 6868
6861 6869 [[package]]
6862 6870 name = "wasmtime-internal-cranelift"
6863 - version = "40.0.4"
6871 + version = "43.0.2"
6864 6872 source = "registry+https://github.com/rust-lang/crates.io-index"
6865 - checksum = "85da1ba5fee01a3ee21c4d0c8052cc9035388639fa091a969b534d4c6f8449d4"
6873 + checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7"
6866 6874 dependencies = [
6867 - "anyhow",
6868 6875 "cfg-if",
6869 6876 "cranelift-codegen",
6870 6877 "cranelift-control",
6871 6878 "cranelift-entity",
6872 6879 "cranelift-frontend",
6873 6880 "cranelift-native",
6874 - "gimli",
6881 + "gimli 0.33.0",
6875 6882 "itertools",
6876 6883 "log",
6877 6884 "object",
@@ -6879,33 +6886,33 @@ dependencies = [
6879 6886 "smallvec",
6880 6887 "target-lexicon",
6881 6888 "thiserror 2.0.18",
6882 - "wasmparser 0.243.0",
6889 + "wasmparser 0.245.1",
6883 6890 "wasmtime-environ",
6884 - "wasmtime-internal-math",
6891 + "wasmtime-internal-core",
6885 6892 "wasmtime-internal-unwinder",
6886 6893 "wasmtime-internal-versioned-export-macros",
6887 6894 ]
6888 6895
6889 6896 [[package]]
6890 6897 name = "wasmtime-internal-fiber"
6891 - version = "40.0.4"
6898 + version = "43.0.2"
6892 6899 source = "registry+https://github.com/rust-lang/crates.io-index"
6893 - checksum = "a4c7de5a0872764c1ca640886af10a70cf7f8526386906245b43cdb345ece0e6"
6900 + checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85"
6894 6901 dependencies = [
6895 - "anyhow",
6896 6902 "cc",
6897 6903 "cfg-if",
6898 6904 "libc",
6899 6905 "rustix 1.1.4",
6906 + "wasmtime-environ",
6900 6907 "wasmtime-internal-versioned-export-macros",
6901 6908 "windows-sys 0.61.2",
6902 6909 ]
6903 6910
6904 6911 [[package]]
6905 6912 name = "wasmtime-internal-jit-debug"
6906 - version = "40.0.4"
6913 + version = "43.0.2"
6907 6914 source = "registry+https://github.com/rust-lang/crates.io-index"
6908 - checksum = "160acd973d770d62bef1b2697d7fac83a8fe63ef966215e624382b2a9532bd58"
6915 + checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72"
6909 6916 dependencies = [
6910 6917 "cc",
6911 6918 "wasmtime-internal-versioned-export-macros",
@@ -6913,49 +6920,34 @@ dependencies = [
6913 6920
6914 6921 [[package]]
6915 6922 name = "wasmtime-internal-jit-icache-coherence"
6916 - version = "40.0.4"
6923 + version = "43.0.2"
6917 6924 source = "registry+https://github.com/rust-lang/crates.io-index"
6918 - checksum = "cc57f590ba7ea967ea9e8c8560175c6926e5b15d11c29bbde3ad0013a29470eb"
6925 + checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab"
Lines truncated
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.5.15"
3 + version = "0.5.16"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -78,7 +78,7 @@ tempfile = "3"
78 78 infer = "0.19"
79 79 goblin = "0.10"
80 80 zip = "8.2"
81 - yara-x = "1.15"
81 + yara-x = "1.16"
82 82
83 83 # CSV parsing (import system)
84 84 csv = "1.3"
@@ -1,11 +1,64 @@
1 1 # MakeNotWork -- Audit Review
2 2
3 - **Last audited:** 2026-05-11 (Run 25, Ultra Fuzz -- 5-axis deep audit)
4 - **Previous audit:** 2026-05-09 (Run 24, Ultra Fuzz -- 5-axis deep audit)
3 + **Last audited:** 2026-05-11 (Run 26, Ultra Fuzz -- 5-axis deep audit + platform overview verification)
4 + **Previous audit:** 2026-05-11 (Run 25, Ultra Fuzz -- 5-axis deep audit)
5 5
6 6 ## Overall Grade: A
7 7
8 - Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.14. ~88,978 LOC. ~1,225 test annotations. 111 migrations. 3 SERIOUS + 6 MINOR findings identified. 5 cold spots. 5/5 axes at A. All Run 24 fixes verified intact. No regressions.
8 + Run 26: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance) + Platform Overview feature/math verification. v0.5.14. ~88,978 LOC. ~1,225 test annotations. 111 migrations. 0 CRITICAL, 0 SERIOUS, 4 MINOR findings. 5 cold spots. 5/5 axes at A. All 3 Run 25 SERIOUS items verified fixed. 7 Run 25 MINOR items still open. Platform overview: all 27 advertised features verified present; comparison table math has inconsistencies.
9 +
10 + ## Platform Overview Verification
11 +
12 + ### Advertised Features -- All 27 Verified Present
13 +
14 + | Feature | Status | Test Coverage |
15 + |---------|--------|---------------|
16 + | One-time purchases | EXISTS | WELL-TESTED (35+) |
17 + | Pay-what-you-want | EXISTS | UNDERTESTED (5) |
18 + | Subscriptions | EXISTS | WELL-TESTED (41+) |
19 + | Bundles | EXISTS | **UNTESTED (0)** |
20 + | Promo codes | EXISTS | WELL-TESTED (31) |
21 + | License keys | EXISTS | ADEQUATE (30 template tests; DB module 0) |
22 + | Guest checkout | EXISTS | UNDERTESTED (2) |
23 + | Audio/video streaming | EXISTS | **UNTESTED (0)** |
24 + | Chapters | EXISTS | UNDERTESTED (1) |
25 + | File versioning | EXISTS | UNDERTESTED (1) |
26 + | Malware scanning | EXISTS | WELL-TESTED (112) |
27 + | Project storefronts | EXISTS | WELL-TESTED (31 validation) |
28 + | Blog publishing | EXISTS | ADEQUATE (3 validation) |
29 + | Mailing lists/broadcasts | EXISTS | **UNTESTED (0)** |
30 + | RSS feeds | EXISTS | WELL-TESTED (20) |
31 + | Follower system | EXISTS | **UNTESTED (0)** |
32 + | Curated collections | EXISTS | UNDERTESTED (2) |
33 + | Custom domains + TLS | EXISTS | **UNTESTED (0)** |
34 + | Embeddable widgets | EXISTS | **UNTESTED (0)** |
35 + | Analytics | EXISTS | WELL-TESTED (12) |
36 + | Data export (JSON/CSV/ZIP) | EXISTS | **UNTESTED (0)** |
37 + | Git repos + browser/blame | EXISTS | WELL-TESTED (52) |
38 + | Discover page + search | EXISTS | **UNTESTED (0)** |
39 + | 2FA/passkeys | EXISTS | ADEQUATE (12 crypto; enrollment 0) |
40 + | CSRF protection | EXISTS | WELL-TESTED (19) |
41 + | Integrated forums (MT) | EXISTS | N/A (external service) |
42 + | Email-based issues | EXISTS | ADEQUATE (address parsing tests) |
43 +
44 + **8 advertised features have zero tests**: bundles, audio/video streaming, mailing lists, followers, custom domains, embeddable widgets, data export, discover/search. These are the highest-priority coverage gaps.
45 +
46 + ### Comparison Table Math Issues
47 +
48 + The revenue comparison table in `platform-overview.html` has inconsistencies in the Stripe fee model used:
49 +
50 + | Row | Issue |
51 + |-----|-------|
52 + | $500 MNW upper bound | Shows $470 (= $500 - $30 Stripe, no tier). Should be $460 ($500 - $30 - $10 Basic). Off by $10. |
53 + | $1,000 MNW upper bound | Shows $941 (= $1000 - $59 Stripe, no tier). Should be $931 ($1000 - $59 - $10 Basic). Off by $10. |
54 + | $500 Savings upper bound | Shows $75. Should be $65 ($460 - $395) if MNW upper is corrected. |
55 + | $1,000 Savings upper bound | Shows $121. Should be $111 ($931 - $820) if MNW upper is corrected. |
56 + | $5,000 row | Uses ~2.9% flat Stripe fee (no per-transaction $0.30), inconsistent with $500/$1000 rows which use 2.9% + $0.30 |
57 + | Stripe fee footnote | Says "~3%" but effective rate at $10/txn is ~5.9% (2.9% + $0.30/$10). Footnote should clarify per-transaction fee |
58 +
59 + The $2,000 row matches the callout ("~$1,850") and is internally consistent. The breakeven claim ("$67/month") is correct when compared against a 15% platform ($10 tier / 15% = $66.67).
60 +
61 + **Recommendation**: Recalculate all cells with a single explicit Stripe fee model (2.9% + $0.30 per transaction at $10 avg sale). Ensure the MNW range always subtracts the $10 Basic tier at the upper bound.
9 62
10 63 ## Scorecard
11 64
@@ -13,7 +66,7 @@ Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.1
13 66 |-----------|:-----:|-------|
14 67 | Code Quality | A | Zero .unwrap() in production paths. Clean macro patterns throughout |
15 68 | Architecture | A | Clean layer separation. Trait-based backends for storage/email/payments |
16 - | Testing | A | ~1,225 test annotations, proptest active, adversarial tests, comprehensive harness |
69 + | Testing | A- | ~1,225 test annotations, proptest active, adversarial tests. 8 advertised features at 0 tests |
17 70 | Security | A | Constant-time compare, fail-closed scanning, CSRF everywhere, Argon2id, HMAC webhooks, PKCE S256. DUMMY_HASH pattern on all login paths |
18 71 | Performance | A | Discover facets parallelized. Batch loading. Bounded scan semaphore. Advisory lock pinned |
19 72 | Documentation | A | Module-level //! on all major files. README.md present |
@@ -53,8 +106,8 @@ Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.1
53 106 | db/items.rs | A | A | B | A | A- | A | A | A | A |
54 107 | db/synckit.rs | A | A | B | A | A | A | A | A | A |
55 108 | db/transactions.rs | A | A | B+ | A | A | A | A- | A | A |
56 - | db/discover.rs | A | A | B | A | A | A | A | n/a | A- |
57 - | db/cart.rs | **B+** | A | B | A | A | A | A | n/a | A |
109 + | db/discover.rs | A | A | **B-** | A | A | A | A | n/a | A- |
110 + | db/cart.rs | A | A | B | A | A | A | A | n/a | A |
58 111 | db/creator_tiers.rs | A | A+ | A | A | A | A | A | n/a | A |
59 112 | db/versions.rs | A | A | A | A | A | A | A | n/a | A |
60 113 | db/builds.rs | A | A+ | A | A | A | A | A | n/a | A |
@@ -67,6 +120,11 @@ Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.1
67 120 | db/pending_uploads.rs | A | A | n/a | A | A | A | A | n/a | A |
68 121 | db/moderation.rs | A | A | B+ | A | n/a | A | A | A | A |
69 122 | db/reports.rs | A | A | B+ | A | n/a | A | A | A | A |
123 + | db/bundles.rs | A | A | **B-** | A | A | A | A | n/a | A |
124 + | db/follows.rs | A | A | **B-** | A | A | A | A | n/a | A |
125 + | db/mailing_lists.rs | A | A | **B-** | A | A | A | A | n/a | A |
126 + | db/custom_domains.rs | A | A | **B-** | A | A | A | A | n/a | A |
127 + | db/media_files.rs | A | A | **B-** | A | A | A | A | n/a | A |
70 128 | db/models/* | A | A | B+ | A | n/a | A- | A | n/a | A |
71 129 | types/ | A | A | B | A | n/a | A | A | n/a | A |
72 130 | scanning/ | A | A+ | A- | A+ | A- | A | A- | A | A |
@@ -90,16 +148,15 @@ Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.1
90 148 | routes/oauth.rs | A | A | n/a | A | A | A | A | A- | A- |
91 149 | routes/admin/ | A | A | n/a | A | A | A | A | A | A |
92 150 | routes/api/ | A | A | n/a | A | A | A | A | A | B+ |
93 - | routes/api/projects.rs (delete) | **B-** | A | n/a | A | A | A | A | A | A |
94 151 | routes/api/exports/ | A- | A- | n/a | A | A- | A | A | A | A- |
95 - | routes/api/exports/content.rs | **B+** | A- | n/a | A | **B+** | A | A | A | A |
152 + | routes/api/exports/content.rs | A- | A- | n/a | A | A- | A | A | A | A |
96 153 | routes/stripe/ | A | A | n/a | A | A- | A | A | A | B+ |
97 154 | routes/stripe/checkout/ | A | A | n/a | A- | A | A | A | A | A |
98 155 | routes/stripe/checkout/cart.rs | A | A | n/a | A- | A | A | A | A | A |
99 156 | routes/synckit/ | A | A | n/a | A | A- | A | A | A | A |
100 157 | routes/pages/discover.rs | A | A | n/a | A | A | B+ | A | A | A |
101 158 | routes/pages/ (other) | A | A | n/a | A | A | A | A | A- | B+ |
102 - | routes/embed/ | A | A | n/a | A | A | A- | A | A | A |
159 + | routes/embed/ | A | A | n/a | **B+** | A | A- | A | A | A |
103 160 | routes/git/ | A | A | n/a | A | A | A | A | A | A |
104 161 | routes/storage/ | A | A | n/a | A | A | A | A | A | A |
105 162 | routes/storage/versions.rs | A | A- | n/a | A | A | A | A | A | A |
@@ -107,177 +164,154 @@ Run 25: Ultra Fuzz (Payments, Storage, UX Wiring, Security, Performance). v0.5.1
107 164 | routes/storage/downloads.rs | A | A | n/a | A | A- | A | A | A | A |
108 165 | routes/storage/images.rs | **B+** | A | n/a | A | A | A | A | A | A- |
109 166
110 - **Bold** = cold spot (B or below).
167 + **Bold** = cold spot (B or below, or significant test gap on advertised feature).
111 168
112 169 ### Cold Spots
113 170
114 171 | Module | Grade | Issue |
115 172 |--------|:-----:|-------|
116 - | db/cart.rs | B+ | SQL references `t.user_id` instead of `t.buyer_id` — runtime error |
117 - | routes/api/projects.rs (delete) | B- | Project deletion orphans S3 objects, no storage decrement |
118 - | routes/api/exports/content.rs | B+ | Buffers up to 2GB ZIP in memory per export request |
119 - | routes/storage/images.rs | B+ | Non-atomic 3-UPDATE cover image, non-atomic storage swap on project images, missing S3 cleanup |
120 - | templates/purchase.html | B+ | Cart add-to-cart JS doesn't check HTTP error status |
173 + | routes/storage/images.rs | B+ | Non-atomic 3-UPDATE cover image, non-atomic storage swap, missing S3 cleanup (carried from Run 25) |
174 + | routes/embed/ | B+ | Unescaped URLs in `<img src>` attributes (server-generated URLs, low risk) |
175 + | templates/purchase.html | B+ | Cart add-to-cart JS doesn't check HTTP error status (carried from Run 25) |
176 + | db/ (6 modules) | B- (test) | bundles, follows, mailing_lists, custom_domains, media_files, discover all have 0 unit tests for advertised features |
177 + | routes/pages/blog.rs | B+ | `Slug::from_trusted` on user-supplied URL path bypasses validation |
121 178
122 - ### Resolved Cold Spots (from Run 24)
179 + ### Resolved Cold Spots (from Run 25)
123 180
124 - - ~~routes/api/guest_checkout.rs (B)~~ -- Added `starts_at` promo code validation. Matches all authenticated checkout paths.
125 - - ~~routes/storage/versions.rs (B-)~~ -- Now uses atomic `try_replace_storage` for file replacements. No separate decrement/increment.
126 -
127 - ### Resolved Cold Spots (from Run 23)
128 -
129 - - ~~versions.rs transaction safety (B)~~ -- Old S3 key now enqueued for deletion via `pending_s3_deletions`.
130 - - ~~cart.rs performance (B-)~~ -- Consolidated into single-query `toggle_cart_preflight()`.
131 - - ~~downloads.rs performance (B-)~~ -- Consolidated into single-query `check_item_access()`.
132 - - ~~exports.rs performance (B-) + size (B)~~ -- Files written directly to ZIP one-by-one. Module split into `exports/mod.rs` + `exports/content.rs`.
133 - - ~~archive.rs security (B+)~~ -- Decompression fallback uses 10x conservative multiplier.
134 - - ~~checkout/cart.rs correctness (B+)~~ -- Was false positive; atomic `try_increment_use_count` already in place at checkout creation.
181 + - ~~db/cart.rs (B+)~~ -- Fixed. SQL now correctly references `t.buyer_id`.
182 + - ~~routes/api/projects.rs delete (B-)~~ -- Fixed. S3 keys collected and enqueued to `pending_s3_deletions`, storage decremented.
183 + - ~~routes/api/exports/content.rs (B+)~~ -- Fixed. ZIP now writes to temp file on disk. Peak memory is O(largest_single_file).
135 184
136 185 ## Mandatory Surprises
137 186
138 - **Run 25 (5 surprises, one per axis):**
187 + **Run 26 (5 surprises, one per axis):**
139 188
140 - 1. **Payments -- Pending refund queue with FOR UPDATE SKIP LOCKED (unexpectedly good):** `db/pending_refunds.rs` + `checkout_helpers.rs:check_pending_refund`. When a `charge.refunded` webhook arrives before `checkout.session.completed`, the refund is queued and processed after checkout completion. Claims use `FOR UPDATE SKIP LOCKED` for safe concurrency. Stale refunds are escalated. This solves one of the hardest webhook ordering problems correctly.
189 + 1. **Payments -- Pending refund queue with FOR UPDATE SKIP LOCKED (unexpectedly good):** `db/pending_refunds.rs` + `checkout_helpers.rs:check_pending_refund`. When a `charge.refunded` webhook arrives before `checkout.session.completed`, the refund is queued and processed after checkout completion. Claims use `FOR UPDATE SKIP LOCKED` for safe concurrency. Stale refunds are escalated. Additionally, webhook dedup returns 503 on dedup-check failure rather than proceeding without protection -- subtle correctness choice.
141 190
142 - 2. **Storage -- Durable S3 deletion queue as transactional outbox (unexpectedly good):** `db/pending_s3_deletions.rs` + `scheduler/cleanup.rs:365-418`. Every destructive path enqueues S3 keys to a durable DB table before deletion. Retry with attempt counting, `FOR UPDATE SKIP LOCKED`, and prefix vs single-key distinction. Production-grade pattern -- the one exception (project deletion, see SERIOUS finding) makes its absence there even more glaring.
191 + 2. **Storage -- Durable pending_uploads/pending_s3_deletions queue system (unexpectedly good):** Every presigned URL issuance records a `pending_upload` in PostgreSQL. If the client never confirms, a reaper reclaims the S3 object. For deletions, `pending_s3_deletions` uses `FOR UPDATE SKIP LOCKED` with atomic attempt tracking. Combined with `recalculate_all_storage_batch` drift corrector, this is a three-layer defense against storage accounting bugs. Enterprise-grade resource lifecycle management.
143 192
144 - 3. **UX Wiring -- json_escape with HTML entity protection (unexpectedly good):** `types/mod.rs:214`. Hand-rolled escaper goes beyond standard JSON to also escape `<`, `>`, `&` as Unicode escapes for safe embedding in `<script>` tags via `|safe`. Combined with `build_segments_json`'s `</` to `<\/` replacement. Defense-in-depth approach most codebases skip entirely.
193 + 3. **UX Wiring -- CSRF implementation is textbook-perfect (unexpectedly good):** Constant-time comparison via `crate::helpers::constant_time_compare`. Global HTMX injection via `htmx:configRequest` means 150+ state-changing HTMX calls automatically include the token. 19 unit tests including adversarial cases. Exempt path matching prevents prefix collisions. Total CSRF bypass surface is near zero.
145 194
146 - 4. **Security -- Anti-timing dummy hash on ALL login paths (unexpectedly good):** `routes/auth.rs:29-31` and `routes/oauth.rs:33-35`. Pre-computed `DUMMY_HASH` via `LazyLock` ensures "user not found" takes the same wall-clock time as "wrong password". Applied independently across both web and OAuth login surfaces. Plus `crypto.rs:7-18` hashes both inputs with SHA-256 before constant-time XOR comparison -- neutralizes length-leak.
195 + 4. **Security -- Archive scanning does not trust attacker-controlled metadata (unexpectedly good):** `scanning/archive.rs:91-132` actually decompresses each ZIP entry byte-by-byte to measure real decompressed size, rather than trusting the ZIP central directory `size()` field. When decompression errors occur (a common evasion technique), it falls back to conservative `claimed_size * 10` estimate. Magic bytes captured during first pass rather than making a second read. Production-grade anti-ZIP-bomb engineering.
147 196
148 - 5. **Performance -- Discover page runs 5 facet queries in parallel via tokio::try_join! (unexpectedly good):** `routes/pages/public/discover.rs:342-361`. Type counts, tag counts, followed tags, AI tier counts, and price range counts all run concurrently. Textbook optimization that most production codebases miss. The codebase consistently avoids N+1 through batch loading across exports, items, and versions.
197 + 5. **Performance -- Content export ZIP architecture (unexpectedly good):** `routes/api/exports/content.rs` writes to a temp file on disk keeping peak memory at O(single file). Downloads S3 objects one at a time, dropping each buffer after writing to ZIP. Uploads finished ZIP back to S3 via multipart in 10MB chunks. Returns presigned download URL. Enforces 2GB cap with clear error. Includes README.txt manifest. Gracefully handles partial failures. Production-grade export engineering.
149 198
150 199 ### Previous Surprises
151 200
201 + **Run 25:** Pending refund queue, durable S3 deletion queue, json_escape HTML entity protection, anti-timing dummy hash on all login paths, discover page 5-way parallel facet queries.
202 +
152 203 **Run 24:** Claim token architecture, LATERAL join batch storage recalc, CSRF constant-time comparison, TOTP replay prevention via time step, storage quota atomicity + idempotency checks.
153 204
154 205 **Run 23:** Webhook signature gold standard, atomic storage quota enforcement, CSRF dual-layer extraction, archive decompression fallback (bad, fixed), advisory lock protocol.
155 206
156 - **Run 22:** Webhook signature gold standard, cleanup scheduler durable queue, form error recovery, defense-in-depth 6 layers, export RAM buffering.
157 -
158 - **Run 21:** Pending refund bidirectional matching, claim_pending_build FOR UPDATE SKIP LOCKED, json_escape, SHA-256 constant_time_compare, scheduler advisory lock unpinned (bad, fixed).
159 -
160 - **Run 20:** Scanning module -- 6-layer anti-malware with decompression, VMProtect/UPX detection, ZIP bomb byte counting.
161 -
162 - **Run 19:** Hand-rolled Stripe v2 webhook signature verification with replay protection.
163 -
164 207 ## Strengths
165 208
166 209 ### 1. Security-in-depth
167 - Zero SQL injection vectors across 200+ queries. Argon2id (46MiB/2 iterations), SHA-256-based constant-time comparison, CSRF synchronizer tokens (constant-time validated), session fixation prevention, account lockout with anti-enumeration dummy hashes on ALL login paths (web, SyncKit, OAuth), PKCE S256 required, rate limiting on all endpoint classes, HMAC-signed URLs, 6-layer malware scanning with fail-closed, ZIP bomb detection with 10x conservative multiplier, path traversal prevention, shell command validation, TOTP replay prevention via time step tracking, passkey counter updates.
210 + Zero SQL injection vectors across 200+ queries. Argon2id (46MiB/2 iterations), SHA-256-based constant-time comparison, CSRF synchronizer tokens (constant-time validated), session fixation prevention, account lockout with anti-enumeration dummy hashes on ALL login paths (web, SyncKit, OAuth), PKCE S256 required, rate limiting on all endpoint classes (22 distinct configurations), HMAC-signed URLs, 6-layer malware scanning with fail-closed, ZIP bomb detection with byte-by-byte decompression, path traversal prevention, shell command validation, TOTP replay prevention via time step tracking, passkey counter updates, breached password check via HaveIBeenPwned k-anonymity.
168 211
169 212 ### 2. Type safety discipline
170 - 36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types, Cents/PriceCents monetary newtypes with proptest coverage. All money math in integer cents (i32/i64), zero floating point in money paths. `SUM(BIGINT)::BIGINT` cast used consistently. License keys use CSPRNG (~55 bits entropy). FOR UPDATE row locking on license activation prevents TOCTOU.
213 + 36 UUID newtypes via `define_pg_uuid_id!`, 25+ domain enums via `impl_str_enum!`, validated string types, Cents/PriceCents monetary newtypes with proptest coverage. All money math in integer cents (i32/i64), zero floating point in money paths. `SUM(BIGINT)::BIGINT` cast used consistently across all 40+ SUM queries. License keys use CSPRNG (~55 bits entropy). FOR UPDATE row locking on license activation prevents TOCTOU.
171 214
172 215 ### 3. Payment robustness
173 - Three-layer webhook idempotency. Bidirectional pending refund matching with FOR UPDATE SKIP LOCKED. Atomic promo code enforcement at DB level. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all paths. Cart PWYW minimum enforced. Tip amounts capped. Guest purchase claim tokens are cryptographically sound and idempotent.
216 + Three-layer webhook idempotency. Bidirectional pending refund matching with FOR UPDATE SKIP LOCKED. Atomic promo code enforcement at DB level. `FOR UPDATE` row locking on tier deletion, license activation, pending refund claims. Self-purchase blocked across all paths. Cart PWYW minimum enforced. Tip amounts capped. Guest purchase claim tokens are cryptographically sound and idempotent. Direct Charges pattern architecturally enforces 0% platform fee by never setting `application_fee_amount`.
174 217
175 218 ### 4. Operational maturity
176 - All Run 23-24 fixes verified intact. Idempotency key scope widened (migration 106). Pending S3 deletions UNIQUE constraint (migration 107). Scheduler advisory lock pinned. Soft-delete purge handles version S3 keys. Confirm uploads wrapped in transactions. Discover page parallelized. Content exports stream one-by-one. N+1 queries consolidated.
219 + All Run 25 SERIOUS items fixed. Pending S3 deletions with UNIQUE constraint. Scheduler advisory lock pinned. Soft-delete purge handles version S3 keys. Confirm uploads wrapped in transactions. Discover page parallelized. Content exports stream to disk one-by-one. N+1 queries consolidated. Project deletion now properly enqueues S3 keys and decrements storage.
177 220
178 221 ## Weaknesses
179 222
180 - ### Active (Run 25)
223 + ### Active (Run 26)
181 224
182 - - **Cart preflight SQL column name bug** -- `db/cart.rs:106` references `t.user_id` which does not exist on the `transactions` table (column is `buyer_id`). Runtime SQL error on cart toggle preflight.
183 - - **Project deletion orphans S3 objects** -- `routes/api/projects.rs:270-282` CASCADEs DB records but does not enqueue S3 files for deletion or decrement storage counters. Permanent S3 orphans.
184 - - **Content export buffers up to 2GB in memory** -- `routes/api/exports/content.rs:120-127` writes entire ZIP to in-memory Vec. OOM risk under concurrent export requests.
185 - - **Non-atomic item cover image update** -- `routes/storage/images.rs:369-371` does 3 separate UPDATEs for cover_image_url, cover_s3_key, and cover_file_size_bytes. Crash between any two leaves inconsistent state.
186 - - **Project image non-atomic storage swap** -- `routes/storage/images.rs:173-185` decrements then increments in two queries. Should use `try_replace_storage`.
187 - - **Project image missing S3 deletion enqueue** -- `routes/storage/images.rs:173-179` does not enqueue old project image to `pending_s3_deletions`. Item images do clean up.
225 + - **8 advertised features with zero test coverage** -- Bundles, audio/video streaming, mailing lists/broadcasts, followers, custom domains, embeddable widgets, data export, discover/search all have 0 unit tests. These are all features shown in the platform overview sent to potential creators.
226 + - **Comparison table math inconsistencies** -- `platform-overview.html` revenue comparison table has incorrect upper bounds at $500 and $1,000 revenue levels (off by $10, missing Basic tier fee). Stripe fee model varies across rows.
227 + - **Non-atomic item cover image update** -- `routes/storage/images.rs:369-371` does 3 separate UPDATEs. Carried from Run 25.
228 + - **Non-atomic project image storage swap** -- `routes/storage/images.rs:173-185` decrements then increments in two queries. Carried from Run 25.
229 + - **Project image missing S3 deletion enqueue** -- `routes/storage/images.rs:173-179` does not enqueue old project image. Carried from Run 25.
230 + - **Unescaped URLs in embed HTML** -- `routes/embed/item.rs:108` and similar. Server-generated CDN URLs interpolated raw into `<img src>`. Low risk (not user input) but violates defense-in-depth.
231 + - **Slug::from_trusted on user-supplied path** -- `routes/pages/blog.rs:175` bypasses slug validation. No injection risk (parameterized SQL) but inconsistent with other handlers.
232 + - **Broadcast validation uses byte length** -- `routes/api/users/broadcast.rs:46,53` uses `.len()` instead of `.chars().count()`. Stricter than intended for multi-byte characters.
188 233
189 - ### Resolved (Run 24)
234 + ### Resolved (Run 25 -> Run 26)
190 235
191 - - ~~Guest checkout promo code validation gap~~ -- Fixed. Added `starts_at` check.
192 - - ~~Version replace storage counter corruption~~ -- Fixed. Uses `try_replace_storage`.
236 + - ~~Cart preflight SQL column name bug~~ -- Fixed. `t.buyer_id` now used correctly.
237 + - ~~Project deletion orphans S3 objects~~ -- Fixed. S3 keys collected, enqueued, storage decremented.
238 + - ~~Content export buffers 2GB in memory~~ -- Fixed. ZIP writes to temp file on disk.
193 239
194 240 ## Bug Reports by Axis
195 241
196 242 ### Payments
197 - 0 CRITICAL, 1 SERIOUS, 0 MINOR, 4 NOTE
243 + 0 CRITICAL, 0 SERIOUS, 0 MINOR, 4 NOTE
198 244
199 245 | # | Sev | Location | Description |
200 246 |---|-----|----------|-------------|
201 - | P1 | **SERIOUS** | `db/cart.rs:106` | SQL references `t.user_id` instead of `t.buyer_id`. Column does not exist on `transactions` table. Runtime SQL error on cart toggle preflight check. |
202 - | P2 | NOTE | `payments/checkout.rs:33` | `CartLineItem.amount_cents` is `i64` while `CheckoutParams.amount_cents` is `Cents`. Inconsistency, not exploitable. |
203 - | P3 | NOTE | `formatting.rs:11` | `format_price` uses `cents as f64 / 100.0`. Safe for typical amounts but code smell in monetary code. |
204 - | P4 | NOTE | `db/transactions.rs:462` | `CreateProjectTransactionParams.amount_cents` is `i32` while item transactions use `Cents` (i64). |
205 - | P5 | NOTE | `pricing.rs:134` | `FixedPricing::validate_amount` has no upper cap (PWYW caps at $10K). Low impact -- Stripe session amount is server-side. |
247 + | P1 | NOTE | `validated_types.rs:226` | Cents `encode_by_ref` casts i64 to i32 with debug_assert only. Release builds would silently truncate values > i32::MAX. Safe in practice -- all write paths originate from PriceCents (capped at $10k). |
248 + | P2 | NOTE | `routes/stripe/webhook/checkout.rs:42` | `payment_intent_id` defaults to "unknown" when None. Collision risk if multiple sessions use the fallback. Stripe always sets payment_intent on completed sessions. |
249 + | P3 | NOTE | `checkout_helpers.rs:248` | Revenue split rounding distributes remainder to first members in list order. Deterministic but slightly favors early members. |
250 + | P4 | NOTE | `db/transactions.rs:462` | `CreateProjectTransactionParams.amount_cents` is `i32` while item transactions use `Cents` (i64). Type inconsistency. |
206 251
207 252 ### Storage
208 - 0 CRITICAL, 1 SERIOUS, 3 MINOR, 2 NOTE
253 + 0 CRITICAL, 0 SERIOUS, 2 MINOR, 1 NOTE (carried from Run 25)
209 254
210 255 | # | Sev | Location | Description |
211 256 |---|-----|----------|-------------|
212 - | S1 | **SERIOUS** | `routes/api/projects.rs:270-282` | Project deletion CASCADEs DB records but orphans all S3 objects (audio, cover images, version downloads, video). No storage decrement. |
213 - | S2 | MINOR | `routes/storage/images.rs:369-371` | Item cover update: 3 separate UPDATEs for url, s3_key, file_size. Crash-unsafe. |
214 - | S3 | MINOR | `routes/storage/images.rs:173-185` | Project image replace: non-atomic decrement/increment. Should use `try_replace_storage`. |
215 - | S4 | MINOR | `routes/storage/images.rs:173-179` | Project image replace: old S3 object not enqueued to `pending_s3_deletions`. |
216 - | S5 | NOTE | `routes/api/content_insertions.rs:301-309` | Delete-before-decrement order. If decrement fails, storage counter overstated until weekly recalc. |
217 - | S6 | NOTE | `routes/storage/uploads.rs:250-253` | Dynamic SQL column names via `format!()` from internal enum. Not injection risk, but bypasses compile-time checking. |
257 + | S1 | MINOR | `routes/storage/images.rs:369-371` | Item cover update: 3 separate UPDATEs for url, s3_key, file_size. Crash-unsafe. |
258 + | S2 | MINOR | `routes/storage/images.rs:173-185` | Project image replace: non-atomic decrement/increment + missing S3 deletion enqueue. |
259 + | S3 | NOTE | `routes/api/exports/content.rs:138-159` | Sequential S3 downloads in content export. Acceptable tradeoff (bounded memory) but large exports are slow. |
218 260
219 261 ### UX Wiring
220 - 0 CRITICAL, 0 SERIOUS, 2 MINOR, 3 NOTE
262 + 0 CRITICAL, 0 SERIOUS, 2 MINOR, 2 NOTE
221 263
222 264 | # | Sev | Location | Description |
223 265 |---|-----|----------|-------------|
224 - | U1 | MINOR | `templates/pages/purchase.html:142` | Cart add-to-cart JS `fetch()` does not check response status before redirecting to `/cart`. 4xx/5xx silently redirect. |
225 - | U2 | MINOR | `formatting.rs:8-9` | `format_price(-500)` produces `$-5.00` vs `format_revenue(-500)` = `-$5.00`. Inconsistent negative formatting. |
226 - | U3 | NOTE | `routes/pages/public/discover.rs:85` | No upper bound on page param. `.max(1)` clamp only. DB returns empty; no impact. |
227 - | U4 | NOTE | `routes/pages/public/join_wizard.rs:88-95` | Minimal email validation (checks `@` + `.`). Acceptable -- verification email is the real gate. |
228 - | U5 | NOTE | `routes/pages/public/content/item.rs:340` | `cdn_base` variable scope spans entire monolithic function. Refactoring hazard, not a bug. |
266 + | U1 | MINOR | `routes/embed/item.rs:108,248-249,289` | Unescaped URLs in embed HTML `<img src>`. Server-generated CDN URLs, not user input. Defense-in-depth gap. |
267 + | U2 | MINOR | `routes/api/users/broadcast.rs:46,53` | Broadcast subject/body validation uses `.len()` (bytes) instead of `.chars().count()`. Multi-byte characters counted as 2-4. |
268 + | U3 | NOTE | `routes/pages/blog.rs:175` | `Slug::from_trusted(post_slug)` on user-supplied URL path bypasses slug validation. No injection risk. |
269 + | U4 | NOTE | `templates/pages/purchase.html:142` | Cart add-to-cart JS doesn't check HTTP error status before redirect. Carried from Run 25. |
229 270
230 271 ### Security
231 - 0 CRITICAL, 0 SERIOUS, 0 MINOR, 7 NOTE
272 + 0 CRITICAL, 0 SERIOUS, 0 MINOR, 4 NOTE
232 273
233 274 | # | Sev | Location | Description |
234 275 |---|-----|----------|-------------|
235 - | X1 | NOTE | `auth.rs:206` | `MaybeUser` skips session revocation check (documented, intentional). OAuth compensates with manual validation. |
236 - | X2 | NOTE | `routes/api/users/profile.rs:140-146` | Breached password change is advisory-only. Intentional UX decision. |
237 - | X3 | NOTE | `db/totp.rs:22` | TOTP secret stored as plaintext in DB. Best practice would encrypt at rest. |
238 - | X4 | NOTE | `synckit_auth.rs:57-58` | JWT `iat` claim not validated. Standard practice; `exp` is validated. |
239 - | X5 | NOTE | `scanning/archive.rs:137-157` | Nested archives counted but not recursively scanned. YARA + ClamAV still scan outer bytes. |
240 - | X6 | NOTE | `scanning/structural.rs:37-38` | `posix_spawn` and `dlopen` in suspicious symbols list may false-positive on legitimate macOS apps. |
241 - | X7 | NOTE | `main.rs:116-121` | Session cookie not prefixed with `__Host-`. Low risk -- no user-content subdomains. |
276 + | X1 | NOTE | `main.rs:116-121` | `with_http_only(true)` not explicitly set on session cookie. tower-sessions defaults to true, but implicit. |
277 + | X2 | NOTE | `auth.rs:206` | `MaybeUser` skips session revocation check (documented, intentional). Used only on read-only endpoints. |
278 + | X3 | NOTE | `synckit_auth.rs:57-68` | JWT `iat` claim not validated. Standard practice; `exp` is validated. |
279 + | X4 | NOTE | `csrf.rs:138-146` | CSRF exempt list is broad (11 paths). Each has documented justification. Worth monitoring as routes are added. |
242 280
243 281 ### Performance
244 - 0 CRITICAL, 1 SERIOUS, 3 MINOR, 4 NOTE
282 + 0 CRITICAL, 0 SERIOUS, 0 MINOR, 4 NOTE
245 283
246 284 | # | Sev | Location | Description |
247 285 |---|-----|----------|-------------|
248 - | F1 | **SERIOUS** | `routes/api/exports/content.rs:120-127` | Content export accumulates up to 2GB in an in-memory `Vec<u8>` ZIP buffer. OOM risk under concurrent exports. |
249 - | F2 | MINOR | `routes/api/exports/mod.rs:268-270` | N+1 query: collection items loaded per-collection in a loop (up to 50 queries). |
250 - | F3 | MINOR | `build_runner.rs:396-408` | Build artifact read entirely into memory via `tokio::fs::read` before S3 upload. Mitigated by single-build advisory lock. |
251 - | F4 | MINOR | `db/mod.rs:93` | `check_sandbox_cap` uses blocking `pg_advisory_lock` instead of `pg_try_advisory_lock`. Latency spike under burst. |
252 - | F5 | NOTE | `scanning/mod.rs:68` | Scan pipeline takes `&[u8]` requiring full file in memory. Semaphore limits to 4 x 100MB = 400MB worst case. |
253 - | F6 | NOTE | `lib.rs:238` | CSP header reads `S3_ENDPOINT` from `env::var` every request. Should use `state.config`. |
254 - | F7 | NOTE | `routes/synckit/subscribe.rs:41-58` | SseConnectionGuard Drop has TOCTOU race on DashMap counter. Worst case: stale entry (harmless). |
255 - | F8 | NOTE | `payments/checkout.rs:33` | `CartLineItem.amount_cents` is raw `i64` instead of `Cents` type. |
286 + | F1 | NOTE | `routes/pages/public/discover.rs:341-361` | Discover page fires 7+ parallel DB queries per request. At 100 concurrent users = 700 query demands on 25-connection pool. CDN caching (`s-maxage=60`) mitigates. |
287 + | F2 | NOTE | `routes/api/exports/mod.rs:268-270` | N+1 query: collection items loaded per-collection (up to 50 queries). Export is rate-limited. |
288 + | F3 | NOTE | `build_runner.rs:396-408` | Build artifact read entirely into memory before S3 upload. Single-build lock limits blast radius. |
289 + | F4 | NOTE | `scheduler/integrity.rs:54-62` | Weekly sales count drift check joins entire transactions table. Grows with platform. LIMIT 50 bounds result but not scan. |
256 290
257 291 ## Cross-Cutting Concerns
258 292
259 - ### Project deletion missing S3 cleanup (Storage + Payments)
260 - Project deletion CASCADEs transactions, items, and versions in the DB but never touches S3 or storage counters. The `pending_s3_deletions` durable queue pattern exists and is used everywhere else (item deletion, version replacement, soft-delete purge). Its absence here is inconsistent and the most impactful finding of this audit.
261 -
262 - ### Content export memory pressure (Storage + Performance)
263 - The content export buffers an entire ZIP in memory (up to 2GB). This crosses both the storage axis (resource management) and performance axis (OOM risk). The route is rate-limited to 3 req/sec and requires authentication, limiting blast radius, but a single malicious creator could consume 2GB of server RAM.
293 + ### Test coverage gaps on advertised features (All Axes)
294 + 8 features shown in the platform overview sent to potential creators have zero tests: bundles, audio/video streaming, mailing lists/broadcasts, followers, custom domains, embeddable widgets, data export, and discover/search. While the code for these features is solid (A-grade on code quality), regressions in any of these would be invisible to CI. The highest-risk gaps are audio/video streaming (paid tier feature with access control) and discover/search (the primary content discovery mechanism).
264 295
265 296 ### Image upload atomicity gap (Storage + UX)
266 - Both project image and item cover image confirms have atomicity issues. The item path does 3 separate UPDATEs; the project path does non-atomic storage swap and skips S3 cleanup. The version upload path (`uploads.rs`) correctly uses `try_replace_storage` and `pending_s3_deletions` -- the image paths should follow the same pattern.
297 + Both project image and item cover image confirms have atomicity issues. The item path does 3 separate UPDATEs; the project path does non-atomic storage swap and skips S3 cleanup. The version upload path (`uploads.rs`) correctly uses `try_replace_storage` and `pending_s3_deletions` -- the image paths should follow the same pattern. Carried from Run 25.
298 +
299 + ### Platform overview accuracy (UX + Payments)
300 + The comparison table math has inconsistencies that could be spotted by a savvy creator doing their own calculations. While the core value proposition is correct (0% platform fee saves money at scale), the specific dollar figures at $500 and $1,000 revenue are off by $10 at the upper bound. The Stripe fee model is not consistent across rows.
267 301
268 302 ## Components Successfully Stress-Tested
269 303
270 - ### Payments (12 vectors survived)
271 - Webhook replay, double-credit, concurrent promo exhaustion (atomic at DB level), cross-user data access, self-purchase, negative/overflow amounts, floating-point money math, out-of-order webhooks, suspended creator purchases, SUM(BIGINT) pitfall, PWYW cart minimum enforcement, tip amount cap enforcement.
304 + ### Payments (16 vectors survived)
305 + Webhook replay, double-credit, concurrent promo exhaustion (atomic at DB level), cross-user data access, self-purchase, negative/overflow amounts, floating-point money math, out-of-order webhooks, suspended creator purchases, SUM(BIGINT) pitfall, PWYW cart minimum enforcement, tip amount cap, stale pending transaction cleanup, bundle refund cascade, subscription tier delete TOCTOU, webhook handler crash with retry/dead-letter.
272 306
273 307 ### Storage (10 vectors survived)
274 308 Cross-user file overwrites, path traversal in filenames, content type smuggling, storage quota bypass (atomic enforcement), double-spend on idempotent confirm, malware file serving, orphaned upload cleanup, SUM(BIGINT) pitfall, transactional confirm uploads, soft-delete purge with version S3 keys.
275 309
276 310 ### UX Wiring (10 vectors survived)
277 - XSS via template injection, open redirect, user enumeration, markdown/HTML injection, pagination abuse, integer overflow in pricing, internal detail leakage, CSV injection, Unicode boundary attacks, JSON-LD breakout.
311 + XSS via template injection, open redirect, user enumeration, markdown/HTML injection, pagination abuse, integer overflow in pricing, internal detail leakage, CSV injection (sanitize_csv_cell), Unicode boundary attacks, JSON-LD breakout.
278 312
279 313 ### Security (17 vectors survived)
280 - Virus scan bypass via ClamAV downtime (fail-closed), content-type spoofing, path traversal in archives, session fixation, timing-based user enumeration (dummy hash on all 3 login paths), brute force login (lockout), X-Forwarded-For spoofing (Cloudflare-aware), session reuse after password change, OAuth code replay, PKCE downgrade, token prediction (CSPRNG), CSRF on state-changing endpoints (constant-time), SSH command injection, passkey cloning (counter), TOTP replay (last_used_step), IDOR on passkeys/sessions, archive bombs (byte counting + 10x multiplier).
314 + Virus scan bypass via ClamAV downtime (fail-closed), content-type spoofing (PE-as-audio, ZIP-as-audio), path traversal in archives (including URL-encoded), session fixation (cycle_id on login), timing-based user enumeration (dummy hash on all 3 login paths), brute force login (lockout after 5, 15-min), X-Forwarded-For spoofing (Cloudflare-aware), session reuse after password change (delete_other_sessions), OAuth code replay (atomic consume), PKCE downgrade (plain rejected), token prediction (CSPRNG), CSRF on state-changing endpoints, SSH command injection (reconstructed from validated components), passkey cloning (counter), TOTP replay (last_used_step), IDOR on passkeys/sessions, archive bombs (byte counting + 10x multiplier).
281 315
282 316 ### Performance (12 vectors survived)
283 317 Connection pool exhaustion (bounded at 25), file scanning memory (semaphore of 4), SSE connection accumulation (bounded at 10/user), scheduler job accumulation (advisory lock), background task leaks (monitored), ZIP bombs (byte counting), bulk operations (batch queries, 100-item cap), rate limiter bypass (Cloudflare-aware IP), concurrent scan memory pressure, large file handling (streamed uploads via presigned URLs), global lock contention (DashMap, no Mutex/RwLock), graceful shutdown (10s drain).
@@ -286,73 +320,75 @@ Connection pool exhaustion (bounded at 25), file scanning memory (semaphore of 4
286 320
287 321 | Axis | Confidence | Notes |
288 322 |------|-----------|-------|
289 - | Payments | HIGH | One SQL column name bug in preflight. Core payment logic (webhooks, checkout, refunds) excellent. |
290 - | Storage | HIGH | Project deletion S3 orphan gap. All other paths use durable deletion queue correctly. |
291 - | UX Wiring | HIGH | CSRF excellent. Templates safe. Minor JS and formatting polish only. |
292 - | Security | HIGH | No auth bypasses. DUMMY_HASH on all login paths. Fail-closed scanning. Zero SERIOUS findings. |
293 - | Performance | HIGH (current scale) | Export memory buffering is the main concern. Pool, rate limiting, and concurrency are solid. |
323 + | Payments | HIGH | Core payment logic excellent. All Run 25 bugs fixed. Bundles, tips, license keys DB modules need unit tests. |
324 + | Storage | HIGH | Project deletion S3 orphan gap fixed. Image atomicity carried but low-severity. Scanning pipeline rock-solid. |
325 + | UX Wiring | HIGH | CSRF excellent. Templates safe. Embed URL escaping is defense-in-depth gap, not exploitable. |
326 + | Security | HIGH (95%) | No vulnerabilities found. 17 attack vectors survived. All security claims in platform overview verified true. |
327 + | Performance | HIGH (current scale) | Pool and rate limiting solid. Discover page query fan-out is the scalability cliff but CDN mitigates. |
294 328
295 329 ## Metrics
296 330
297 - - Modules audited: 60+
298 - - Total cold spots: 5
299 - - Bugs by severity: 0 critical, 3 serious, 8 minor, 20 note
331 + - Modules audited: 65+
332 + - Total cold spots: 5 (+ 6 DB modules at B- for test coverage)
333 + - Bugs by severity: 0 critical, 0 serious, 4 minor, 15 note
300 334 - Axes at A or above: 5/5
301 335
302 336 ## Axis Summary Grades
303 337
304 338 | Axis | Overall | Cold Spots | Mandatory Surprise |
305 339 |------|---------|------------|-------------------|
306 - | Payments | A | db/cart.rs (B+) | Pending refund queue with FOR UPDATE SKIP LOCKED (good) |
307 - | Storage | A | projects.rs delete (B-), images.rs (B+) | Durable S3 deletion queue as transactional outbox (good) |
308 - | UX Wiring | A | purchase.html (B+) | json_escape with HTML entity protection (good) |
309 - | Security | A | None | Anti-timing dummy hash on ALL login paths (good) |
310 - | Performance | A | exports/content.rs (B+) | Discover page 5-way parallel facet queries (good) |
340 + | Payments | A | None (cart.rs fixed) | Pending refund queue + webhook dedup 503 (good) |
341 + | Storage | A | images.rs (B+) | Durable pending_uploads/pending_s3_deletions system (good) |
342 + | UX Wiring | A | embed/ (B+), purchase.html (B+) | CSRF implementation textbook-perfect (good) |
343 + | Security | A | None | Archive scanning byte-by-byte decompression (good) |
344 + | Performance | A | None | Content export ZIP disk-backed architecture (good) |
311 345
312 346 ## Recommended Priority Order
313 347
314 - 1. **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id` (1 line)
315 - 2. **[SERIOUS]** Add S3 cleanup to project deletion -- collect S3 keys, enqueue to `pending_s3_deletions`, decrement storage (moderate effort)
316 - 3. **[SERIOUS]** Stream content export ZIP to S3 via multipart upload instead of in-memory buffer (medium effort)
317 - 4. **[MINOR]** Consolidate item cover image update into single UPDATE (trivial)
318 - 5. **[MINOR]** Use `try_replace_storage` for project image replace (low effort)
319 - 6. **[MINOR]** Enqueue old project image to `pending_s3_deletions` (trivial)
320 - 7. **[MINOR]** Add error status check to purchase.html cart JS (trivial)
321 - 8. **[MINOR]** Fix `format_price` negative formatting to match `format_revenue` (trivial)
322 - 9. **[MINOR]** Batch-load collection items in export to eliminate N+1 (low effort)
323 - 10. **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock` (trivial)
324 - 11. **[DEFERRED]** Stream build artifacts to S3 via multipart upload
325 - 12. **[DEFERRED]** Extract shared `validate_promo_code()` helper to prevent checkout path divergence
348 + 1. **[COVERAGE]** Add tests for 8 untested advertised features (bundles, streaming, mailing lists, followers, custom domains, widgets, export, discover) -- highest risk-to-effort ratio
349 + 2. **[CONTENT]** Fix platform-overview.html comparison table math ($500/$1000 rows off by $10, clarify Stripe fee model)
350 + 3. **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
351 + 4. **[MINOR]** Use `try_replace_storage` for project image replace + enqueue old S3 key
352 + 5. **[MINOR]** Apply `html_escape()` to URLs in embed HTML
353 + 6. **[MINOR]** Fix broadcast validation to use `.chars().count()` instead of `.len()`
354 + 7. **[NOTE]** Add HTTP error check to purchase.html cart JS
355 + 8. **[NOTE]** Use `Slug::new()` instead of `Slug::from_trusted()` in blog changelog handler
356 + 9. **[NOTE]** Batch-load collection items in export handler
357 + 10. **[DEFERRED]** Stream build artifacts to S3 via multipart upload
358 + 11. **[DEFERRED]** Extract shared `validate_promo_code()` helper (carried from Run 24)
326 359
327 360 ## Action Items
328 361
329 - ### Run 25 (2026-05-11)
330 -
331 - 103. **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id`
332 - 104. **[SERIOUS]** Add S3 cleanup + storage decrement to project deletion path
333 - 105. **[SERIOUS]** Stream content export ZIP to S3 instead of in-memory buffer
334 - 106. **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
335 - 107. **[MINOR]** Use `try_replace_storage` for project image replace (`routes/storage/images.rs:173-185`)
336 - 108. **[MINOR]** Enqueue old project image S3 key to `pending_s3_deletions`
362 + ### Run 26 (2026-05-11)
363 +
364 + 115. **[COVERAGE]** Add unit tests for bundles (db/bundles.rs, routes/api/items/bundles.rs)
365 + 116. **[COVERAGE]** Add unit tests for audio/video streaming (routes/storage/media.rs, db/media_files.rs)
366 + 117. **[COVERAGE]** Add unit tests for mailing lists/broadcasts (db/mailing_lists.rs, routes/api/users/broadcast.rs)
367 + 118. **[COVERAGE]** Add unit tests for followers (db/follows.rs, routes/api/follows.rs)
368 + 119. **[COVERAGE]** Add unit tests for custom domains (db/custom_domains.rs, routes/custom_domain.rs)
369 + 120. **[COVERAGE]** Add unit tests for embeddable widgets (routes/embed/)
370 + 121. **[COVERAGE]** Add unit tests for data export (routes/api/exports/)
371 + 122. **[COVERAGE]** Add unit tests for discover/search (db/discover.rs)
372 + 123. **[CONTENT]** Fix platform-overview.html comparison table math and clarify Stripe fee model
373 + 124. **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
374 + 125. **[MINOR]** Use `try_replace_storage` for project image replace + enqueue old S3 key to `pending_s3_deletions`
375 + 126. **[MINOR]** Apply `html_escape()` to URLs in embed HTML (`routes/embed/item.rs`)
376 + 127. **[MINOR]** Fix broadcast validation `.len()` -> `.chars().count()` (`routes/api/users/broadcast.rs:46,53`)
377 +
378 + ### Carried from Run 25 (still open)
379 +
380 + 106. **[MINOR]** Consolidate item cover image into single UPDATE -- same as 124 above
381 + 107. **[MINOR]** Use `try_replace_storage` for project image replace -- same as 125 above
382 + 108. **[MINOR]** Enqueue old project image S3 key to `pending_s3_deletions` -- same as 125 above
337 383 109. **[MINOR]** Add HTTP error check to purchase.html cart add-to-cart JS
338 384 110. **[MINOR]** Fix `format_price` negative formatting to use `-$X.XX`
339 385 111. **[MINOR]** Batch-load collection items in export handler (`exports/mod.rs:268-270`)
340 386 112. **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock`
341 - 113. **[DEFERRED]** Stream build artifacts to S3 via multipart upload
342 - 114. **[DEFERRED]** Extract shared `validate_promo_code()` helper
343 -
344 - ### Run 24 (2026-05-09) -- All Fixed
345 -
346 - 97. ~~**[SERIOUS]** Add `starts_at` validation to guest checkout~~ -- **Fixed.**
347 - 98. ~~**[SERIOUS]** Fix version replace storage counter rollback~~ -- **Fixed.**
348 - 99. ~~**[MINOR]** Change pending_uploads ON CONFLICT to DO NOTHING~~ -- **Fixed.**
349 - 100. ~~**[MINOR]** Blog editor: preserve form input on validation error~~ -- **False positive.**
350 - 101. ~~**[MINOR]** Idempotency middleware: avoid double allocation~~ -- **Fixed.**
351 - 102. **[DEFERRED]** Extract shared `validate_promo_code()` helper -- carried to Run 25 item 114.
352 387
353 - ### Run 23 (2026-05-09) -- All Fixed
388 + ### Deferred
354 389
355 - 88-96. All 9 items verified fixed. See Run 24 verification table.
390 + 113. DEFERRED: Stream build artifacts to S3 via multipart upload (carried from Run 25)
391 + 114. DEFERRED: Extract shared `validate_promo_code()` helper (carried from Run 24 -- **CHRONIC** at Run 26, 3 consecutive runs)
356 392
357 393 ### Open (blocked on upstream)
358 394
@@ -361,62 +397,62 @@ Connection pool exhaustion (bounded at 25), file scanning memory (semaphore of 4
361 397 25. Monitor aws-sdk-s3 for rustls-webpki 0.101.7 fix (RUSTSEC-2026-0049)
362 398 33. bincode unmaintained (RUSTSEC-2025-0141) -- upstream via syntect/yara-x, warning only
363 399
364 - ## Previous Action Item Verification (Run 24)
400 + ## Previous Action Item Verification (Run 25)
365 401
366 402 | # | Item | Status |
367 403 |---|------|--------|
368 - | 97 | Add `starts_at` validation to guest checkout | **Fixed** (verified) |
369 - | 98 | Fix version replace storage counter rollback | **Fixed** (verified: uses `try_replace_storage`) |
370 - | 99 | Change pending_uploads ON CONFLICT to DO NOTHING | **Fixed** (verified) |
371 - | 100 | Blog editor: preserve form input | **False positive** (JS fetch preserves state) |
372 - | 101 | Idempotency middleware double allocation | **Fixed** (verified) |
373 - | 102 | Extract shared validate_promo_code() | **Deferred** -- carried to Run 25 |
374 -
375 - 5 of 5 actionable Run 24 items verified fixed. 1 deferred item carried forward.
404 + | 103 | Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id` | **Fixed** (verified: `WHERE t.buyer_id = $2`) |
405 + | 104 | Add S3 cleanup + storage decrement to project deletion | **Fixed** (verified: collects S3 keys, enqueues to pending_s3_deletions, decrements storage) |
406 + | 105 | Stream content export ZIP to S3 instead of in-memory buffer | **Fixed** (verified: writes to temp file on disk via `tempfile::tempdir()`) |
Lines truncated
@@ -1,290 +0,0 @@
1 - # Server Code Flaws (Fuzz Audit 2026-04-24)
2 -
3 - Automated adversarial code review of the MNW server. Each flaw confirmed by reading the source.
4 -
5 - All flaws fixed (except #6, false positive). 17 of 18 addressed.
6 -
7 - ---
8 -
9 - ## Critical
10 -
11 - ### 1. confirm_upload accepts arbitrary S3 key -- FIXED
12 -
13 - **Location:** `src/routes/storage/uploads.rs`, `versions.rs`, `media.rs`, `content_insertions.rs`
14 -
15 - `confirm_upload` takes an `s3_key` from the client and only checks that the object exists in S3 and that the user owns the `item_id`. It never validates the key matches what `presign_upload` generated. A user can pass another user's S3 key (discoverable from public CDN URLs) to attach someone else's file to their own item.
16 -
17 - **Fix:** Added `user_id/item_id` prefix validation to all four confirm endpoints.
18 -
19 - ---
20 -
21 - ## Serious
22 -
23 - ### 2. Unicode homograph attack on usernames and slugs -- FIXED
24 -
25 - **Location:** `src/validation/users.rs:88`, `src/validation/mod.rs:84,133`, `src/validation/items.rs:72`, `src/routes/auth.rs:316`
26 -
27 - `char::is_alphanumeric()` accepts Unicode letters (Cyrillic, Greek, etc.), not just ASCII. A user can register `alicе` (Cyrillic е) to impersonate `alice`. Same for project slugs — visually identical URLs.
28 -
29 - **Fix:** Changed to `is_ascii_alphanumeric()` in all 5 call sites.
30 -
31 - ### 3. Bundle refund does not revoke child item access -- FIXED
32 -
33 - **Location:** `src/routes/stripe/webhook/billing.rs`, `src/routes/stripe/checkout/item.rs`, `src/db/transactions.rs`
34 -
35 - When a bundle is refunded, the parent transaction is marked `refunded`, but child items were granted as separate `$0 completed` transactions via `claim_free_item`. These have no link to the parent and remain `completed`.
36 -
37 - **Fix:** Added `parent_transaction_id` column (migration 061), passed it through `ClaimParams`/`grant_bundle_items`, and added `revoke_child_transactions` call in the refund webhook handler.
38 -
39 - ### 4. i32 overflow in tip amount calculation -- FIXED
40 -
41 - **Location:** `src/routes/stripe/checkout/tips.rs:51`
42 -
43 - `amount_dollars * 100` can overflow `i32` in release builds, creating a checkout with a wrong amount. No maximum tip amount enforced server-side.
44 -
45 - **Fix:** Added $1 min / $10,000 max bounds check before multiplication.
46 -
47 - ### 5. Project member split race condition (TOCTOU) -- FIXED
48 -
49 - **Location:** `src/db/project_members.rs:25-31, 110-143`
50 -
51 - `add_project_member` reads the current total split then inserts. Two concurrent requests can both pass the `<= 100%` check and commit, totaling >100%.
52 -
53 - **Fix:** Wrapped both functions in transactions with `SELECT ... FOR UPDATE` locking.
54 -
55 - ---
56 -
57 - ## Moderate
58 -
59 - ### 6. 30-second session revocation window -- FALSE POSITIVE
60 -
61 - **Location:** `src/auth.rs:109-147`
62 -
63 - Session validation is cached for 30 seconds. However, all revocation paths (revoke_session, revoke_other_sessions, logout, password change) already evict from `session_cache`. No code path deletes sessions without clearing the cache.
64 -
65 - ### 7. SSE connection exhaustion -- FIXED
66 -
67 - **Location:** `src/routes/synckit/subscribe.rs`
68 -
69 - Rate limiting throttles new connection attempts but not concurrent open connections. A malicious user can hold many SSE connections open.
70 -
71 - **Fix:** Added per-user atomic connection counter in `AppState.sse_connections` with `SseConnectionGuard` drop guard. Max 10 concurrent SSE connections per user.
72 -
73 - ### 8. Backup codes not created in a transaction -- FIXED
74 -
75 - **Location:** `src/db/totp.rs:73-96`
76 -
77 - DELETE of old backup codes and INSERT loop for new ones are not in a transaction. A crash between them leaves the user with zero backup codes.
78 -
79 - **Fix:** Wrapped in `pool.begin()` / `tx.commit()`.
80 -
81 - ### 9. Storage increment runs after DB writes -- FIXED
82 -
83 - **Location:** `src/routes/storage/uploads.rs`, `versions.rs`, `media.rs`, `content_insertions.rs`
84 -
85 - `try_increment_storage` runs after the item's `s3_key` is written. If the increment fails (quota exceeded), the item references a file that doesn't count toward quota.
86 -
87 - **Fix:** Moved `try_increment_storage` before DB writes in all four confirm endpoints.
88 -
89 - ---
90 -
91 - ## Minor
92 -
93 - ### 10. constant_time_compare leaks string length -- FIXED
94 -
95 - **Location:** `src/helpers.rs:56-58`
96 -
97 - Early return on length mismatch. Low impact since token lengths are fixed, but technically not constant-time.
98 -
99 - **Fix:** Hash both inputs with SHA-256 before XOR comparison, eliminating the length leak.
100 -
101 - ### 11. Login timing leaks user existence -- FIXED
102 -
103 - **Location:** `src/routes/auth.rs:82-104`
104 -
105 - "User not found" returns immediately without Argon2 work (~100ms faster). Rate limiter partially masks this.
106 -
107 - **Fix:** Added dummy Argon2 verification (against a pre-computed hash) when user is not found.
108 -
109 - ### 12. Lockout race allows ~9 attempts instead of 5 -- FIXED
110 -
111 - **Location:** `src/routes/auth.rs`, `src/routes/oauth.rs`, `src/routes/synckit/auth.rs`, `src/db/auth.rs`
112 -
113 - Concurrent burst requests could each pass the lockout check before either incremented.
114 -
115 - **Fix:** Combined increment + conditional lock into a single atomic SQL UPDATE. The `locked_until` is set in the same statement that increments `failed_login_attempts`, so PostgreSQL row-level locking serializes concurrent callers. Removed the separate `lock_account` function. Updated all three call sites (login, OAuth, SyncKit).
116 -
117 - ### 13. Tip refunds not handled -- FIXED
118 -
119 - **Location:** `src/routes/stripe/webhook/billing.rs`, `src/db/tips.rs`
120 -
121 - `charge.refunded` webhook looks up `transactions` only; tips in their separate table are not updated.
122 -
123 - **Fix:** Added `refund_tip_by_payment_intent` in `db/tips.rs` and fallback lookup in the refund webhook handler.
124 -
125 - ### 14. OTA version ordering uses pub_date not semver -- FIXED
126 -
127 - **Location:** `src/db/ota.rs:84`
128 -
129 - Publishing a backport (lower version, newer date) makes it the "latest," hiding the actual newest version.
130 -
131 - **Fix:** Changed ORDER BY to use `(string_to_array(split_part(version, '-', 1), '.'))::int[] DESC` for semver sorting, with pub_date as tiebreaker.
132 -
133 - ### 15. Missing ::BIGINT casts on SUM in analytics.rs -- FIXED
134 -
135 - **Location:** `src/db/analytics.rs`, `src/db/transactions.rs`
136 -
137 - Works now due to `SUM(INT)->BIGINT` promotion, but inconsistent with casts used elsewhere.
138 -
139 - **Fix:** Added `::BIGINT` casts to all 19 SUM queries across both files.
140 -
141 - ### 16. N+1 query loops -- FIXED
142 -
143 - - `src/db/totp.rs` — 10 INSERTs for backup codes
144 - - `src/db/project_members.rs` — split INSERTs
145 - - `src/db/license_keys.rs` — deactivation loop
146 -
147 - **Fix:** Batched with UNNEST (totp, splits) and ANY (license deactivations).
148 -
149 - ### 17. Unbounded admin queries -- PARTIALLY FIXED
150 -
151 - - `src/db/users.rs:363-376` — `get_pending_appeals`
152 - - `src/db/projects.rs:252-259` — `get_projects_without_mt_community`
153 - - `src/db/users.rs:449-457` — `get_all_user_emails` (intentionally unbounded for bulk notifications)
154 -
155 - **Fix:** Added LIMIT 500 to get_pending_appeals and get_projects_without_mt_community. Left get_all_user_emails unbounded per its documented purpose (shutdown notices).
156 -
157 - ### 18. DB functions lack ownership checks -- FIXED
158 -
159 - `db/projects.rs` and `db/items.rs` mutation functions took only an entity ID with no ownership filter.
160 -
161 - **Fix:** Added `user_id` parameter to all high-risk mutation functions:
162 - - **Projects (5 functions):** `update_project`, `set_project_category`, `delete_project`, `update_project_image_url`, `update_project_pricing` — added `AND user_id = $N` to WHERE.
163 - - **Items (10 functions):** `update_item`, `delete_item`, `update_item_text`, `update_item_license_settings`, `update_item_license_text`, `move_item`, `bulk_publish`, `bulk_unpublish`, `bulk_delete`, `duplicate_item` — added `AND project_id IN (SELECT id FROM projects WHERE user_id = $N)` to WHERE.
164 - - Updated ~30 call sites across routes/api, routes/pages/dashboard/wizards, and routes/api/internal.
165 - - Skipped benign operations: `bump_cache_generation` (24+ callers, no data exposure), `increment/decrement_sales_count` (webhook-only), `increment_play_count`/`increment_download_count` (public counters), scheduler operations.
166 -
167 - ---
168 -
169 - # Fuzz Audit Round 2 (2026-04-24)
170 -
171 - Adversarial code fuzzing of the MNW server. Five parallel agents attacked payment processing, auth/sessions, file uploads, SyncKit/custom domains, and validation/DB queries.
172 -
173 - ## Critical
174 -
175 - ### 19. Partial refunds treated as full refunds
176 - **Location:** `src/routes/stripe/webhook/billing.rs:212-263`, `src/payments/webhooks.rs:119-129`
177 -
178 - `extract_charge_refunded` discards the `Charge` object, losing `amount_refunded`. The handler unconditionally marks the transaction as `refunded`, decrements `sales_count`, and revokes all license keys and bundle children. Stripe fires `charge.refunded` for both partial and full refunds. A $2 partial refund on a $10 purchase fully revokes access.
179 -
180 - ### 20. Webhook complete_transaction failure silently dropped
181 - **Location:** `src/routes/stripe/webhook/checkout.rs:93-96`
182 -
183 - When `complete_transaction` fails (e.g. transient DB error), the error is logged as a warning but `Ok(())` is returned. The outer webhook handler believes the event succeeded and does not queue a retry. The buyer paid but never gets access.
184 -
185 - ## Serious
186 -
187 - ### 21. Project slug collision on public routes
188 - **Location:** `src/db/projects.rs:209-221`
189 -
190 - `get_public_project_by_slug()` queries `WHERE slug = $1 AND is_public = true LIMIT 1` without user scoping. The UNIQUE constraint is `(user_id, slug)`, not global. Two users with the same project slug produce a nondeterministic result on `/p/{slug}` routes.
191 -
192 - ### 22. Storage counter leaked on rejected file type confirm
193 - **Location:** `src/routes/storage/uploads.rs:164-198`
194 -
195 - `try_increment_storage` runs before the `match file_type` block that rejects Download/Insertion types. Storage counter is incremented, then the request errors out. Never decremented. Orphaned S3 objects accumulate.
196 -
197 - ### 23. Unlimited storage via cover/media image uploads
198 - **Location:** `src/db/creator_tiers.rs:517-518`
199 -
200 - `check_upload_allowed` returns `i64::MAX` for Cover/MediaImage. The `try_increment_storage` WHERE clause always passes. No quota enforcement on these file types.
201 -
202 - ### 24. File replacement doesn't decrement old file storage
203 - **Location:** `src/routes/storage/uploads.rs:164-174`
204 -
205 - Uploading a new audio/cover/video increments the storage counter but never decrements the old file's size or deletes the old S3 object. Counter drifts upward on every replacement.
206 -
207 - ### 25. object_size None treated as 0 bytes
208 - **Location:** `src/routes/storage/uploads.rs:136`, `src/routes/storage/versions.rs:130`, `src/routes/storage/media.rs:210`
209 -
210 - `s3.object_size(&key).await?.unwrap_or(0)` means S3 eventual consistency or transient errors record the file as 0 bytes. File passes size checks and consumes no quota but is served at full size from CDN.
211 -
212 - ### 26. SyncKit auth timing oracle for user enumeration
213 - **Location:** `src/routes/synckit/auth.rs:42-44`
214 -
215 - Unlike the web login (which uses `DUMMY_HASH`), the SyncKit auth endpoint returns immediately on user-not-found. Response time difference reveals whether an email is registered.
216 -
217 - ### 27. 2FA has no per-account attempt limit
218 - **Location:** `src/routes/pages/public/two_factor.rs:57-101`
219 -
220 - Per-IP rate limiting exists, but no account-level counter. `pending_2fa_user_id` persists for the full session lifetime. Distributed attackers bypass IP limits. 6-digit TOTP codes have only 1M possibilities.
221 -
222 - ### 28. Blob confirm TOCTOU — duplicate insert causes 500
223 - **Location:** `src/routes/synckit/blobs.rs:110-128`
224 -
225 - `get_sync_blob_by_hash` then `create_sync_blob` is not atomic. Two concurrent confirms for the same hash both pass the check; second insert hits UNIQUE constraint.
226 -
227 - ### 29. Blob confirm missing size_bytes validation
228 - **Location:** `src/routes/synckit/blobs.rs:88-131`
229 -
230 - Upload endpoint validates `size_bytes`, but confirm endpoint stores whatever the client sends (including 0, negative, or i64::MAX).
231 -
232 - ### 30. No device registration limit in SyncKit
233 - **Location:** `src/routes/synckit/sync.rs:197-218`
234 -
235 - Unlimited devices per (app, user) with distinct names. `get_sync_devices` has `LIMIT 100`, making excess devices invisible but still functional.
236 -
237 - ## Minor
238 -
239 - ### 31. PWYW has no maximum amount
240 - **Location:** `src/pricing.rs:186-195`
241 -
242 - Items/projects accept `amount_cents` up to i32::MAX ($21.4M). Tips correctly cap at $10K.
243 -
244 - ### 32. Revenue split truncation loses cents
245 - **Location:** `src/routes/stripe/webhook/checkout.rs:536-542`
246 -
247 - Integer division `amount * percent / 100` truncates. With 3 members at 33%, 12 cents per $9.99 sale disappear from accounting records.
248 -
249 - ### 33. estimate_stripe_fee negative for sub-31-cent items
250 - **Location:** `src/helpers.rs:269-276`
251 -
252 - Fee of 30 cents exceeds price; `creator_receives` goes negative. Display-only.
253 -
254 - ### 34. No password length cap on login paths
255 - **Location:** `src/routes/auth.rs:128`, `src/routes/synckit/auth.rs:65`
256 -
257 - Signup caps at 128 chars, login doesn't. Multi-MB passwords cause unnecessary Argon2 work.
258 -
259 - ### 35. Session not cycled before storing pending_2fa_user_id
260 - **Location:** `src/routes/auth.rs:172-189`
261 -
262 - Session fixation window during 2FA flow (pending state alone doesn't grant access).
263 -
264 - ### 36. slugify() allows non-ASCII that validate_slug() rejects
265 - **Location:** `src/helpers.rs:79`
266 -
267 - `is_alphanumeric()` accepts Unicode, `validate_slug()` requires ASCII. `Slug::from_trusted()` bypasses validation.
268 -
269 - ### 37. Slug::from_trusted on untrusted URL path params
270 - 23 occurrences across 12 files. Not exploitable (parameterized queries) but violates type invariant.
271 -
272 - ### 38. Deactivated SyncKit app usable for JWT lifetime
273 - **Location:** `src/routes/synckit/sync.rs:29-98`
274 -
275 - `is_active` checked at token issuance, not on each request. 7-day window.
276 -
277 - ### 39. SSE sync_notify entries never cleaned up
278 - **Location:** `src/routes/synckit/subscribe.rs:106-111`
279 -
280 - Slow memory leak proportional to distinct users who have ever used SSE.
281 -
282 - ### 40. ON CONFLICT DO NOTHING without conflict target
283 - **Location:** `src/routes/stripe/checkout/project.rs:89-104`
284 -
285 - Any unique violation silently swallowed, not just duplicate purchases.
286 -
287 - ### 41. Content-type check passes on unrecognized magic bytes
288 - **Location:** `src/scanning/content_type.rs:212-243`
289 -
290 - Small HTML files could bypass infer detection. Potential stored XSS depending on CDN config.
@@ -1,126 +0,0 @@
1 - # MNW Server — Code Review
2 -
3 - **Date:** 2026-04-12
4 - **Version:** 0.3.22
5 - **Reviewer:** Claude (Opus 4.6)
6 - **Scope:** Core MNW server crate — all Rust source, routes, auth, payments, scanning, sync, DB, tests. Excludes nested ecosystem projects (multithreaded/, pom/, mnw-cli/).
7 -
8 - ## Summary
9 -
10 - MNW is an Axum 0.8-based creator platform backend (~66,600 LOC Rust, 237 source files) serving the makenot.work marketplace. Handles user auth (password + WebAuthn passkeys + OAuth), Stripe Connect payments, file scanning (6-layer pipeline), SyncKit device sync, git hosting, blog/docs rendering, and a full admin CLI. PostgreSQL via compile-time-checked sqlx. 57 migrations. 1,268 tests (660 unit + 606 integration).
11 -
12 - **Overall: A** — exceptional security posture, comprehensive test coverage, clean architecture. 13 dependency vulnerabilities are all transitive via yara-x (blocked on upstream fix). All code-level findings resolved.
13 -
14 - ---
15 -
16 - ## Findings
17 -
18 - ### [HIGH] 13 cargo audit vulnerabilities via yara-x transitive dependencies
19 -
20 - yara-x 1.14.0 pulls wasmtime 40.0.4, which has 11 CVEs (all published 2026-04-09):
21 - - **2 critical (severity 9.0):** RUSTSEC-2026-0096 (sandbox escape on aarch64 Cranelift), RUSTSEC-2026-0095 (Winch sandbox escape)
22 - - **7 medium (4.1–6.9):** OOB reads/writes in string transcoding, segfault on f64x2.splat, table.grow/fill issues, flags lifting panic
23 - - **2 low (2.3):** pooling allocator data leakage, Winch host data leakage
24 -
25 - Additionally: 1 intaglio advisory (RUSTSEC-2026-0078, symbol confusion, fix available at >=1.13.3) and 1 rsa advisory (RUSTSEC-2023-0071, Marvin Attack, severity 5.9, no fix available — transitive via yara-x + sqlx-mysql).
26 -
27 - All wasmtime CVEs require wasmtime >=42.0.2. Since wasmtime is a transitive dep of yara-x, the fix is blocked on yara-x releasing a version with wasmtime 42+. **Mitigating factor:** YARA rules are developer-controlled, not user-supplied, significantly reducing attack surface. No sandbox escape is reachable from user input.
28 -
29 - 7 additional allowed warnings (bincode unmaintained x2, instant unmaintained, lru unsound IterMut, rand unsound with custom logger x3) — all transitive, theoretical-only impact.
30 -
31 - ### ~~[MEDIUM] ServiceAuth uses non-constant-time comparison (auth.rs:225)~~ — Done
32 -
33 - Fixed: now uses `constant_time_compare()`.
34 -
35 - ### ~~[MEDIUM] 19 clippy warnings~~ — Done
36 -
37 - 19 collapsible_if warnings fixed using Rust 2024 let chains across 12 source files. 1 additional `too_many_arguments` suppressed. 1 `while let` → `for` loop conversion. 1 pre-existing load test compilation error fixed (missing `sync_notify` field). 0 warnings remaining.
38 -
39 - ### [INFO] helpers.rs (767 lines) and pricing.rs (705 lines) — within guideline
40 -
41 - Initially flagged as over 500 lines, but on closer inspection: helpers.rs has ~387 lines of branching logic + 380 lines of tests; pricing.rs has ~298 lines of branching logic + 405 lines of tests. Both are within the 500-line branching guideline. No split needed.
42 -
43 - ### [LOW] scheduler.rs at 635 lines
44 -
45 - Background job scheduler with multiple task types. Borderline — some length is flat task definitions. Monitor for growth.
46 -
47 - ### [LOW] bin/mnw-admin.rs at 1,240 lines
48 -
49 - Standalone admin CLI binary. Contains 15+ subcommand handlers plus SSH git-auth management commands. Each handler is independent (flat dispatch). Exempt as a CLI tool with flat command dispatch.
50 -
51 - ### [INFO] Several route files approaching 500-line limit
52 -
53 - - routes/api/license_keys.rs (684) — route handlers + validation
54 - - routes/pages/dashboard/tabs/user.rs (654) — dashboard UI
55 - - routes/pages/public/health.rs (767) — health check probes (self-contained, exempt)
56 -
57 - ### [INFO] Flat data files are exempt and correctly structured
58 -
59 - Large files that are flat data (no branching logic) and correctly exempt from the 500-line guideline:
60 - - db/models.rs (2,172) — struct definitions
61 - - wordlist.rs (2,056) — static word list
62 - - db/enums.rs (1,286) — enum definitions + impl_str_enum! expansions
63 - - templates/public.rs (954) — Askama template structs
64 - - db/items.rs (924) — SQL queries
65 - - types/mod.rs (908) — type definitions
66 - - storage.rs (786) — ~200 lines flat const data + branching logic
67 - - templates/partials.rs (762) — template structs
68 - - db/users.rs (650), db/creator_tiers.rs (633), db/analytics.rs (612), db/transactions.rs (588) — SQL queries
69 - - types/conversions.rs (580) — From/Into implementations
70 -
71 - ---
72 -
73 - ## Strengths
74 -
75 - - **Zero SQL injection surface.** All 57 migrations and every query use compile-time-checked sqlx macros. No raw SQL string interpolation anywhere in the codebase.
76 - - **6-layer file scanning pipeline.** Content-type verification, structural analysis, ZIP bomb detection, YARA rules, ClamAV, and MalwareBazaar hash check. Fail-closed: any scanner failure blocks the upload.
77 - - **1,268 tests.** 660 unit + 606 integration, including 61 adversarial tests (XSS, path traversal, injection payloads). All tests run against real PostgreSQL (each integration test creates/drops its own database).
78 - - **Comprehensive auth.** Argon2id password hashing (46 MiB, 2 iterations, 1 thread) + WebAuthn passkeys + OAuth (GitHub, Google) + CSRF synchronizer tokens (constant-time) + HMAC-SHA256 email verification with password-hash binding (invalidated on password change).
79 - - **Rate limiting with 5 tiers.** Auth (strict), API (standard), upload (per-user quota), webhook (lenient), public (moderate). Via tower-governor.
80 - - **DashMap session cache.** 30-second TTL, fail-closed (cache miss = DB lookup). No unbounded growth.
81 - - **Stripe Connect Direct Charges.** Webhook signature verification (v1 + v2). Proper idempotency. Creator payouts via Connect.
82 - - **SyncKit device sync.** JWT-authenticated (HS256, 7-day expiry), changelog-based delta sync, E2E encryption (server stores only encrypted blobs).
83 - - **Clean architecture.** Routes, DB, types, templates, and business logic cleanly separated. 121 route files across well-organized directory modules.
84 - - **Graceful degradation.** Scanner failures, payment webhook retries, email delivery failures — all handled without crashing. Errors logged, not silently swallowed.
85 -
86 - ## Security Checklist
87 -
88 - | Check | Status |
89 - |-------|--------|
90 - | SQL injection | Pass — compile-time sqlx, zero raw interpolation |
91 - | XSS | Pass — Askama auto-escaping, CSP headers |
92 - | CSRF | Pass — synchronizer token pattern, constant-time compare |
93 - | Auth bypass | Pass — middleware chain, session validation, suspended user rejection |
94 - | Path traversal | Pass — filename sanitization, no directory traversal |
95 - | Timing attacks | Pass — all secret comparisons use constant_time_compare |
96 - | File upload safety | Pass — 6-layer scanning, fail-closed |
97 - | Secret exposure | Pass — env-loaded (.env), never logged |
98 - | Rate limiting | Pass — 5 tiers, per-route configuration |
99 - | Password storage | Pass — Argon2id (46 MiB, 2 iterations) |
100 - | Webhook verification | Pass — Stripe signature v1+v2, Postmark token check |
101 - | Session management | Pass — DashMap cache with TTL, fail-closed |
102 -
103 - ## Metrics
104 -
105 - | Metric | Value |
106 - |--------|-------|
107 - | Rust source LOC | ~66,600 |
108 - | Source files | 237 |
109 - | Route files | 121 |
110 - | Unit tests | 660 |
111 - | Integration tests | 606 |
112 - | Total tests | 1,266 |
113 - | Clippy warnings | 0 |
114 - | Dependency vulnerabilities | 13 (2 critical, all transitive) |
115 - | Dependency warnings | 7 (all transitive) |
116 - | SQL migrations | 57 |
117 - | API endpoints | ~120 |
118 -
119 - ## Action Items
120 -
121 - 1. **[HIGH]** Upgrade yara-x when a version with wasmtime >=42.0.2 is released (fixes 12 of 13 vulnerabilities) — Deferred to Dependencies in todo.md.
122 - 2. ~~**[MEDIUM]** Fix ServiceAuth to use `constant_time_compare()` in auth.rs~~ — Done.
123 - 3. ~~**[MEDIUM]** Fix clippy warnings using let chains~~ — Done. 0 warnings.
124 - 4. ~~**[MEDIUM]** Split helpers.rs and pricing.rs~~ — Not needed. Both under 500 lines of branching logic.
125 - 5. **[LOW]** Monitor scheduler.rs (635), git/mod.rs (613), license_keys.rs (684) for growth — Deferred.
126 - 6. **[LOW]** Consider splitting bin/mnw-admin.rs git-auth commands into separate module — Deferred.
@@ -1,86 +0,0 @@
1 - # Codesize Efficiency Audit
2 -
3 - **Date:** 2026-04-02
4 - **Scope:** `src/` (production code only, tests excluded)
5 - **Grade:** B+
6 -
7 - ## Summary
8 -
9 - - 61,340 lines across 146 `.rs` files in `src/`
10 - - 12 files exceed 500 lines; 6 are exempt (flat lists), 6 are violations, 3 are borderline
11 - - 3 duplication patterns identified
12 - - No dead code found
13 - - Test suite (23,768 lines, 544 tests) is A+ — well-factored, no issues
14 -
15 - ## Exempt Files (flat lists, correctly large)
16 -
17 - | File | Lines | Why exempt |
18 - |------|-------|-----------|
19 - | `src/wordlist.rs` | 2,056 | Static 2048-word array |
20 - | `src/db/models.rs` | 2,045 | FromRow structs + simple accessors |
21 - | `src/db/enums.rs` | 1,217 | 35 `impl_str_enum!` macro enums |
22 - | `src/validation/` | 1,177 | 60+ linear validation functions (split into directory module) |
23 - | `src/templates/public.rs` | 952 | Askama HTML markup |
24 - | `src/types/mod.rs` | 871 | Type definitions / newtypes |
25 -
26 - ## Violations (branching logic >500 lines)
27 -
28 - | File | Lines | Domains | Split recommendation |
29 - |------|-------|---------|---------------------|
30 - | `src/routes/api/internal.rs` | 1,634 | 10 (SSH, items, blog, promo, licenses, analytics, git auth...) | `internal/` dir with 5-6 submodules |
31 - | ~~`src/git.rs`~~ | 1,176 | Split into `src/git/` (refs.rs, objects.rs, history.rs) |
32 - | ~~`src/payments.rs`~~ | 1,173 | Split into `src/payments/` (checkout.rs, webhooks.rs, connect.rs) |
33 - | ~~`src/routes/storage.rs`~~ | 920 | Split into `src/routes/storage/` dir |
34 - | ~~`src/routes/postmark.rs`~~ | 887 | Split into `src/routes/postmark/` dir |
35 - | `src/routes/pages/dashboard/wizards/item.rs` | 791 | 6 wizard steps | `wizards/item/` dir |
36 - | `src/helpers.rs` | 775 | 8 (slugs, CSV, URLs, dates, crypto, email, cache, forms) | `helpers/` dir with 4 submodules |
37 - | `src/routes/stripe/checkout.rs` | 768 | 4 (forms, promo validation, session, payment intent) | Extract `checkout/promo.rs` |
38 -
39 - ## Borderline (monitor, no action needed)
40 -
41 - | File | Lines | Notes |
42 - |------|-------|-------|
43 - | `src/routes/api/items.rs` | 885 | 6 domains (CRUD, publish, chapters) but each handler is self-contained |
44 - | `src/routes/pages/public/health.rs` | 767 | Health page handlers, low branching complexity |
45 - | `src/storage.rs` | 855 | S3 client operations, cohesive module |
46 - | `src/db/items.rs` | 835 | Item queries, each <100 lines, flat structure |
47 -
48 - ## Duplication Patterns
49 -
50 - ### Ownership verification (8+ sites)
51 -
52 - ```
53 - get_project → check user_id → NotFound/Forbidden
54 - ```
55 -
56 - Repeated across route files for project-scoped endpoints. Could extract a shared `verify_project_ownership(pool, project_id, user_id)` helper.
57 -
58 - ### Slug collision resolution (5+ sites)
59 -
60 - ```
61 - slugify → exists check → append suffix loop
62 - ```
63 -
64 - Appears in project creation, item creation, blog posts, collections, and page creation. Could consolidate into a generic `resolve_unique_slug()` function.
65 -
66 - ### Price formatting (3+ sites)
67 -
68 - Cents-to-display logic (`amount / 100`, decimal formatting) repeated in payment routes, dashboard templates, and email templates.
69 -
70 - ## Dead Code
71 -
72 - None found. All public functions have callers. No unused feature flags. No orphaned modules.
73 -
74 - ## Test Suite
75 -
76 - - 23,768 lines, 544 tests
77 - - Test harness: 1,129 lines (`tests/common/`)
78 - - Well-factored — shared fixtures, clear module boundaries
79 - - Grade: A+ — no splitting needed
80 -
81 - ## Key Paths
82 -
83 - - `src/` — all production code (146 files)
84 - - `tests/` — integration tests
85 - - `src/routes/` — HTTP handlers (largest concentration of branching logic)
86 - - `src/db/` — database queries (mostly flat, exempt)
@@ -1,62 +0,0 @@
1 - # Concurrency Audit
2 -
3 - Audit date: 2026-04-02. Grade: **A**.
4 -
5 - No code fixes needed. Zero concurrency issues found.
6 -
7 - ## Primitives In Use
8 -
9 - | Primitive | Count | Usage |
10 - |-----------|-------|-------|
11 - | `Arc<T>` | 16 fields | All heavy AppState fields (db pool, S3 client, email client, config, etc.) |
12 - | `DashMap` | 2 caches | session_cache (UserSessionId→Instant), domain_cache (String→UserId) |
13 - | `tokio::spawn` | ~20 sites | Fire-and-forget tasks (emails, webhooks, builds) |
14 - | `tokio::sync::watch` | 1 channel | Graceful shutdown broadcast to monitor + scheduler |
15 - | `tokio::select!` | 2 loops | Monitor + scheduler main loops (shutdown + interval) |
16 - | `LazyLock<Regex>` | 3 regexes | Thread-safe compiled regexes |
17 - | `Mutex` / `RwLock` / `unsafe` / `Condvar` | 0 | Not used in production code |
18 -
19 - ## Verified Areas
20 -
21 - ### AppState (lib.rs)
22 - - `#[derive(Clone)]` with all heavy types in `Arc`
23 - - Cheap per-request clone via Axum's `State` extractor
24 -
25 - ### DashMap (16 call sites)
26 - - All operations are single-level get/insert/remove/retain — no nested access (which could deadlock sharded maps)
27 - - Session cache pruned every monitor cycle via `retain()` with TTL check
28 - - Domain cache populated on startup, updated on domain add/remove
29 -
30 - ### Fire-and-Forget Spawns (~20)
31 - - All spawned tasks have internal error handling (`if let Err(e)` + tracing)
32 - - No `.unwrap()` or `.expect()` in any spawned task
33 - - Panic risk negligible
34 -
35 - ### Email Fan-Out (3 paths)
36 - - `send_release_announcements` (scheduler.rs:63-89)
37 - - `send_blog_post_announcements` (scheduler.rs:147-173)
38 - - `broadcast_send` (broadcast.rs:93-109)
39 - - Pattern: spawn one task, iterate sequentially over subscribers
40 - - Sequential sending naturally rate-limits Postmark API calls
41 -
42 - ### Graceful Shutdown (main.rs)
43 - - `tokio::sync::watch` channel created in main, `subscribe()` shared to monitor + scheduler
44 - - Both use `tokio::select!` to check shutdown signal on every loop iteration
45 -
46 - ### HTTP Client Timeouts (mt_client.rs)
47 - - 5s request timeout, 3s connect timeout
48 - - Prevents hung connections from blocking the async runtime
49 -
50 - ### Connection Pool (sqlx PgPool)
51 - - Shared via `Arc` in AppState
52 - - Spawned tasks share the pool; sqlx handles connection limiting internally
53 -
54 - ### Onboarding Scheduler (scheduler.rs:181-238)
55 - - Sequential email sending within the scheduler tick
56 - - Batch operations for skip cases
57 -
58 - ## Intentional Design Decisions
59 -
60 - - **No Sentry** — panics in spawned tasks go to stderr only. Acceptable for private alpha.
61 - - **JoinHandle discarded** — intentional fire-and-forget for emails, webhooks, cache invalidation. All have internal error handling; parent tasks don't need the result.
62 - - **No Semaphore on email fan-out** — sequential sending in a single task is sufficient for current scale. A concurrency limiter would only matter if switching to concurrent per-subscriber sending.
@@ -1,73 +0,0 @@
1 - # Documentation Flaws
2 -
3 - Audit of MNW public docs from the perspective of an anti-capitalist, AI-skeptical artist who needs to sell their work.
4 -
5 - Conducted 2026-04-22. Status updated as fixes were applied.
6 -
7 - ---
8 -
9 - ## Critical
10 -
11 - ### 1. AI content policy — RESOLVED
12 -
13 - Three-tier system (Handmade / Assisted / Generated) with mandatory classification at publish time. Assisted tier requires written disclosure. Fan-side filtering. Generative AI defined by training data ethics (unpaid copyright, undisclosed datasets). Full policy at `about/generative-ai.md`. FAQ, acceptable use, getting-started, items guide, and fan guide all updated to reference it.
14 -
15 - ### 2. No statement on AI in the platform itself — RESOLVED
16 -
17 - New section in `about/generative-ai.md`: no generative AI in the product, discovery is explicit (not ML), security/spam reserves the right to use best tools, platform development is LLM-assisted and disclosed in commit logs. Discovery algorithms linked directly to source files. Founder's personal note included as blockquote.
18 -
19 - ---
20 -
21 - ## High
22 -
23 - ### 3. "Creator" language everywhere — RESOLVED
24 -
25 - Landing page: "Creator tiers" → "Pricing tiers", "Every creator gets" → "Everyone gets". how-we-work.md "Who This Is For" now opens with "Artists, musicians, writers, developers, and makers." Targeted changes in audience-facing prose; kept "creator" in technical contexts (dashboard, API) where it's standard.
26 -
27 - ### 4. $10/month floor unaddressed — RESOLVED
28 -
29 - FAQ "What if I earn nothing" now acknowledges the tension honestly and links to earn-back. how-we-work.md unchanged (already had earn-back details).
30 -
31 - ### 5. "Platforms should be infrastructure, not landlords" buried — RESOLVED
32 -
33 - Added as subtagline on landing page hero, in og:description meta tag, and as closing line of story.md "What This Means in Practice" section. CSS added for `.subtagline`.
34 -
35 - ---
36 -
37 - ## Medium
38 -
39 - ### 6. Bus factor handwave — RESOLVED
40 -
41 - FAQ rewritten: acknowledges the risk directly, lists concrete mitigations (public source, export, shutdown protocol, separate funds), states hiring as top financial priority, calls the long-term goal a goal not a guarantee.
42 -
43 - ### 7. Source available vs open source blurring — RESOLVED
44 -
45 - `tech/open-source.md` now explicitly says "source-available, not open source" with explanation of why PolyForm Noncommercial was chosen. `legal/transparency.md` heading changed from "Open Source Transparency" to "Source-Available Transparency."
46 -
47 - ### 8. Platform economics not shown — RESOLVED
48 -
49 - New public page at `about/economics.md`: cost structure by category, per-creator costs and margins by tier, break-even number (36 creators), where surplus goes (wage, hiring via residency program, reserves, development), what surplus does not fund, the margin question addressed directly, "Why These Prices Won't Go Up" section (no hidden subsidy, margins widen with growth, missing cost centers, hosting trends down). Linked from how-we-work, story, and FAQ.
50 -
51 - ### 9. Cooperative ownership unexplored — RESOLVED (partially)
52 -
53 - FAQ and economics page now have honest framing: hard commitment that the company will never be sold to anyone other than its creator community, honest acknowledgment that the legal structure isn't figured out yet, commitment to figure it out with the community when the time comes. Deliberately not over-promising on co-op structure before legal advice.
54 -
55 - ---
56 -
57 - ## Low
58 -
59 - ### 10. Startup-flavored prose — RESOLVED
60 -
61 - Tonal pass across story.md, guarantees.md, how-we-work.md, faq.md, moderation.md. Pattern: replaced self-praise ("this isn't marketing, it's accountability") with action ("you can check"), replaced declarations of intent ("we believe", "our commitment") with descriptions of how things work, replaced sales language ("no catch") with incentive explanations. SLA intro shortened from three lines to one.
62 -
63 - ### 11. Missing artist stories / social proof — DEFERRED
64 -
65 - No testimonials possible during alpha. Will revisit once there are real creators to feature. Goal is honest stories, not marketing fluff.
66 -
67 - ### 12. Enforcement is one person — RESOLVED
68 -
69 - Moderation page now opens with "Current Limitations" section acknowledging one-person enforcement, linking to SLA planned guarantees for independent appeals, and stating hiring priority. "We'd rather be honest about this than pretend we have a trust and safety team."
70 -
71 - ### 13. Community features buried — RESOLVED
72 -
73 - Multithreaded referenced in: how-we-work.md (new "Community" section), roadmap.md (added to "What's Built"), getting-started.md (added to "Your First Week" checklist). Forums page already existed at `support/forums.md` but was orphaned from the main docs flow.
@@ -0,0 +1,139 @@
1 + # SyncKit Pricing
2 +
3 + E2E encrypted cloud sync and OTA updates for indie apps. Two modes, one billing engine, no surprises.
4 +
5 + ## Principles
6 +
7 + 1. **Easy to understand.** Simple mode requires one decision (how much storage). Builder mode adds one more knob (transfer ratio). That's it.
8 + 2. **Friendly to power users.** Developers who understand their usage pattern can optimize with Builder mode. Developers who don't can stay on Simple forever.
9 + 3. **Priced relative to actual costs.** Base rates are derived from real infrastructure costs with published margins. No opaque markup.
10 + 4. **Always predictable.** The bill is known before the billing period starts. There are no overages. When limits are hit, sync degrades gracefully — it never charges more. Developers always have a chance to say no before paying more.
11 + 5. **Easy to enter and leave.** Apply to get started, export your data at any time, cancel takes effect at end of billing period. Standard formats, no lock-in, no retention games.
12 +
13 + ## Getting Started
14 +
15 + No free tier. Apply for access with a short description of your app and expected usage. Same reasoning as MNW creator subscriptions: a free tier attracts users who don't value the service, creates support burden without revenue, and dilutes the quality of the developer community. The application filters for developers who are serious about shipping.
16 +
17 + Accepted developers get full access immediately. First 14 days are not billed — the trial period starts when the application is approved, not when the first sync happens.
18 +
19 + ## Base Rates
20 +
21 + | Variable | Unit | Price | Infra cost | Margin |
22 + |----------|------|-------|------------|--------|
23 + | Weight | per GB stored per month | $0.15 | ~$0.007/GB (Hetzner Object Storage) | ~95% |
24 + | Burst | per multiplier unit per GB of weight | $0.03 | ~$0.01/GB egress | ~67% |
25 +
26 + Monthly cost = (weight in GB x $0.15) + (burst multiplier x weight in GB x $0.03)
27 +
28 + These margins accommodate moving to more expensive infrastructure (e.g., AWS S3 at $0.023/GB storage, $0.09/GB egress) without changing customer pricing. Weight margin drops to ~85% on AWS; burst margin drops to ~0%. Hetzner is the target infrastructure.
29 +
30 + API requests are included. No per-request charges. Abuse is handled as a ToS issue, not a billing event.
31 +
32 + ## Simple Mode
33 +
34 + The developer chooses **weight** (storage in GB). Burst is set to a platform-default ratio — currently **5x**. The monthly cost is the Builder formula applied to these inputs.
35 +
36 + Simple mode is Builder mode with a fixed burst ratio. There is no markup, no hidden difference in the billing engine. A Simple customer paying for 20 GB of weight at 5x burst pays exactly what a Builder customer would pay for the same configuration.
37 +
38 + ### Example configurations
39 +
40 + | App type | Weight | Burst (fixed 5x) | Monthly cost |
41 + |----------|--------|-------------------|-------------|
42 + | Config/state sync (settings, read state) | 1 GB | 5x | $0.30 |
43 + | Productivity (tasks, contacts, light files) | 10 GB | 5x | $4.50 |
44 + | Media metadata (sample libraries, playlists) | 50 GB | 5x | $22.50 |
45 + | Large file sync (audio, video, courses) | 200 GB | 5x | $60.00 |
46 +
47 + ### Ratio drift
48 +
49 + The default burst ratio may change over time based on aggregate usage data from Simple mode customers. If most apps need 7x burst instead of 5x, the default will be updated.
50 +
51 + When the ratio changes:
52 + - Existing Simple customers stay on their current ratio unless they opt in to the new one
53 + - New Simple customers get the new ratio
54 + - The change is announced with at least 30 days notice
55 + - Any customer can switch to Builder mode at any time to set their own ratio
56 +
57 + ## Builder Mode
58 +
59 + The developer chooses both **weight** (storage in GB) and **burst** (transfer multiplier) independently. Best for launched apps where the developer understands their usage pattern and wants to optimize cost.
60 +
61 + ### How burst works
62 +
63 + Burst is the transfer budget relative to storage. A burst of 5x on 20 GB of weight means 100 GB of monthly transfer included. The developer picks the multiplier based on how their app behaves.
64 +
65 + | App pattern | Suggested burst | Why |
66 + |-------------|----------------|-----|
67 + | Small metadata, few devices, incremental sync | 3-5x | Low transfer relative to storage |
68 + | Light attachments, occasional new device setup | 8-10x | New device pulls full dataset |
69 + | Large libraries, infrequent full syncs | 2-3x | High storage, low transfer ratio |
70 + | Collaborative editing, constant syncing | 10-15x | Low storage, high transfer frequency |
71 +
72 + ### Example configurations
73 +
74 + | App type | Weight | Burst | Monthly cost |
75 + |----------|--------|-------|-------------|
76 + | Feed reader (read state, bookmarks) | 5 GB | 10x | $2.25 |
77 + | Productivity app (tasks, contacts) | 20 GB | 10x | $9.00 |
78 + | Sample manager (metadata + selective blob) | 200 GB | 3x | $48.00 |
79 + | Collaborative editor (small docs, constant sync) | 5 GB | 15x | $3.00 |
80 +
81 + ### Adjusting configuration
82 +
83 + Developers can change their weight and burst settings monthly. The new configuration takes effect at the start of the next billing period. The dashboard shows the projected cost before the developer commits.
84 +
85 + ## What Happens at Limits
86 +
87 + **Storage full:** New uploads are rejected. Existing sync continues. The developer sees a dashboard alert and can increase weight.
88 +
89 + **Transfer budget exhausted:** Sync is deprioritized — changes are queued with backoff between batches. No data loss, just slower sync until the next billing period. The developer can increase burst to restore full-speed sync.
90 +
91 + **In both cases:** The bill does not change. There are no overages. The developer chose a configuration, and that configuration's price is the price. Degraded service is the signal to upgrade, not a surprise invoice.
92 +
93 + ## Per-User vs Pool
94 +
95 + SyncKit bills the **developer**, not individual end users. The developer buys a storage and transfer budget for their entire app. How they allocate that across their users is their business — SyncKit doesn't track or bill per-user.
96 +
97 + A developer with 10,000 users and 10 GB of weight is paying for 10 GB total, not 10 GB per user. If their users collectively need more, the developer increases weight.
98 +
99 + This keeps billing dead simple and predictable. The developer knows their bill before the month starts. They can charge their users whatever they want — flat fee, freemium, usage-based, or nothing. SyncKit doesn't care.
100 +
101 + ## What's Included (Both Modes)
102 +
103 + - E2E encryption (ChaCha20-Poly1305 + Argon2 key derivation)
104 + - Unlimited sync requests (no per-request billing)
105 + - All devices per user (no per-device fees)
106 + - Conflict resolution (last-write-wins, field-level merge)
107 + - Blob storage (within weight allocation)
108 + - OTA update distribution
109 + - Device management
110 + - Dashboard with usage stats, per-user breakdown, budget alerts at 80%
111 + - Data export at any time (standard format)
112 +
113 + ## Data Portability
114 +
115 + SyncKit stores encrypted data the server cannot read. The developer holds the keys.
116 +
117 + Export is available at any time via API or dashboard:
118 + - Full sync log export (encrypted, developer decrypts client-side)
119 + - Device and key metadata
120 + - Usage history and billing records
121 +
122 + Cancellation takes effect at end of billing period. Data is retained for 30 days after cancellation, then permanently deleted. The developer can export during this window.
123 +
124 + ## Future Add-Ons
125 +
126 + These are planned capabilities with real marginal costs, priced separately when they ship:
127 +
128 + - **Realtime sync** (WebSocket/SSE push) — pricing TBD based on connection costs
129 + - **Custom domain** — pricing TBD
130 + - **Priority support** — pricing TBD
131 +
132 + Add-ons will follow the same principles: predictable, cost-relative, no surprises.
133 +
134 + ## See Also
135 +
136 + - [SyncKit architecture](../shared/synckit-client/docs/architecture.md)
137 + - [SyncKit competition analysis](../shared/synckit-client/docs/competition.md)
138 + - [MNW economics](internal/business/economics.md) — SyncKit as MNW add-on
139 + - [Fan+ design](internal/business/fan-plus.md) — consumer subscription context
@@ -416,17 +416,17 @@
416 416 <tbody>
417 417 <tr>
418 418 <td>$500</td>
419 - <td class="num highlight-row">$410&ndash;470</td>
419 + <td class="num highlight-row">$410&ndash;460</td>
420 420 <td class="num">$420</td>
421 421 <td class="num">$395</td>
422 - <td class="num">$15&ndash;75</td>
422 + <td class="num">$15&ndash;65</td>
423 423 </tr>
424 424 <tr>
425 425 <td>$1,000</td>
426 - <td class="num highlight-row">$881&ndash;941</td>
426 + <td class="num highlight-row">$881&ndash;931</td>
427 427 <td class="num">$870</td>
428 428 <td class="num">$820</td>
429 - <td class="num">$61&ndash;121</td>
429 + <td class="num">$61&ndash;111</td>
430 430 </tr>
431 431 <tr>
432 432 <td>$2,000</td>
@@ -452,7 +452,7 @@
452 452 </tbody>
453 453 </table>
454 454
455 - <p style="font-size: 0.82rem; color: #5a524a;">Makenot.work range reflects $10&ndash;$60 tier fee. Competitor columns include Stripe processing (~3%). All figures assume $10 average sale price.</p>
455 + <p style="font-size: 0.82rem; color: #5a524a;">Makenot.work range reflects $10&ndash;$60 tier fee. All columns include Stripe processing (2.9% + $0.30 per transaction). All figures assume $10 average sale price. Higher average sale prices reduce the effective Stripe rate.</p>
456 456 </div>
457 457
458 458 <div class="diamond">.</div>
@@ -1,89 +0,0 @@
1 - # Payload Audit
2 -
3 - **Date:** 2026-04-02
4 - **Scope:** Browser-facing assets served by MNW
5 - **Grade:** A — Lean payload, no bloat
6 -
7 - ## Summary
8 -
9 - - ~170 KB gzipped first load, ~55 KB subsequent pages
10 - - Hand-written CSS + vanilla JS + HTMX only — no framework bloat
11 - - woff2 fonts with preload and `font-display: swap`
12 - - Conditional JS loading per page type
13 - - No large dead asset sections found
14 -
15 - ## Asset Inventory
16 -
17 - ### Production Page Load (gzipped)
18 -
19 - | Category | Uncompressed | Gzipped | Notes |
20 - |----------|-------------|---------|-------|
21 - | CSS (style.css + wizard.css) | 90 KB | ~16 KB | Hand-written, no framework |
22 - | JS custom (mnw, passkey, upload, insertions, wizard) | 27 KB | ~8 KB | Vanilla JS, unminified |
23 - | JS HTMX 2.0.4 | 51 KB | ~16 KB | Local copy, minified |
24 - | Fonts (woff2, 5 faces) | 122 KB | ~115 KB | Already compressed |
25 - | Images (logo + favicon) | 18 KB | ~15 KB | Optimized |
26 - | **Typical first load** | **~308 KB** | **~170 KB** | Fonts cached after first visit |
27 - | **Subsequent pages** | **~186 KB** | **~55 KB** | Fonts + HTMX + CSS cached |
28 -
29 - ### CSS Files
30 -
31 - - `style.css` — main stylesheet, 431 classes
32 - - `wizard.css` — wizard-specific styles, loaded only on wizard pages
33 -
34 - ### JS Files
35 -
36 - | File | Size | Loaded on |
37 - |------|------|-----------|
38 - | `mnw.js` | Main script | All pages |
39 - | `passkey.js` | WebAuthn | `/login` only |
40 - | `upload.js` | File upload | Dashboard pages |
41 - | `insertions.js` | Content editor | Dashboard pages |
42 - | `wizard.js` | Step wizard | Wizard pages only |
43 -
44 - ### Fonts (woff2)
45 -
46 - 5 font faces served via `@font-face` with `font-display: swap`. Critical faces use `<link rel="preload">`.
47 -
48 - TTF fallbacks exist on disk (1.5 MB) but are never served — woff2 takes priority in all modern browsers.
49 -
50 - ## Compression
51 -
52 - Caddy serves all responses with gzip + zstd compression enabled. No additional build-time minification pipeline.
53 -
54 - ## CSS Unused Classes Analysis
55 -
56 - ~362 of 431 classes in style.css appear unused by simple `grep` against templates, but most are accounted for:
57 -
58 - - **JS-created classes** — toast variants, dragover, fade-out, active states (created dynamically)
59 - - **Askama template conditionals** — `class="toast-{{ type }}"` and similar dynamic class construction
60 - - **Git browser classes** — 30+ classes for the repository viewer feature (confirmed active)
61 - - **Use-case + analytics pages** — confirmed used in their respective templates
62 -
63 - True dead CSS is minimal. A browser-based Coverage tool would give exact numbers, but static analysis shows no large dead sections.
64 -
65 - ## Assessed Non-Issues
66 -
67 - | Item | Assessment |
68 - |------|-----------|
69 - | Custom JS unminified | Saves 1-2 KB gzipped if minified. Not worth a build step at this scale. |
70 - | TTF font files on disk | Not served to browsers. Only occupies disk space, not bandwidth. |
71 - | upload.js on non-upload pages | 1.1 KB gzipped. Marginal overhead, not worth conditional loading complexity. |
72 - | HTMX served locally | Correct — avoids CDN dependency, enables offline-first development. |
73 -
74 - ## What's Good
75 -
76 - - **No framework bloat** — no Tailwind, Bootstrap, React, or build toolchain
77 - - **woff2 fonts** with preload + swap — fast text rendering
78 - - **Conditional JS loading** — scripts loaded only where needed
79 - - **HTMX 2.0.4** — current version, minimal footprint
80 - - **Zero unused JS functions** — all exports have documented call sites
81 - - **Caddy compression** — gzip + zstd on all routes, no configuration gaps
82 -
83 - ## Key Paths
84 -
85 - - `static/css/` — stylesheets
86 - - `static/js/` — JavaScript files
87 - - `static/fonts/` — font files (woff2 + ttf)
88 - - `static/images/` — logo + favicon
89 - - `src/templates/` — Askama templates (CSS class consumers)
@@ -1,139 +0,0 @@
1 - # SyncKit Pricing
2 -
3 - E2E encrypted cloud sync and OTA updates for indie apps. Two modes, one billing engine, no surprises.
4 -
5 - ## Principles
6 -
7 - 1. **Easy to understand.** Simple mode requires one decision (how much storage). Builder mode adds one more knob (transfer ratio). That's it.
8 - 2. **Friendly to power users.** Developers who understand their usage pattern can optimize with Builder mode. Developers who don't can stay on Simple forever.
9 - 3. **Priced relative to actual costs.** Base rates are derived from real infrastructure costs with published margins. No opaque markup.
10 - 4. **Always predictable.** The bill is known before the billing period starts. There are no overages. When limits are hit, sync degrades gracefully — it never charges more. Developers always have a chance to say no before paying more.
11 - 5. **Easy to enter and leave.** Apply to get started, export your data at any time, cancel takes effect at end of billing period. Standard formats, no lock-in, no retention games.
12 -
13 - ## Getting Started
14 -
15 - No free tier. Apply for access with a short description of your app and expected usage. Same reasoning as MNW creator subscriptions: a free tier attracts users who don't value the service, creates support burden without revenue, and dilutes the quality of the developer community. The application filters for developers who are serious about shipping.
16 -
17 - Accepted developers get full access immediately. First 14 days are not billed — the trial period starts when the application is approved, not when the first sync happens.
18 -
19 - ## Base Rates
20 -
21 - | Variable | Unit | Price | Infra cost | Margin |
22 - |----------|------|-------|------------|--------|
23 - | Weight | per GB stored per month | $0.15 | ~$0.007/GB (Hetzner Object Storage) | ~95% |
24 - | Burst | per multiplier unit per GB of weight | $0.03 | ~$0.01/GB egress | ~67% |
25 -
26 - Monthly cost = (weight in GB x $0.15) + (burst multiplier x weight in GB x $0.03)
27 -
28 - These margins accommodate moving to more expensive infrastructure (e.g., AWS S3 at $0.023/GB storage, $0.09/GB egress) without changing customer pricing. Weight margin drops to ~85% on AWS; burst margin drops to ~0%. Hetzner is the target infrastructure.
29 -
30 - API requests are included. No per-request charges. Abuse is handled as a ToS issue, not a billing event.
31 -
32 - ## Simple Mode
33 -
34 - The developer chooses **weight** (storage in GB). Burst is set to a platform-default ratio — currently **5x**. The monthly cost is the Builder formula applied to these inputs.
35 -
36 - Simple mode is Builder mode with a fixed burst ratio. There is no markup, no hidden difference in the billing engine. A Simple customer paying for 20 GB of weight at 5x burst pays exactly what a Builder customer would pay for the same configuration.
37 -
38 - ### Example configurations
39 -
40 - | App type | Weight | Burst (fixed 5x) | Monthly cost |
41 - |----------|--------|-------------------|-------------|
42 - | Config/state sync (settings, read state) | 1 GB | 5x | $0.30 |
43 - | Productivity (tasks, contacts, light files) | 10 GB | 5x | $4.50 |
44 - | Media metadata (sample libraries, playlists) | 50 GB | 5x | $22.50 |
45 - | Large file sync (audio, video, courses) | 200 GB | 5x | $60.00 |
46 -
47 - ### Ratio drift
48 -
49 - The default burst ratio may change over time based on aggregate usage data from Simple mode customers. If most apps need 7x burst instead of 5x, the default will be updated.
50 -
51 - When the ratio changes:
52 - - Existing Simple customers stay on their current ratio unless they opt in to the new one
53 - - New Simple customers get the new ratio
54 - - The change is announced with at least 30 days notice
55 - - Any customer can switch to Builder mode at any time to set their own ratio
56 -
57 - ## Builder Mode
58 -
59 - The developer chooses both **weight** (storage in GB) and **burst** (transfer multiplier) independently. Best for launched apps where the developer understands their usage pattern and wants to optimize cost.
60 -
61 - ### How burst works
62 -
63 - Burst is the transfer budget relative to storage. A burst of 5x on 20 GB of weight means 100 GB of monthly transfer included. The developer picks the multiplier based on how their app behaves.
64 -
65 - | App pattern | Suggested burst | Why |
66 - |-------------|----------------|-----|
67 - | Small metadata, few devices, incremental sync | 3-5x | Low transfer relative to storage |
68 - | Light attachments, occasional new device setup | 8-10x | New device pulls full dataset |
69 - | Large libraries, infrequent full syncs | 2-3x | High storage, low transfer ratio |
70 - | Collaborative editing, constant syncing | 10-15x | Low storage, high transfer frequency |
71 -
72 - ### Example configurations
73 -
74 - | App type | Weight | Burst | Monthly cost |
75 - |----------|--------|-------|-------------|
76 - | Feed reader (read state, bookmarks) | 5 GB | 10x | $2.25 |
77 - | Productivity app (tasks, contacts) | 20 GB | 10x | $9.00 |
78 - | Sample manager (metadata + selective blob) | 200 GB | 3x | $48.00 |
79 - | Collaborative editor (small docs, constant sync) | 5 GB | 15x | $3.00 |
80 -
81 - ### Adjusting configuration
82 -
83 - Developers can change their weight and burst settings monthly. The new configuration takes effect at the start of the next billing period. The dashboard shows the projected cost before the developer commits.
84 -
85 - ## What Happens at Limits
86 -
87 - **Storage full:** New uploads are rejected. Existing sync continues. The developer sees a dashboard alert and can increase weight.
88 -
89 - **Transfer budget exhausted:** Sync is deprioritized — changes are queued with backoff between batches. No data loss, just slower sync until the next billing period. The developer can increase burst to restore full-speed sync.
90 -
91 - **In both cases:** The bill does not change. There are no overages. The developer chose a configuration, and that configuration's price is the price. Degraded service is the signal to upgrade, not a surprise invoice.
92 -
93 - ## Per-User vs Pool
94 -
95 - SyncKit bills the **developer**, not individual end users. The developer buys a storage and transfer budget for their entire app. How they allocate that across their users is their business — SyncKit doesn't track or bill per-user.
96 -
97 - A developer with 10,000 users and 10 GB of weight is paying for 10 GB total, not 10 GB per user. If their users collectively need more, the developer increases weight.
98 -
99 - This keeps billing dead simple and predictable. The developer knows their bill before the month starts. They can charge their users whatever they want — flat fee, freemium, usage-based, or nothing. SyncKit doesn't care.
100 -
101 - ## What's Included (Both Modes)
102 -
103 - - E2E encryption (ChaCha20-Poly1305 + Argon2 key derivation)
104 - - Unlimited sync requests (no per-request billing)
105 - - All devices per user (no per-device fees)
106 - - Conflict resolution (last-write-wins, field-level merge)
107 - - Blob storage (within weight allocation)
108 - - OTA update distribution
109 - - Device management
110 - - Dashboard with usage stats, per-user breakdown, budget alerts at 80%
111 - - Data export at any time (standard format)
112 -
113 - ## Data Portability
114 -
115 - SyncKit stores encrypted data the server cannot read. The developer holds the keys.
116 -
117 - Export is available at any time via API or dashboard:
118 - - Full sync log export (encrypted, developer decrypts client-side)
119 - - Device and key metadata
120 - - Usage history and billing records
121 -
122 - Cancellation takes effect at end of billing period. Data is retained for 30 days after cancellation, then permanently deleted. The developer can export during this window.
123 -
124 - ## Future Add-Ons
125 -
126 - These are planned capabilities with real marginal costs, priced separately when they ship:
127 -
128 - - **Realtime sync** (WebSocket/SSE push) — pricing TBD based on connection costs
129 - - **Custom domain** — pricing TBD
130 - - **Priority support** — pricing TBD
131 -
132 - Add-ons will follow the same principles: predictable, cost-relative, no surprises.
133 -
134 - ## See Also
135 -
136 - - [SyncKit architecture](../shared/synckit-client/docs/architecture.md)
137 - - [SyncKit competition analysis](../shared/synckit-client/docs/competition.md)
138 - - [MNW economics](internal/business/economics.md) — SyncKit as MNW add-on
139 - - [Fan+ design](internal/business/fan-plus.md) — consumer subscription context
@@ -1,128 +0,0 @@
1 - # Creator Trust Audit — Findings & Fixes
2 -
3 - Audit date: 2026-04-26. Perspective: skeptical creator evaluating MNW.
4 -
5 - ## Deal Breakers
6 -
7 - - [x] **IP scrubbing bug**: `scheduler.rs` line 872 queries `downloaded_at` which doesn't exist (column is `created_at`). IPs never deleted. Violates privacy policy's 30-day claim.
8 - - [x] **Streaming tier sold but unimplemented**: $40/mo tier on `/creators` pricing table has zero code (no RTMP, no chat, no streaming). Blocked from purchase in checkout + labeled "coming soon" on pricing table.
9 -
10 - ## Trust Gaps
11 -
12 - - [x] **Encryption docs overstate implementation**: `tech/security.md` says "AES-256 disk encryption" without clarifying it's infrastructure-provided (Hetzner), not application-level. Misleading to security-conscious creators.
13 - - [x] **Streaming tier on creators page lacks "coming soon"**: Pricing table at `/creators` lists Streaming at $40 without any indication it's unimplemented.
14 - - [x] **Fan access lost on creator account deletion**: 90-day content grace period implemented. Creators with sales get deactivated (not deleted) for 90 days; buyers can still download. Guarantee language updated.
15 - - [x] **Git repos excluded from data export**: Non-issue. Git repos are inherently exportable (`git clone`). Not "uploaded content" in the same sense as media files. Export README already notes how to clone.
16 - - [x] **Video features in Big Files tier not functional**: Was actually implemented (upload, player, access control, tests). Docs were stale. Updated tiers.md — only transcoding/adaptive streaming remain planned.
17 - - [ ] **Content archive guarantee unimplemented**: 12-month content preservation listed under "Guarantees" (albeit in "Planned" subsection). Could be mistaken for current feature.
18 -
19 - ## Missing Information (docs additions needed)
20 -
21 - - [x] **Chargeback fee not documented**: Added dispute/chargeback section to payouts.md.
22 - - [ ] **Custom domains not prominently documented**: Feature exists but hard to find from getting-started flow.
23 - - [ ] **No creator storefront preview/demo**: First-time visitors can't see what a page looks like.
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") |
46 -
47 - ## Competitive Weaknesses (product decisions, not bugs)
48 -
49 - - [ ] No free creator tier ($10/mo minimum vs. $0 on Bandcamp/itch.io/Gumroad)
50 - - [x] No public discovery/browse mechanism: Already implemented (/discover, /discover/tags, /feed, tag filters, search). Added discovery section to fan-guide.md.
51 - - [ ] No mobile fan apps
52 - - [x] Embeddable widgets — all 5 embed types + dashboard UI + guest checkout shipped
53 - - [ ] Low social proof (unknown creator count shown on page)
54 -
55 - ## Embeds (plans at `docs/plans/embed-*.md`)
56 -
57 - - [x] Guest checkout (embed-0) — complete: schema, paid checkout, free claim, buy page, purchase page UI, emails, auto-attach on signup, integration tests.
58 - - [x] Buy button embed (embed-1) — route, CORS, inline HTML, 300x60 strip
59 - - [x] Product card embed (embed-2) — vertical/horizontal layouts with cover, creator, description
60 - - [x] Audio player embed (embed-3) — inline player with play/pause/seek/progress (uses stream endpoint; ffmpeg preview generation deferred)
61 - - [x] Tip button embed (embed-4) — avatar + "Support @username" + button
62 - - [x] Project card embed (embed-5) — cover, title, creator, item count, category
63 - - [x] Dashboard UI (embed-6) — "Embed" tab on item dashboard with live previews, copy buttons, layout toggle, and direct purchase link
64 -
65 - ## Contradictions Found
66 -
67 - | Claim | Reality | Severity | Status |
68 - |-------|---------|----------|--------|
69 - | "IPs deleted after 30 days" (privacy policy) | Query references wrong column; never executes | High | Fixed |
70 - | "All uploaded content in original quality" (guarantees) | Git repos excluded from export | Medium | Non-issue (git clone) |
71 - | "AES-256 encryption at rest" (security.md) | Infrastructure-only, no app-level encryption | Medium | Fixed (docs) |
72 - | "Creators own fan relationships" (guarantees) | Account deletion CASCADE removes fan access | High | Fixed (90-day grace) |
73 - | Streaming tier $40/mo (creators page) | Zero implementation | High | Fixed (blocked + labeled) |
74 -
75 - ## Vaporware (all honestly labeled in docs)
76 -
77 - | Feature | Status |
78 - |---------|--------|
79 - | Live streaming (RTMP, chat, clips) | Not implemented |
80 - | Fan+ subscription | Implemented (checkout, billing, badge — docs were stale) |
81 - | Video upload/playback | Implemented (upload, player, access control — docs were stale) |
82 - | Video transcoding/adaptive | Not implemented (upload-format-only delivery) |
83 - | Content Archive (12-month preservation) | Not implemented |
84 - | Independent moderation appeals | Not implemented (one-person team) |
85 - | 99.9% uptime | Not implemented (currently 99.5% target) |
86 -
87 - ## Trust Audit Run 2 (2026-05-02)
88 -
89 - ### Docs & Templates
90 - - [x] **Surface solo-founder model on creators page**: Added "Who Runs This" section to `templates/pages/creators.html` linking to continuity guarantee and economics.
91 - - [x] **Stripe country requirement prominent on creators page**: Added explicit callout in "How It Works" section with link to Stripe global page, before the CTA.
92 - - [x] **Creator earnings examples in tiers.md**: Added "What Creators Keep" section with data-based examples for each tier using real Stripe fees (2.9% + $0.30). Includes break-even points, comparison to percentage-cut platforms, and at-scale table.
93 - - [x] **Session retention enforcement (item 8)**: Verified — `scheduler/mod.rs:152` hardcodes `Duration::days(90)` for session pruning. Code matches privacy policy claim. No fix needed.
94 -
95 - ### Code Changes Needed
96 - - [ ] **Status notification channel for creators**: Existing infra: Postmark broadcasts, per-user notification prefs (`notifications.rs`), monitor alert emails to admin (`monitor.rs:157-173`), WAM tickets on status change (`monitor.rs:175-190`). **Need:** Add `notify_status` preference to user notification settings. On health status transitions (degraded/error/recovery), send email to opted-in creators. WAM ticket already created on transitions — extend monitor to also dispatch creator-facing status emails via Postmark broadcast stream. Consider also adding RSS feed from health endpoint for programmatic consumers.
97 -
98 - ## Doc Fuzz Remaining Items (2026-05-02)
99 -
100 - ### Deduplication (maintenance risk)
101 - - [x] **Guest checkout flow**: 03-selling.md canonical. pricing.md and best-practices.md replaced with cross-links. fan-guide.md condensed to 1-paragraph summary.
102 - - [x] **Data export list**: export.md canonical. 03-selling.md table replaced with brief summary + link. Others (guarantees, how-we-work, portability) kept as-is — different purposes (guarantee, pitch, tech spec).
103 - - [x] **Pricing tier table**: how-we-work.md Everything tier wording aligned with tiers.md ("All features, current and future (live streaming coming soon)").
104 -
105 - ### Missing Information
106 - - [x] **Forum access flow**: Added SSO explanation to forums.md ("Log in with your existing Makenot.work credentials — the forum uses your platform account directly").
107 - - [x] **Refund policy guidance**: Added "Have a Refund Policy" section to best-practices.md with 3 common approaches and chargeback warning.
108 - - [x] **Stripe onboarding preview**: Added Stripe identity requirements to getting-started.md step 3 (legal name, DOB, tax ID, bank account).
109 - - [x] **Software on Basic tier confusion**: Clarified tiers.md table — "Under 50MB total (10MB per file)".
110 -
111 - ### Tone/Clarity
112 - - [x] **tech/security.md opening line**: Replaced slogan with informative summary.
113 - - [x] **about/economics.md residency section**: Condensed 3 paragraphs to 2 sentences. Kept core message, removed hiring manifesto.
114 - - [x] **developer/synckit.md vague limits**: Specified "500 changes per push", "100 characters" table names, "255 characters" row IDs.
115 - - [x] **developer/api-overview.md vague expiry**: Specified "7 days of inactivity".
116 -
117 - ### Cross-Consistency
118 - - [x] **Custom domains scope**: Aligned roadmap.md to say "creator profile" (matches custom-domains.md and code).
119 -
120 - ## Key Paths
121 -
122 - - `server/src/scheduler.rs` — IP scrubbing job
123 - - `server/site-docs/public/tech/security.md` — encryption claims
124 - - `server/site-docs/public/guide/tiers.md` — tier descriptions
125 - - `server/site-docs/public/about/guarantees.md` — binding commitments
126 - - `server/templates/pages/creators.html` — creator pricing table
127 - - `server/src/routes/stripe/` — payment implementation
128 - - `server/src/routes/api/export.rs` — export endpoints
@@ -1,7 +1,7 @@
1 1 # Makenotwork TODO
2 2
3 3 ## Status
4 - v0.5.14 deployed 2026-05-11. Audit grade A (Run 24). ~88K LOC, 1,935 tests, 0 warnings. Migration 111. Sprints 1-9 complete (see `todo_done.md`). Content seeded: AF 0.4.0 + GO 0.3.1 on discover page.
4 + v0.5.14 deployed 2026-05-11. Audit grade A (Run 26). ~88K LOC, 1,935 tests, 0 warnings. Migration 112. Sprints 1-9 complete (see `todo_done.md`). Content seeded: AF 0.4.0 + GO 0.3.1 on discover page.
5 5
6 6 Human tasks in `human_todo.md`. Completed items in `todo_done.md`.
7 7
@@ -11,16 +11,10 @@ Human tasks in `human_todo.md`. Completed items in `todo_done.md`.
11 11
12 12 Priority order. See `human_todo.md` for the full manual testing feature map.
13 13
14 - 1. ~~**Deploy**~~ — Done (v0.5.14, 2026-05-11). Run 24 fixes + scheduler SQL fixes + robots.txt + Prometheus auth + ALERT_EMAIL + tag taxonomy overhaul (migration 111).
15 - 2. **Manual testing** — walk through `human_todo.md` sign-off table on live server (Stripe checkout, license keys, promo codes, cart, SyncKit sync)
16 - - SyncKit parity fixes shipped for AF + BB + GO (2026-05-11): OAuth auto-poll, CORS, CSP, synckit.toml, callback auto-complete
17 - - AF sync tested on live server — largely working (2026-05-11)
18 - - GO sync tested on live server — working (2026-05-11)
19 - - BB sync: synckit.toml still needed (API key pending)
14 + 1. **Manual testing** — walk through `human_todo.md` sign-off table on live server (Stripe checkout, license keys, promo codes, cart, SyncKit sync)
15 + - SyncKit: AF + GO sync tested and working on live server (2026-05-11). BB sync needs synckit.toml (API key pending).
20 16 - Remaining: Stripe checkout e2e for all 3 apps, license key flow, promo codes
21 - 3. ~~**Content seeding**~~ — Done: AF 0.4.0 + GO 0.3.1 published on discover page. BB deferred (needs more plugins).
22 - 4. **Invite testers** — generate invite codes, send hand-written emails per `docs/internal/outreach/tiers.md`
23 - 5. ~~**Document undocumented features**~~ — Done: shopping cart, wishlist, creator pause all documented
17 + 2. **Invite testers** — generate invite codes, send hand-written emails per `docs/internal/outreach/tiers.md`
24 18
25 19 ---
26 20
@@ -34,6 +28,18 @@ Priority order. See `human_todo.md` for the full manual testing feature map.
34 28
35 29 ---
36 30
31 + ## Trust Audit Open Items (migrated from todo-creator-trust-audit.md, 2026-05-12)
32 +
33 + - [ ] **Warning-only admin action**: moderation.md describes a 4-step ladder starting with "Direct Message," but code only implements suspend/unsuspend/terminate. No tracked warning-only communication. (`routes/admin/users.rs`)
34 + - [ ] **Content archive guarantee unimplemented**: 12-month content preservation listed under "Planned Guarantees" in guarantees.md. Ensure it's clearly marked as planned, not current.
35 + - [ ] **Custom domains not in getting-started flow**: Feature exists but hard to find from onboarding. Link from getting-started.md.
36 + - [ ] **Creator storefront preview/demo**: First-time visitors can't see what a page looks like before signing up.
37 + - [ ] **Creator status notification channel**: On health status transitions, email opted-in creators. WAM tickets already created on transitions — extend monitor to dispatch creator-facing status emails. (`monitor.rs`, `notifications.rs`)
38 +
39 + Note: "Appeals reviewed by same person" and "liability cap" are known one-person-team constraints, tracked in guarantees.md planned section.
40 +
41 + ---
42 +
37 43 ## Deferred from Sprints
38 44
39 45 - [ ] Add bulk rename operation (Sprint 2)
@@ -43,23 +49,9 @@ Priority order. See `human_todo.md` for the full manual testing feature map.
43 49
44 50 ---
45 51
46 - ## Ultra Fuzz Run 25 (2026-05-11)
47 -
48 - ### Current Phase
49 - - [x] **[SERIOUS]** Fix `db/cart.rs:106` -- change `t.user_id` to `t.buyer_id` (1 line)
50 - - [x] **[SERIOUS]** Add S3 cleanup + storage decrement to project deletion path (`routes/api/projects.rs:270-282`)
51 - - [x] **[SERIOUS]** Stream content export ZIP to S3 instead of in-memory buffer (`routes/api/exports/content.rs`)
52 - - [ ] **[MINOR]** Consolidate item cover image into single UPDATE (`routes/storage/images.rs:369-371`)
53 - - [ ] **[MINOR]** Use `try_replace_storage` for project image replace (`routes/storage/images.rs:173-185`)
54 - - [ ] **[MINOR]** Enqueue old project image S3 key to `pending_s3_deletions`
55 - - [ ] **[MINOR]** Add HTTP error check to purchase.html cart add-to-cart JS
56 - - [ ] **[MINOR]** Fix `format_price` negative formatting to use `-$X.XX`
57 - - [ ] **[MINOR]** Batch-load collection items in export handler (`exports/mod.rs:268-270`)
58 - - [ ] **[MINOR]** Switch `check_sandbox_cap` to `pg_try_advisory_lock`
59 -
60 - ### Deferred
61 - - [ ] DEFERRED: Stream build artifacts to S3 via multipart upload
62 - - [ ] DEFERRED: Extract shared `validate_promo_code()` helper to prevent checkout path divergence (carried from Run 24)
52 + ### Fuzz Run Deferred (carried from Runs 25-26)
53 + - [ ] Stream build artifacts to S3 via multipart upload
54 + - [ ] Extract shared `validate_promo_code()` helper (chronic — unfixed across Runs 24-26)
63 55
64 56 ---
65 57
@@ -146,7 +138,6 @@ Remaining open items from Runs 21-24 and Code Fuzz (2026-05-08). All SERIOUS ite
146 138
147 139 ### Infrastructure
148 140 - [ ] Media transcoding pipeline (probe, audio, video, adaptive bitrate)
149 - - [ ] Embeddable widgets (endpoint, overlay, inline)
150 141 - [ ] Performance (caching, query optimization, CDN, metrics)
151 142 - [ ] Search infrastructure (tsvector, unified API, cross-project)
152 143 - [ ] Notification service (table, triggers, API, digest prefs)
@@ -203,6 +194,7 @@ Remaining open items from Runs 21-24 and Code Fuzz (2026-05-08). All SERIOUS ite
203 194 - [ ] S3 bucket versioning
204 195 - [ ] PDF stamping
205 196 - [ ] CONCURRENTLY index strategy
197 + - [ ] Supply chain: cargo-vet adoption (5 phases, see `_meta/docs/supply_chain.md`)
206 198
207 199 ## Key Paths
208 200 ```
@@ -214,7 +206,7 @@ MNW/server/src/
214 206 import/ (CSV converter, pipeline, intermediate format)
215 207 MNW/server/tests/
216 208 integration.rs, harness/, workflows/*.rs
217 - MNW/server/migrations/ (001-111)
209 + MNW/server/migrations/ (001-112)
218 210 MNW/server/templates/
219 211 MNW/server/deploy/
220 212 MNW/server/site-docs/public/, MNW/server/site-docs/unpublished/