max / makenotwork
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. |
| @@ -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", |
| @@ -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–470</td> | |
| 419 | + | <td class="num highlight-row">$410–460</td> | |
| 420 | 420 | <td class="num">$420</td> | |
| 421 | 421 | <td class="num">$395</td> | |
| 422 | - | <td class="num">$15–75</td> | |
| 422 | + | <td class="num">$15–65</td> | |
| 423 | 423 | </tr> | |
| 424 | 424 | <tr> | |
| 425 | 425 | <td>$1,000</td> | |
| 426 | - | <td class="num highlight-row">$881–941</td> | |
| 426 | + | <td class="num highlight-row">$881–931</td> | |
| 427 | 427 | <td class="num">$870</td> | |
| 428 | 428 | <td class="num">$820</td> | |
| 429 | - | <td class="num">$61–121</td> | |
| 429 | + | <td class="num">$61–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–$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–$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/ |