max / makenotwork
82 files changed,
+3524 insertions,
-173 deletions
| @@ -1804,6 +1804,7 @@ dependencies = [ | |||
| 1804 | 1804 | "ammonia", | |
| 1805 | 1805 | "pulldown-cmark", | |
| 1806 | 1806 | "regex", | |
| 1807 | + | "regex-lite", | |
| 1807 | 1808 | "serde", | |
| 1808 | 1809 | "toml", | |
| 1809 | 1810 | "tracing", | |
| @@ -3372,7 +3373,7 @@ dependencies = [ | |||
| 3372 | 3373 | ||
| 3373 | 3374 | [[package]] | |
| 3374 | 3375 | name = "makenotwork" | |
| 3375 | - | version = "0.3.21" | |
| 3376 | + | version = "0.3.22" | |
| 3376 | 3377 | dependencies = [ | |
| 3377 | 3378 | "anyhow", | |
| 3378 | 3379 | "argon2", | |
| @@ -3412,6 +3413,7 @@ dependencies = [ | |||
| 3412 | 3413 | "tempfile", | |
| 3413 | 3414 | "thiserror 2.0.18", | |
| 3414 | 3415 | "tokio", | |
| 3416 | + | "tokio-stream", | |
| 3415 | 3417 | "totp-rs", | |
| 3416 | 3418 | "tower", | |
| 3417 | 3419 | "tower-http", |
| @@ -14,6 +14,7 @@ axum-extra = { version = "0.10.3", features = ["cookie", "form", "typed-header"] | |||
| 14 | 14 | serde = { version = "1.0.228", features = ["derive"] } | |
| 15 | 15 | serde_json = "1.0.149" | |
| 16 | 16 | tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "net", "signal"] } | |
| 17 | + | tokio-stream = { version = "0.1", features = ["sync"] } | |
| 17 | 18 | tower = "0.5.3" | |
| 18 | 19 | tower-http = { version = "0.6.8", features = ["trace", "fs", "limit", "request-id", "propagate-header", "set-header"] } | |
| 19 | 20 | tracing = "0.1.44" | |
| @@ -80,7 +81,7 @@ thiserror = "2.0.18" | |||
| 80 | 81 | anyhow = "1.0.101" | |
| 81 | 82 | ||
| 82 | 83 | # Markdown rendering + documentation engine | |
| 83 | - | docengine = { path = "../Shared/docengine", features = ["doc-loader", "frontmatter"] } | |
| 84 | + | docengine = { path = "../Shared/docengine", features = ["doc-loader", "directives", "frontmatter", "media-urls"] } | |
| 84 | 85 | ||
| 85 | 86 | # Tag standard | |
| 86 | 87 | tagtree = { path = "../Shared/tagtree" } |
| @@ -1,5 +1,5 @@ | |||
| 1 | 1 | #!/bin/bash | |
| 2 | - | # Generate rustdoc for library crates (synckit-client, docengine, tagtree). | |
| 2 | + | # Generate rustdoc for shared library crates. | |
| 3 | 3 | # Output goes to rustdoc-out/ (relative to MNW/). | |
| 4 | 4 | # Run from the MNW directory. | |
| 5 | 5 | ||
| @@ -13,7 +13,7 @@ fi | |||
| 13 | 13 | OUT_DIR="$(pwd)/rustdoc-out" | |
| 14 | 14 | SHARED_DIR="$(cd ../Shared && pwd)" | |
| 15 | 15 | ||
| 16 | - | CRATES=("synckit-client" "docengine" "tagtree") | |
| 16 | + | CRATES=("synckit-client" "docengine" "tagtree" "s3-storage" "theme-common") | |
| 17 | 17 | ||
| 18 | 18 | rm -rf "$OUT_DIR" | |
| 19 | 19 | mkdir -p "$OUT_DIR" |
| @@ -4,16 +4,16 @@ Run after major features, monthly during active development, or when concerned a | |||
| 4 | 4 | ||
| 5 | 5 | ## Audit Scope | |
| 6 | 6 | ||
| 7 | - | | Project | Path | Stack | | |
| 8 | - | |---------|------|-------| | |
| 9 | - | | MakeNotWork | MNW/ | Rust/Axum/PostgreSQL, Askama, HTMX | | |
| 10 | - | | SyncKit Client SDK | Shared/synckit-client/ | Rust, reqwest, ChaCha20-Poly1305, Argon2, keychain | | |
| 11 | - | | PoM | pom/ | Rust/Axum/SQLite, MCP (rmcp), reqwest | | |
| 12 | - | | audiofiles | Apps/audiofiles/ | Rust, eframe, egui, cpal, rusqlite | | |
| 13 | - | | Balanced Breakfast | Apps/balanced_breakfast/ | Rust/Tauri 2, SQLite, Rhai plugins, vanilla JS | | |
| 14 | - | | GoingsOn | Apps/goingson/ | Rust/Tauri 2, SQLite, vanilla JS, IMAP/SMTP | | |
| 15 | - | | Multithreaded | multithreaded/ | Rust/Axum/PostgreSQL, Askama, HTMX, MNW OAuth | | |
| 16 | - | | TagTree | Shared/tagtree/ | Rust library crate, no_std compatible, serde, criterion benchmarks | | |
| 7 | + | | Harness ID | Project | Path | Stack | | |
| 8 | + | |------------|---------|------|-------| | |
| 9 | + | | `mnw` | MakeNotWork | MNW/ | Rust/Axum/PostgreSQL, Askama, HTMX | | |
| 10 | + | | `synckit` | SyncKit Client SDK | Shared/synckit-client/ | Rust, reqwest, ChaCha20-Poly1305, Argon2, keychain | | |
| 11 | + | | `pom` | PoM | pom/ | Rust/Axum/SQLite, MCP (rmcp), reqwest | | |
| 12 | + | | `af` | audiofiles | Apps/audiofiles/ | Rust, eframe, egui, cpal, rusqlite | | |
| 13 | + | | `bb` | Balanced Breakfast | Apps/balanced_breakfast/ | Rust/Tauri 2, SQLite, Rhai plugins, vanilla JS | | |
| 14 | + | | `go` | GoingsOn | Apps/goingson/ | Rust/Tauri 2, SQLite, vanilla JS, IMAP/SMTP | | |
| 15 | + | | `mt` | Multithreaded | multithreaded/ | Rust/Axum/PostgreSQL, Askama, HTMX, MNW OAuth | | |
| 16 | + | | `tagtree` | TagTree | Shared/tagtree/ | Rust library crate, no_std compatible, serde, criterion benchmarks | | |
| 17 | 17 | ||
| 18 | 18 | --- | |
| 19 | 19 | ||
| @@ -41,6 +41,8 @@ Run after major features, monthly during active development, or when concerned a | |||
| 41 | 41 | ||
| 42 | 42 | ## Module-Level Assessment | |
| 43 | 43 | ||
| 44 | + | Use `get_project(id)` from the codebase harness to get the full module inventory for each project — all source files with extracted types, exports, and line counts. This replaces manual filesystem scanning. | |
| 45 | + | ||
| 44 | 46 | Grade each dimension **per module** (crate, directory module, or route group with ~100+ lines), then roll up to project-level. Weight by risk and size — an untested 2,000-line file drags Testing down more than an untested 50-line wrapper. | |
| 45 | 47 | ||
| 46 | 48 | **Cold spots:** any module graded B or below, or 2+ letter grades below the project median. Include a Module Heatmap in the audit review. | |
| @@ -63,6 +65,7 @@ Grade each dimension **per module** (crate, directory module, or route group wit | |||
| 63 | 65 | - [ ] No secrets, database files, or large binaries tracked | |
| 64 | 66 | ||
| 65 | 67 | ### Documentation Consistency | |
| 68 | + | - [ ] `startup_check` shows no stale docs (all hashes current) | |
| 66 | 69 | - [ ] File paths in docs still exist | |
| 67 | 70 | - [ ] Code references (structs, endpoints, env vars) match codebase | |
| 68 | 71 | - [ ] Feature/pricing descriptions match across CLAUDE.md, public docs, and code | |
| @@ -82,10 +85,12 @@ Grade each dimension **per module** (crate, directory module, or route group wit | |||
| 82 | 85 | ||
| 83 | 86 | ## Execution Order | |
| 84 | 87 | ||
| 85 | - | 1. Run the core audit — module heatmaps, scorecards, cold spots per project (parallelizable) | |
| 86 | - | 2. Write/update `audit_review.md` per project (reads previous review only now) | |
| 87 | - | 3. File action items in each project's `todo.md` (critical = current phase, architectural = deferred) | |
| 88 | - | 4. Update `audit_history.md` with run summary | |
| 88 | + | 0. **Harness startup** — call `startup_check` to get doc freshness report, version mismatches, and project overview. Call `list_projects` for current versions and test counts. Flag any stale docs before beginning. | |
| 89 | + | 1. **Core audit** — for each project, call `get_project(id)` to get its module map, then grade per module. Module heatmaps, scorecards, cold spots (parallelizable across projects). | |
| 90 | + | 2. **Write/update** `audit_review.md` per project (reads previous review only now) | |
| 91 | + | 3. **File action items** in each project's `todo.md` (critical = current phase, architectural = deferred) | |
| 92 | + | 4. **Update** `audit_history.md` with run summary | |
| 93 | + | 5. **Harness update** — for each audited project: `update_test_count(id, count)` with latest test counts. `mark_verified(path)` on all docs that were reviewed. If versions changed, `record_deploy(id, version)`. Repopulate if module structure changed significantly: `cd _meta/harness/server && node dist/populate.js` | |
| 89 | 94 | ||
| 90 | 95 | --- | |
| 91 | 96 | ||
| @@ -123,5 +128,5 @@ Each project's `docs/<short-name>/audit_review.md` should contain: overall grade | |||
| 123 | 128 | ## Post-Session Cleanup | |
| 124 | 129 | ||
| 125 | 130 | ```bash | |
| 126 | - | find /Users/max/Git -name "target" -type d -maxdepth 4 -exec rm -rf {} + | |
| 131 | + | find /Users/max/Code -name "target" -type d -maxdepth 4 -exec rm -rf {} + | |
| 127 | 132 | ``` |
| @@ -0,0 +1,126 @@ | |||
| 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. |
| @@ -0,0 +1,159 @@ | |||
| 1 | + | # Feature-to-Docs Coverage Matrix | |
| 2 | + | ||
| 3 | + | Three documentation levels for every feature. Source: codebase harness DB (727 modules, 638 key files, 49 docs). | |
| 4 | + | ||
| 5 | + | **Legend:** Y = covered, P = partial, N = missing, -- = not applicable | |
| 6 | + | ||
| 7 | + | ## MNW Server (237 modules, 66,815 LOC) | |
| 8 | + | ||
| 9 | + | ### Creator Platform Features | |
| 10 | + | ||
| 11 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 12 | + | |---------|-------------|:---:|:---:|:---:| | |
| 13 | + | | **Auth (login/signup/sessions)** | routes/auth.rs, auth.rs, db/sessions.rs, db/auth.rs | Y | -- | guide/01-getting-started | | |
| 14 | + | | **Passkeys (WebAuthn)** | routes/api/passkeys.rs, db/passkeys.rs | Y | -- | guide/security | | |
| 15 | + | | **TOTP (2FA)** | routes/api/totp.rs, db/totp.rs | Y | -- | guide/security | | |
| 16 | + | | **User profiles** | routes/api/users/profile.rs, db/users.rs | Y | -- | guide/profile | | |
| 17 | + | | **Projects** | routes/api/projects.rs, db/projects.rs | Y | -- | guide/projects | | |
| 18 | + | | **Items (CRUD)** | routes/api/items/*.rs, db/items.rs | Y | -- | guide/items | | |
| 19 | + | | **Item sections (tabbed content)** | routes/api/items/sections.rs, db/item_sections.rs | Y | -- | N | | |
| 20 | + | | **Chapters** | routes/api/items/chapters.rs, db/chapters.rs | Y | -- | N | | |
| 21 | + | | **Bundles** | routes/api/items/bundles.rs, db/bundles.rs | Y | -- | N | | |
| 22 | + | | **Versions (file releases)** | routes/api/items/versions.rs, db/versions.rs | Y | -- | guide/files | | |
| 23 | + | | **Tags (hierarchical)** | routes/api/tags.rs, db/tags.rs | Y | -- | guide/tags | | |
| 24 | + | | **Collections** | routes/api/collections.rs, db/collections.rs | Y | -- | guide/collections | | |
| 25 | + | | **Blog** | routes/api/blog.rs, db/blog_posts.rs, pages/blog.rs | Y | -- | guide/blog | | |
| 26 | + | | **Custom links** | routes/api/links.rs, db/custom_links.rs | Y | -- | N | | |
| 27 | + | | **Labels** | routes/api/labels.rs, db/labels.rs | Y | -- | N | | |
| 28 | + | | **Categories** | routes/api/categories.rs, db/categories.rs | Y | -- | N | | |
| 29 | + | | **Follows** | routes/api/follows.rs, db/follows.rs | Y | -- | N | | |
| 30 | + | | **Discover** | pages/public/discover.rs, db/discover.rs | Y | -- | N | | |
| 31 | + | | **RSS feeds** | rss.rs, pages/feeds.rs | Y | -- | guide/rss | | |
| 32 | + | ||
| 33 | + | ### Payments & Commerce | |
| 34 | + | ||
| 35 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 36 | + | |---------|-------------|:---:|:---:|:---:| | |
| 37 | + | | **Stripe Connect** | routes/stripe/*.rs, payments/*.rs | Y | -- | guide/03-selling, guide/payouts | | |
| 38 | + | | **Checkout (items)** | routes/stripe/checkout/item.rs | Y | -- | guide/03-selling | | |
| 39 | + | | **Checkout (subscriptions)** | routes/stripe/checkout/subscriptions.rs | Y | -- | guide/tiers | | |
| 40 | + | | **Subscription tiers** | routes/api/subscriptions.rs, db/subscriptions.rs | Y | -- | guide/tiers | | |
| 41 | + | | **License keys** | routes/api/license_keys.rs, db/license_keys.rs | Y | -- | developer/license-keys | | |
| 42 | + | | **Promo codes** | routes/api/promo_codes.rs, db/promo_codes.rs | Y | -- | guide/promo-codes | | |
| 43 | + | | **Pricing models** | pricing.rs | Y | -- | guide/pricing | | |
| 44 | + | | **Transactions** | db/transactions.rs | Y | -- | guide/03-selling | | |
| 45 | + | | **Fan+ subscription** | db/fan_plus.rs | Y | -- | guide/fan-plus | | |
| 46 | + | | **Creator tiers (platform)** | db/creator_tiers.rs | Y | -- | guide/tiers | | |
| 47 | + | | **Analytics** | db/analytics.rs | Y | -- | guide/analytics | | |
| 48 | + | | **Exports (data portability)** | routes/api/exports.rs | Y | -- | guide/export | | |
| 49 | + | ||
| 50 | + | ### File Storage & Security | |
| 51 | + | ||
| 52 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 53 | + | |---------|-------------|:---:|:---:|:---:| | |
| 54 | + | | **S3 storage (upload/download)** | storage.rs, routes/storage/*.rs | Y | -- | guide/files | | |
| 55 | + | | **Media files** | routes/storage/media.rs, db/media_files.rs | Y | -- | N | | |
| 56 | + | | **Content insertions** | routes/api/content_insertions.rs, db/content_insertions.rs | Y | -- | N | | |
| 57 | + | | **File scanning (ClamAV/YARA)** | scanning/*.rs | Y | -- | tech/content-protection | | |
| 58 | + | | **Content fingerprinting** | fingerprint/*.rs | Y | -- | tech/content-protection | | |
| 59 | + | | **Video upload/playback** | (migration 053, FileType::Video) | Y | -- | N | | |
| 60 | + | ||
| 61 | + | ### Git & Code | |
| 62 | + | ||
| 63 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 64 | + | |---------|-------------|:---:|:---:|:---:| | |
| 65 | + | | **Git source browser** | routes/git/*.rs, git/*.rs | Y | -- | guide/git | | |
| 66 | + | | **Git issues (email-first)** | routes/git_issues/*.rs, db/issues.rs | Y | -- | N | | |
| 67 | + | | **SSH keys** | routes/api/ssh_keys.rs, db/ssh_keys.rs | Y | -- | N | | |
| 68 | + | | **Patches (git send-email)** | routes/postmark/patches.rs, db/patches.rs | Y | -- | N | | |
| 69 | + | | **Git repos** | db/git_repos.rs | Y | -- | N | | |
| 70 | + | ||
| 71 | + | ### Platform Infrastructure | |
| 72 | + | ||
| 73 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 74 | + | |---------|-------------|:---:|:---:|:---:| | |
| 75 | + | | **OAuth2 PKCE provider** | routes/oauth.rs, db/oauth.rs | Y | -- | developer/oauth | | |
| 76 | + | | **Email system** | email/*.rs | Y | -- | N | | |
| 77 | + | | **CSRF protection** | csrf.rs | Y | -- | tech/security | | |
| 78 | + | | **Custom domains** | routes/custom_domain.rs, routes/api/domains.rs, db/custom_domains.rs | Y | -- | guide/custom-domains | | |
| 79 | + | | **Mailing lists** | db/mailing_lists.rs | Y | -- | guide/mailing-lists | | |
| 80 | + | | **Import system** | import/*.rs, routes/api/imports.rs, db/imports.rs | Y | -- | guide/migration | | |
| 81 | + | | **Waitlist** | routes/api/users/waitlist.rs, db/waitlist.rs | Y | -- | N | | |
| 82 | + | | **Invites** | routes/api/users/invites.rs, db/invites.rs | Y | -- | N | | |
| 83 | + | | **Reports/moderation** | routes/api/reports.rs, db/reports.rs | Y | -- | legal/moderation | | |
| 84 | + | | **Health monitoring** | pages/public/health.rs, monitor.rs, db/health.rs, db/monitor.rs | Y | -- | tech/monitoring | | |
| 85 | + | | **Scheduler** | scheduler.rs | Y | -- | N | | |
| 86 | + | | **Admin panel** | routes/admin/*.rs, bin/mnw-admin.rs | Y | -- | N | | |
| 87 | + | | **Onboarding emails** | email/templates/onboarding.rs | Y | -- | N | | |
| 88 | + | | **MT client integration** | mt_client.rs | Y | -- | support/forums | | |
| 89 | + | | **DocEngine rendering** | markdown.rs, routes/pages/public/docs.rs | Y | -- | N | | |
| 90 | + | ||
| 91 | + | ### SyncKit Server | |
| 92 | + | ||
| 93 | + | | Feature | Code Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 94 | + | |---------|-------------|:---:|:---:|:---:| | |
| 95 | + | | **SyncKit auth (JWT)** | synckit_auth.rs, routes/synckit/auth.rs | Y | -- | developer/synckit | | |
| 96 | + | | **SyncKit apps** | routes/synckit/apps.rs, db/synckit.rs | Y | -- | developer/synckit | | |
| 97 | + | | **SyncKit push/pull** | routes/synckit/sync.rs | Y | -- | developer/synckit | | |
| 98 | + | | **SyncKit blobs** | routes/synckit/blobs.rs | Y | -- | developer/synckit | | |
| 99 | + | | **SyncKit SSE push** | routes/synckit/subscribe.rs | Y | -- | developer/sse | | |
| 100 | + | | **OTA updates** | routes/ota.rs, db/ota.rs | Y | -- | developer/ota | | |
| 101 | + | | **Build pipeline** | routes/builds.rs, db/builds.rs, build_runner.rs | Y | -- | N | | |
| 102 | + | ||
| 103 | + | --- | |
| 104 | + | ||
| 105 | + | ## SyncKit Client SDK (13 modules, 5,431 LOC) | |
| 106 | + | ||
| 107 | + | | Feature | Code Module | Inline (//!) | Rustdoc | Public Doc | | |
| 108 | + | |---------|------------|:---:|:---:|:---:| | |
| 109 | + | | **Auth (OAuth PKCE + email/pw)** | client/auth.rs | Y | Y | developer/synckit | | |
| 110 | + | | **Push/pull sync** | client/sync.rs | Y | Y | developer/synckit | | |
| 111 | + | | **Blob upload/download** | client/blob.rs | Y | Y | developer/synckit | | |
| 112 | + | | **E2E encryption** | crypto.rs, client/encryption.rs | Y | Y | developer/synckit | | |
| 113 | + | | **OS keychain** | keystore.rs | Y | Y | N | | |
| 114 | + | | **Conflict resolution** | conflict.rs | Y | Y | N | | |
| 115 | + | | **SSE notifications** | client/subscribe.rs | Y | Y | N | | |
| 116 | + | | **Pull filtering** | types.rs (PullFilter) | Y | Y | N | | |
| 117 | + | ||
| 118 | + | --- | |
| 119 | + | ||
| 120 | + | ## Shared Libraries | |
| 121 | + | ||
| 122 | + | | Library | Modules | Inline (//!) | Rustdoc | Public Doc | | |
| 123 | + | |---------|---------|:---:|:---:|:---:| | |
| 124 | + | | **docengine** | 13 (2,259 LOC) | P (23%) | Y | N | | |
| 125 | + | | **tagtree** | 1 (1,871 LOC) | Y | Y | N | | |
| 126 | + | | **theme-common** | 1 (585 LOC) | Y | Y | N | | |
| 127 | + | | **s3-storage** | 1 (287 LOC) | Y | Y | N | | |
| 128 | + | ||
| 129 | + | --- | |
| 130 | + | ||
| 131 | + | ## Gap Summary | |
| 132 | + | ||
| 133 | + | ### Level 1: Inline Code Docs (//!) | |
| 134 | + | **Overall: 98% module coverage for MNW.** Nearly every module has `//!` headers. Scheduler and monitor expanded from single-line to full 4-5 line descriptions (2026-04-11). All other projects range from 23% (docengine) to 100% (tagtree). | |
| 135 | + | ||
| 136 | + | ### Level 2: Rustdoc | |
| 137 | + | **Generated for 5 crates: synckit-client, docengine, tagtree, s3-storage, theme-common.** Hosted at `/rustdoc`. s3-storage and theme-common added 2026-04-11. MNW server itself is not a library crate so rustdoc is less relevant — it's an application binary. | |
| 138 | + | ||
| 139 | + | ### Level 3: Public-Facing Docs (site-docs/) | |
| 140 | + | **56 docs across 7 categories.** 10 new docs added 2026-04-11. Strong coverage of core creator workflows plus developer integration points. | |
| 141 | + | ||
| 142 | + | **Remaining undocumented features:** | |
| 143 | + | ||
| 144 | + | | Feature | Priority | Reason | | |
| 145 | + | |---------|----------|--------| | |
| 146 | + | | Bundles | Medium | Commerce feature, no guide | | |
| 147 | + | | Item sections | Low | New feature, relatively self-explanatory UI | | |
| 148 | + | | Custom links | Low | Simple feature | | |
| 149 | + | | Build pipeline | Low | Internal/advanced developer feature | | |
| 150 | + | | Video playback | Low | New, deferred post-beta | | |
| 151 | + | | Chapters | Low | Self-explanatory UI | | |
| 152 | + | | Git issues (email-first) | Medium | Unique differentiator, no user-facing docs | | |
| 153 | + | | Discover page | Low | Self-explanatory UI | | |
| 154 | + | ||
| 155 | + | ### Cross-Reference Links (Level Linking) | |
| 156 | + | ||
| 157 | + | 11 source modules now have `//! See also:` lines linking to their corresponding public docs (added 2026-04-11). `developer/synckit.md` links to `/rustdoc/synckit_client/` (Level 3 -> Level 2). | |
| 158 | + | ||
| 159 | + | **Remaining:** Add "Source: `src/routes/...`" notes in public docs for developer-facing pages. |
| @@ -0,0 +1,230 @@ | |||
| 1 | + | # Filetype Transcoding Matrix | |
| 2 | + | ||
| 3 | + | Reference for MNW's ingest pipeline. Defines which formats can be safely converted, what saves space, and how conversions map to creator tiers. | |
| 4 | + | ||
| 5 | + | --- | |
| 6 | + | ||
| 7 | + | ## Design Principle | |
| 8 | + | ||
| 9 | + | - **BigFiles / Streaming tiers**: Store original file. Lossless preservation is the feature. | |
| 10 | + | - **SmallFiles tier**: Store a high-quality transparent encode. Humans can't tell the difference. | |
| 11 | + | - Originals are always kept for BigFiles+. SmallFiles stores only the optimized version. | |
| 12 | + | - Never transcode lossy-to-lossy. Never transcode lossy-to-lossless (wastes space, no quality gain). | |
| 13 | + | ||
| 14 | + | --- | |
| 15 | + | ||
| 16 | + | ## Audio Formats | |
| 17 | + | ||
| 18 | + | ### Format Properties | |
| 19 | + | ||
| 20 | + | | Format | Type | MB/min (stereo) | Notes | | |
| 21 | + | |--------|------|-----------------|-------| | |
| 22 | + | | WAV | Uncompressed PCM | ~10 (16-bit/44.1k), ~33 (24-bit/96k) | Baseline reference | | |
| 23 | + | | AIFF | Uncompressed PCM | ~10 (16-bit/44.1k) | WAV equivalent (Apple, big-endian) | | |
| 24 | + | | FLAC | Lossless compressed | ~5-6 (16-bit/44.1k) | ~50% of WAV. Bit-perfect. Open, royalty-free | | |
| 25 | + | | ALAC | Lossless compressed | ~5-6 (16-bit/44.1k) | ~50% of WAV. 5-10% larger than FLAC | | |
| 26 | + | | MP3 | Lossy | ~2.3 (320k), ~1.4 (192k), ~0.9 (128k) | Oldest, most compatible | | |
| 27 | + | | AAC | Lossy | ~1.9 (256k), ~1.0 (128k) | ~20-30% more efficient than MP3 at same bitrate | | |
| 28 | + | | OGG Vorbis | Lossy | ~1.2 (160k), ~1.0 (128k) | Open-source. Comparable to AAC | | |
| 29 | + | | Opus | Lossy | ~1.0 (128k), ~0.7 (96k) | Best lossy codec. Transparent at 128 kbps | | |
| 30 | + | ||
| 31 | + | ### Transparency Thresholds (indistinguishable from lossless) | |
| 32 | + | ||
| 33 | + | | Codec | Transparent bitrate | Conservative bitrate | | |
| 34 | + | |-------|--------------------|--------------------| | |
| 35 | + | | Opus | 128 kbps | 160 kbps | | |
| 36 | + | | AAC | 192 kbps | 256 kbps | | |
| 37 | + | | OGG Vorbis | 160 kbps | 192 kbps | | |
| 38 | + | | MP3 | 256 kbps | 320 kbps | | |
| 39 | + | ||
| 40 | + | ### Audio Conversion Matrix | |
| 41 | + | ||
| 42 | + | Source format on the left, target on the top. Each cell describes what happens. | |
| 43 | + | ||
| 44 | + | | Source | FLAC | Opus 128 | AAC 256 | MP3 320 | | |
| 45 | + | |--------|------|----------|---------|---------| | |
| 46 | + | | **WAV** | Lossless, saves ~50% | Transparent, saves ~90% | Transparent, saves ~81% | Transparent, saves ~77% | | |
| 47 | + | | **AIFF** | Lossless, saves ~50% | Transparent, saves ~90% | Transparent, saves ~81% | Transparent, saves ~77% | | |
| 48 | + | | **FLAC** | No-op | Transparent, saves ~83% | Transparent, saves ~68% | Transparent, saves ~61% | | |
| 49 | + | | **ALAC** | Lossless, saves ~5-10% | Transparent, saves ~83% | Transparent, saves ~68% | Transparent, saves ~61% | | |
| 50 | + | | **MP3** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | No-op | | |
| 51 | + | | **AAC** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | No-op | NEVER (lossy-to-lossy) | | |
| 52 | + | | **OGG** | NEVER (bigger file, no quality gain) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | | |
| 53 | + | | **Opus** | NEVER (bigger file, no quality gain) | No-op | NEVER (lossy-to-lossy) | NEVER (lossy-to-lossy) | | |
| 54 | + | ||
| 55 | + | ### Audio Decision Tree | |
| 56 | + | ||
| 57 | + | ``` | |
| 58 | + | Is the source lossless (WAV/AIFF/FLAC/ALAC)? | |
| 59 | + | YES: | |
| 60 | + | Tier = BigFiles/Streaming? | |
| 61 | + | -> Store as FLAC (lossless, ~50% smaller than WAV/AIFF) | |
| 62 | + | -> Keep original alongside if WAV/AIFF (creator can re-download) | |
| 63 | + | Tier = SmallFiles? | |
| 64 | + | -> Store as Opus 128 kbps (transparent, ~90% smaller than WAV) | |
| 65 | + | -> Alternatively AAC 256 kbps for broader device compat | |
| 66 | + | NO (source is lossy MP3/AAC/OGG/Opus): | |
| 67 | + | -> Store as-is. No conversion possible without degradation. | |
| 68 | + | -> File is already small. No space savings available. | |
| 69 | + | ``` | |
| 70 | + | ||
| 71 | + | --- | |
| 72 | + | ||
| 73 | + | ## Video Formats | |
| 74 | + | ||
| 75 | + | ### Codec Properties | |
| 76 | + | ||
| 77 | + | | Codec | Container | MB/min (1080p, visually lossless) | Encoding speed | Browser support | | |
| 78 | + | |-------|-----------|-----------------------------------|----------------|-----------------| | |
| 79 | + | | H.264 | MP4 | 34-45 | Fast (baseline) | Universal | | |
| 80 | + | | H.265/HEVC | MP4 | 17-27 | 10-20x slower | Safari only (licensing) | | |
| 81 | + | | VP9 | WebM | 18-24 | 10-20x slower | Chrome, Firefox, Edge, Safari 14+ | | |
| 82 | + | | AV1 | MP4/WebM | 11-17 | 10-50x slower | Chrome, Firefox, Edge; Safari M3+ only | | |
| 83 | + | | ProRes | MOV | 100-200 | Fast (decode-optimized) | Safari only | | |
| 84 | + | ||
| 85 | + | ### Video Transparency Thresholds (CRF values, lower = better) | |
| 86 | + | ||
| 87 | + | | Codec | Transparent CRF | Notes | | |
| 88 | + | |-------|----------------|-------| | |
| 89 | + | | H.264 (x264) | CRF 18 | Well-established threshold | | |
| 90 | + | | H.265 (x265) | CRF 24 | Equivalent to x264 CRF 18 | | |
| 91 | + | | VP9 (libvpx) | CRF 18 | Range 0-63 | | |
| 92 | + | | AV1 (SVT-AV1) | CRF 23 | Range 0-63, equivalent to x264 CRF 19 | | |
| 93 | + | ||
| 94 | + | ### Compression Savings (same perceived quality, relative to H.264) | |
| 95 | + | ||
| 96 | + | | Codec | Size vs H.264 | Best for | | |
| 97 | + | |-------|---------------|----------| | |
| 98 | + | | H.265 | 40-50% smaller | Native apps, Apple ecosystem | | |
| 99 | + | | VP9 | 40-50% smaller | Web delivery (royalty-free) | | |
| 100 | + | | AV1 | 60-70% smaller | Web VOD (royalty-free, best compression) | | |
| 101 | + | ||
| 102 | + | ### Video Conversion Matrix | |
| 103 | + | ||
| 104 | + | | Source | H.264 CRF 18 | VP9 CRF 18 | AV1 CRF 23 | | |
| 105 | + | |--------|--------------|------------|-------------| | |
| 106 | + | | **MOV (ProRes)** | Transparent, saves ~80% | Transparent, saves ~85% | Transparent, saves ~90% | | |
| 107 | + | | **MOV (H.264)** | Remux only (no re-encode) | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) | | |
| 108 | + | | **MP4 (H.264)** | No-op | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) | | |
| 109 | + | | **MP4 (H.265)** | NEVER (bigger, lossy-to-lossy) | AVOID (lossy-to-lossy) | AVOID (lossy-to-lossy) | | |
| 110 | + | | **WebM (VP9)** | NEVER (bigger, lossy-to-lossy) | No-op | AVOID (lossy-to-lossy) | | |
| 111 | + | ||
| 112 | + | Note: "AVOID" means technically possible but always degrades quality. "NEVER" means it also increases file size. | |
| 113 | + | ||
| 114 | + | ### Video Decision Tree | |
| 115 | + | ||
| 116 | + | ``` | |
| 117 | + | Is the source lossless/production (ProRes, uncompressed)? | |
| 118 | + | YES: | |
| 119 | + | Tier = BigFiles/Streaming? | |
| 120 | + | -> Re-encode to H.264 CRF 18 in MP4 (universal playback) | |
| 121 | + | -> Optionally also generate VP9/AV1 for web streaming | |
| 122 | + | -> Keep original? Only if tier storage allows it | |
| 123 | + | Tier = SmallFiles? | |
| 124 | + | -> Re-encode to VP9 CRF 18 in WebM (transparent, ~85% smaller) | |
| 125 | + | -> Or AV1 CRF 23 (best compression, but slow encode) | |
| 126 | + | NO (source is already lossy H.264/H.265/VP9): | |
| 127 | + | Can we remux without re-encoding? (e.g., H.264 in MOV -> H.264 in MP4) | |
| 128 | + | YES -> Remux (zero quality loss, trivial CPU cost) | |
| 129 | + | NO -> Store as-is. No conversion possible without degradation. | |
| 130 | + | ``` | |
| 131 | + | ||
| 132 | + | --- | |
| 133 | + | ||
| 134 | + | ## Image Formats (for completeness) | |
| 135 | + | ||
| 136 | + | | Source | WebP (lossy 90) | WebP (lossless) | Notes | | |
| 137 | + | |--------|----------------|-----------------|-------| | |
| 138 | + | | **PNG** | Transparent, saves ~70-90% | Saves ~25-35% | Lossless-to-lossless safe | | |
| 139 | + | | **JPEG** | Marginal savings, quality loss | Larger than original | Store as-is | | |
| 140 | + | | **WebP** | No-op | No-op | Already optimal | | |
| 141 | + | | **GIF** | Transparent, saves ~50-80% | Saves ~20-40% | Can also convert to WebP anim | | |
| 142 | + | ||
| 143 | + | Cover images and media library images are small (10 MB cap). Savings are negligible at platform scale. Not worth the complexity. | |
| 144 | + | ||
| 145 | + | --- | |
| 146 | + | ||
| 147 | + | ## Tier-Based Storage Strategy | |
| 148 | + | ||
| 149 | + | ### SmallFiles ($20/mo) — "Transparent quality, efficient storage" | |
| 150 | + | ||
| 151 | + | | Upload type | Stored as | Savings vs original | | |
| 152 | + | |-------------|-----------|-------------------| | |
| 153 | + | | WAV/AIFF audio | Opus 128 kbps (or AAC 256) | ~90% smaller | | |
| 154 | + | | FLAC/ALAC audio | Opus 128 kbps (or AAC 256) | ~83% smaller | | |
| 155 | + | | Lossy audio (MP3/AAC/OGG) | As-is | None (already small) | | |
| 156 | + | | ProRes/uncompressed video | VP9 CRF 18 | ~85% smaller | | |
| 157 | + | | Lossy video (H.264/VP9) | As-is | None | | |
| 158 | + | ||
| 159 | + | Fan downloads: the stored version (transparent quality). | |
| 160 | + | ||
| 161 | + | ### BigFiles ($30/mo) — "Lossless preservation" | |
| 162 | + | ||
| 163 | + | | Upload type | Stored as | Savings vs original | | |
| 164 | + | |-------------|-----------|-------------------| | |
| 165 | + | | WAV/AIFF audio | FLAC + original preserved | ~50% on the FLAC copy | | |
| 166 | + | | FLAC/ALAC audio | As-is (already lossless compressed) | None needed | | |
| 167 | + | | Lossy audio (MP3/AAC/OGG) | As-is | None | | |
| 168 | + | | ProRes/uncompressed video | H.264 CRF 18 + original preserved | ~80% on the delivery copy | | |
| 169 | + | | Lossy video (H.264/VP9) | As-is | None | | |
| 170 | + | ||
| 171 | + | Fan downloads: choice of original lossless or delivery format. | |
| 172 | + | ||
| 173 | + | ### Streaming ($40/mo) — "Lossless + adaptive streaming" | |
| 174 | + | ||
| 175 | + | Same as BigFiles, plus: | |
| 176 | + | - Multiple quality tiers generated for adaptive streaming (HLS/DASH) | |
| 177 | + | - E.g., audio: FLAC + Opus 128 + Opus 64 | |
| 178 | + | - E.g., video: original + 1080p VP9 + 720p VP9 + 480p VP9 | |
| 179 | + | ||
| 180 | + | --- | |
| 181 | + | ||
| 182 | + | ## Storage Impact Estimates | |
| 183 | + | ||
| 184 | + | Per 4-minute stereo song (16-bit/44.1kHz source): | |
| 185 | + | ||
| 186 | + | | Strategy | Size | vs WAV original | | |
| 187 | + | |----------|------|-----------------| | |
| 188 | + | | Store WAV (current, no transcode) | ~40 MB | baseline | | |
| 189 | + | | Store FLAC (BigFiles ingest) | ~22 MB | 45% smaller | | |
| 190 | + | | Store Opus 128 (SmallFiles ingest) | ~3.8 MB | 90% smaller | | |
| 191 | + | | Store AAC 256 (SmallFiles alt) | ~7.7 MB | 81% smaller | | |
| 192 | + | ||
| 193 | + | Per 10-minute 1080p video (H.264 source at 8 Mbps): | |
| 194 | + | ||
| 195 | + | | Strategy | Size | Notes | | |
| 196 | + | |----------|------|-------| | |
| 197 | + | | Store as-is | ~600 MB | Already lossy, no savings | | |
| 198 | + | | Store ProRes original as VP9 | ~200 MB | Only saves if source is ProRes | | |
| 199 | + | ||
| 200 | + | Key insight: **the biggest wins are on lossless audio uploads (WAV/AIFF)**. Lossy uploads (which are likely the majority at launch) can't be improved. | |
| 201 | + | ||
| 202 | + | --- | |
| 203 | + | ||
| 204 | + | ## Implementation Notes | |
| 205 | + | ||
| 206 | + | ### Required tooling | |
| 207 | + | - `ffmpeg` / `ffprobe` on the server for all transcoding | |
| 208 | + | - Probe uploaded file on confirm to detect actual codec (M4A can be AAC or ALAC) | |
| 209 | + | - Transcoding is async (background job after upload confirm, not blocking) | |
| 210 | + | ||
| 211 | + | ### Probing before deciding | |
| 212 | + | - M4A: must probe to distinguish AAC (lossy, store as-is) from ALAC (lossless, can optimize) | |
| 213 | + | - MOV: must probe to distinguish ProRes (lossless, can optimize) from H.264 (lossy, store as-is) | |
| 214 | + | - OGG: almost always Vorbis (lossy), but could theoretically be FLAC-in-OGG | |
| 215 | + | ||
| 216 | + | ### What NOT to do | |
| 217 | + | - Never transcode lossy-to-lossy (MP3 -> AAC, H.264 -> VP9, etc.) | |
| 218 | + | - Never transcode lossy-to-lossless (MP3 -> FLAC) — bigger file, identical quality | |
| 219 | + | - Never re-encode at a higher bitrate thinking it improves quality (it doesn't) | |
| 220 | + | - Never delete the only copy before transcoding completes successfully | |
| 221 | + | ||
| 222 | + | --- | |
| 223 | + | ||
| 224 | + | ## Key Paths | |
| 225 | + | ||
| 226 | + | - Upload/validation logic: `src/storage.rs` | |
| 227 | + | - Upload endpoints: `src/routes/storage/uploads.rs` | |
| 228 | + | - Creator tier enforcement: `src/db/creator_tiers.rs` | |
| 229 | + | - Tier definitions: `src/db/enums.rs` (CreatorTier enum) | |
| 230 | + | - Phase 14D in todo: `docs/todo.md` |
| @@ -1,9 +1,9 @@ | |||
| 1 | 1 | # Makenotwork TODO | |
| 2 | 2 | ||
| 3 | 3 | ## Status | |
| 4 | - | Done: All pre-beta phases + frontend audit + content fingerprinting + bundled license text + video upload/playback + maintainability splits + S3 storage extraction + item sections + Phase 13D-A import system. Active: None. Next: Deploy v0.3.22, then post-beta features below. | |
| 4 | + | Done: All pre-beta phases + frontend audit + content fingerprinting + bundled license text + video upload/playback + maintainability splits + S3 storage extraction + item sections + Phase 13D-A import system + tag taxonomy expansion (migration 056) + OTA slug dashboard UI + codebase harness (MCP + SQLite index) + doc coverage remediation (10 new docs, rustdoc expansion, cross-linking) + content seeding (tags, blog posts, changelog project, collection) + doc deploy (all 55 pages verified) + DocEngine custom directives (extensible alerts + code tabs) + SyncKit/OTA tests pass (20/20 on astra). Active: Creator setup (Stripe), manual testing. Next: Soft launch. | |
| 5 | 5 | ||
| 6 | - | Live at makenot.work. v0.3.21. Audit grade A. Stripe + Postmark live. All platform integrations (I1-I5) deployed. | |
| 6 | + | Live at makenot.work. v0.3.22 (deployed 2026-04-10). Audit grade A. Stripe + Postmark live. All platform integrations (I1-I5) deployed. 34 tags in taxonomy. | |
| 7 | 7 | ||
| 8 | 8 | **Scope:** Sections tagged `(pre-beta)` ship before initial beta. Untagged sections are post-beta. | |
| 9 | 9 | ||
| @@ -11,6 +11,19 @@ Completed phases archived in `docs/archive/mnw_todo_done.md`. | |||
| 11 | 11 | ||
| 12 | 12 | --- | |
| 13 | 13 | ||
| 14 | + | ## Code Review Remediation (2026-04-12) | |
| 15 | + | ||
| 16 | + | ### Done | |
| 17 | + | - [x] Fix ServiceAuth to use `constant_time_compare()` (auth.rs:225) | |
| 18 | + | - [x] Fix 19 clippy warnings: collapsible_if → let chains (12 files), too_many_arguments (1), while→for (1), load test fix (1) | |
| 19 | + | - [x] helpers.rs (767) and pricing.rs (705) — confirmed within 500-line branching guideline (inflated by test suites, not branching logic) | |
| 20 | + | ||
| 21 | + | ### Deferred | |
| 22 | + | - [ ] Monitor scheduler.rs (635), git/mod.rs (613), license_keys.rs (684) for growth | |
| 23 | + | - [ ] Consider splitting bin/mnw-admin.rs git-auth commands into separate module | |
| 24 | + | ||
| 25 | + | --- | |
| 26 | + | ||
| 14 | 27 | ## External Blockers | |
| 15 | 28 | ||
| 16 | 29 | | Blocker | Status | Blocks | | |
| @@ -49,9 +62,9 @@ Copy and configuration pre-generated in `docs/content_seed.md`. | |||
| 49 | 62 | - [x] Set cover image | |
| 50 | 63 | - [x] Upload signed+notarized DMG | |
| 51 | 64 | - [ ] Create subscription tier: "Cloud Sync" ($3/mo) — not yet created | |
| 52 | - | - [ ] Add tags (planned: productivity, tasks, email, calendar, macos, desktop, rust) | |
| 53 | - | - [ ] Write launch blog post | |
| 54 | - | - [ ] Register OTA slug `goingson` | |
| 65 | + | - [x] Add tags (productivity, tasks, email, calendar, macos, desktop, rust) | |
| 66 | + | - [x] Write launch blog post | |
| 67 | + | - [x] Register OTA slug `goingson` | |
| 55 | 68 | ||
| 56 | 69 | #### Project: audiofiles (one-time purchase) | |
| 57 | 70 | - [x] Create project (slug: audiofiles) | |
| @@ -60,10 +73,10 @@ Copy and configuration pre-generated in `docs/content_seed.md`. | |||
| 60 | 73 | - [x] Set cover image | |
| 61 | 74 | - [x] Upload signed+notarized plugin bundle | |
| 62 | 75 | - [ ] Enable license keys (test activation flow) | |
| 63 | - | - [ ] Add tags (planned: audio, samples, plugin, clap, vst3, music-production, macos, daw) | |
| 64 | - | - [ ] Write launch blog post | |
| 76 | + | - [x] Add tags (audio, samples, plugin, clap, vst3, music-production, macos, daw) | |
| 77 | + | - [x] Write launch blog post | |
| 65 | 78 | - [ ] Create a test discount code (e.g. LAUNCH50, 50% off) | |
| 66 | - | - [ ] Register OTA slug `audiofiles` | |
| 79 | + | - [x] Register OTA slug `audiofiles` | |
| 67 | 80 | ||
| 68 | 81 | #### Project: Balanced Breakfast | |
| 69 | 82 | - [x] Create project (slug: balanced-breakfast) | |
| @@ -71,26 +84,41 @@ Copy and configuration pre-generated in `docs/content_seed.md`. | |||
| 71 | 84 | - [x] Create content item | |
| 72 | 85 | - [x] Set cover image | |
| 73 | 86 | - [x] Upload signed+notarized DMG | |
| 74 | - | - [ ] Add tags (planned: rss, feeds, reader, macos, desktop, rust) | |
| 75 | - | - [ ] Write launch blog post | |
| 76 | - | - [ ] Register OTA slug `balanced-breakfast` | |
| 87 | + | - [x] Add tags (rss, feeds, reader, macos, desktop, rust) | |
| 88 | + | - [x] Write launch blog post | |
| 89 | + | - [x] Register OTA slug `balanced-breakfast` | |
| 77 | 90 | ||
| 78 | 91 | #### Project: Changelog (platform development log) | |
| 79 | 92 | - [x] `/changelog` and `/changelog/{post_slug}` route aliases (`routes/pages/blog.rs`, `constants.rs`) | |
| 80 | - | - [ ] Create project (slug: changelog, type: blog) | |
| 81 | - | - [ ] Write description | |
| 82 | - | - [ ] Write "What this is" blog post | |
| 93 | + | - [x] Create project (slug: changelog, type: blog) | |
| 94 | + | - [x] Write description | |
| 95 | + | - [x] Write "What this is" blog post | |
| 83 | 96 | ||
| 84 | 97 | #### Cross-Project | |
| 85 | - | - [ ] Create "All Apps" collection (GO + AF + BB) | |
| 98 | + | - [x] Create "All Apps" collection (GO + AF + BB) | |
| 86 | 99 | - [ ] Add custom links (source code link, support@makenot.work — currently profile has Twitter/Mastodon/htpy.app) | |
| 87 | - | - [ ] Verify all 4 projects appear on `/discover` (currently 3 — changelog blog missing) | |
| 100 | + | - [x] Verify all 4 projects appear on `/discover` | |
| 88 | 101 | - [ ] Test free download flow (GO), PWYW flow (BB), purchase flow (AF), subscription flow (GO) | |
| 89 | 102 | - [ ] Test discount code on AF purchase | |
| 90 | 103 | - [ ] Test license key delivery after AF purchase | |
| 91 | 104 | - [ ] Capture screenshots for docs (dashboard, audio player, discover, pricing, git browser) | |
| 92 | 105 | ||
| 93 | 106 | ### Documentation | |
| 107 | + | ||
| 108 | + | #### Done | |
| 109 | + | - [x] Rustdoc expansion: added s3-storage and theme-common to `deploy/generate-rustdoc.sh` (now 5 crates) | |
| 110 | + | - [x] Inline doc expansion: scheduler.rs, monitor.rs `//!` headers expanded to 4-5 lines | |
| 111 | + | - [x] Cross-linking: 11 source files now have `//! See also:` linking to public docs | |
| 112 | + | - [x] New guide docs: security, blog, collections, export, promo-codes, git, custom-domains, mailing-lists, fan-plus | |
| 113 | + | - [x] New developer doc: sse (SyncKit SSE push notifications) | |
| 114 | + | - [x] SUBCATEGORIES updated in `docs.rs` with new slugs + "Advanced" subcategory | |
| 115 | + | ||
| 116 | + | #### Remaining | |
| 117 | + | - [x] Register `tech` section in `main.rs` DocLoader config (7 tech/ docs exist but weren't served) | |
| 118 | + | - [x] Deploy docs to production (`deploy.sh --config` — rsync site-docs/, static/, rustdoc) | |
| 119 | + | - [x] Verify all 55 doc pages render at `makenot.work/docs/{slug}` | |
| 120 | + | - [x] Verify all internal links resolve (no 404s) | |
| 121 | + | - [ ] Review new docs against live UI for accuracy (button labels, navigation paths) | |
| 94 | 122 | - [ ] liability.md legal review (has [PENDING LEGAL REVIEW] placeholders) | |
| 95 | 123 | - [ ] dmca-counter.md designated agent address (needs DMCA agent registration) | |
| 96 | 124 | ||
| @@ -241,8 +269,8 @@ Per-item license configuration: creators pick from 7 presets or write custom ter | |||
| 241 | 269 | ||
| 242 | 270 | Competitive context: Gumroad has per-product affiliates with configurable rates. Bandcamp and itch.io have no affiliate programs. This is a moderate gap — most valuable for software and course creators. | |
| 243 | 271 | ||
| 244 | - | ### Phase 13B: Labels — Remaining | |
| 245 | - | - [ ] Publish/update reminder: show applied labels summary when publishing | |
| 272 | + | ### Phase 13B: Labels | |
| 273 | + | - [x] Publish/update reminder: show applied labels summary when publishing | |
| 246 | 274 | ||
| 247 | 275 | ### Phase 13D: Creator Platform Import System | |
| 248 | 276 | Migration 055. 28 unit tests + 8 integration tests. Three-phase build: A (infra + CSV), B (Substack + Ghost), C (Gumroad + Bandcamp + Lemon Squeezy + Patreon). | |
| @@ -284,14 +312,42 @@ Migration 053. 6 integration tests. | |||
| 284 | 312 | - [x] Scanning: content-type verification for video | |
| 285 | 313 | - [x] Data export includes video fields | |
| 286 | 314 | ||
| 287 | - | ### Phase 14E: Video Streaming Tier Features (post-beta) | |
| 288 | - | Streaming tier ($40/mo) premium features. Trigger: >100 creators with video content. | |
| 289 | - | - [ ] Server-side transcoding (ffmpeg): upload any format, serve optimized MP4/WebM | |
| 315 | + | ### Phase 14E: Media Transcoding Pipeline (post-beta) | |
| 316 | + | Tier-based ingest transcoding. Reference: `docs/filetype_matrix.md`. | |
| 317 | + | ||
| 318 | + | Core rule: never transcode lossy-to-lossy or lossy-to-lossless. Only optimize lossless sources. | |
| 319 | + | ||
| 320 | + | #### Phase 14E-1: Probe + Detect Infrastructure | |
| 321 | + | - [ ] Add `ffprobe` to production server (detect codec inside M4A/MOV containers) | |
| 322 | + | - [ ] Probe uploaded files at confirm time: extract codec, bitrate, sample rate, duration, dimensions | |
| 323 | + | - [ ] Classify uploads as lossless (WAV/AIFF/FLAC/ALAC/ProRes) or lossy (MP3/AAC/OGG/Opus/H.264/VP9) | |
| 324 | + | - [ ] Auto-populate `duration_seconds`, `video_width`, `video_height` from probe data | |
| 325 | + | - [ ] Store detected codec + source classification in DB (new columns or metadata JSON) | |
| 326 | + | ||
| 327 | + | #### Phase 14E-2: Audio Transcoding (SmallFiles tier) | |
| 328 | + | - [ ] Background job: lossless audio uploads -> Opus 128 kbps (SmallFiles) or FLAC (BigFiles+) | |
| 329 | + | - [ ] WAV/AIFF -> FLAC saves ~50%; WAV/AIFF -> Opus 128 saves ~90% | |
| 330 | + | - [ ] FLAC/ALAC -> Opus 128 saves ~83% (SmallFiles only) | |
| 331 | + | - [ ] Lossy uploads (MP3/AAC/OGG/Opus): store as-is, no conversion | |
| 332 | + | - [ ] Track `original_s3_key` + `delivery_s3_key` per file (originals kept for BigFiles+) | |
| 333 | + | ||
| 334 | + | #### Phase 14E-3: Fan Download Format Choice | |
| 335 | + | - [ ] Fan chooses download format at purchase time (original lossless, FLAC, MP3 320, AAC 256) | |
| 336 | + | - [ ] Pre-generate common formats on first request, cache in S3 | |
| 337 | + | - [ ] SmallFiles: delivery format only (transparent Opus/AAC). BigFiles+: original + delivery formats | |
| 338 | + | ||
| 339 | + | #### Phase 14E-4: Video Transcoding | |
| 340 | + | - [ ] ProRes/uncompressed MOV -> H.264 CRF 18 MP4 (universal playback) | |
| 341 | + | - [ ] Optionally generate VP9 for web streaming (40-50% smaller than H.264) | |
| 342 | + | - [ ] Lossy video (H.264/VP9/H.265): store as-is, no re-encode | |
| 343 | + | - [ ] Remux where possible (H.264 in MOV -> H.264 in MP4, zero quality loss) | |
| 344 | + | - [ ] Auto-generated thumbnails (ffmpeg frame extraction at configurable timestamp) | |
| 345 | + | ||
| 346 | + | #### Phase 14E-5: Streaming Tier Features | |
| 290 | 347 | - [ ] Adaptive bitrate streaming (HLS/DASH) — multiple quality levels per video | |
| 291 | - | - [ ] Background lossless re-encoding: re-mux uploaded videos to more efficient containers, optimize keyframes, strip unused metadata. No quality loss, just smaller files | |
| 348 | + | - [ ] Audio: FLAC + Opus 128 + Opus 64 quality ladder | |
| 349 | + | - [ ] Video: original + 1080p + 720p + 480p quality ladder (VP9 or AV1) | |
| 292 | 350 | - [ ] Bandwidth metering + per-tier bandwidth caps | |
| 293 | - | - [ ] Auto-generated thumbnails (ffmpeg frame extraction at configurable timestamp) | |
| 294 | - | - [ ] Video duration auto-detection on upload (ffprobe) | |
| 295 | 351 | ||
| 296 | 352 | ### Phase 14B: Embeddable Widgets | |
| 297 | 353 | - [ ] Embed endpoint /embed/i/{uuid} — embeddable buy button, audio player, 30-sec preview | |
| @@ -300,10 +356,8 @@ Streaming tier ($40/mo) premium features. Trigger: >100 creators with video cont | |||
| 300 | 356 | ||
| 301 | 357 | Competitive context: Gumroad has overlay + inline + WordPress plugin. Bandcamp has customizable player widget. itch.io has purchase widget + playable game embed. This is a significant gap for creators selling from their own sites. | |
| 302 | 358 | ||
| 303 | - | ### Phase 14D: Audio Format Transcoding | |
| 304 | - | - [ ] Upload lossless (WAV/AIFF/FLAC), serve in multiple formats (MP3 320, FLAC, AAC, OGG) | |
| 305 | - | - [ ] Fan chooses download format at purchase time | |
| 306 | - | - [ ] Server-side transcoding pipeline (ffmpeg or similar) | |
| 359 | + | ### Phase 14D: Audio Format Transcoding — Merged into 14E | |
| 360 | + | Superseded by Phase 14E (Media Transcoding Pipeline), which covers audio + video transcoding with tier-based strategy. See `docs/filetype_matrix.md` for the full format compatibility matrix. | |
| 307 | 361 | ||
| 308 | 362 | Competitive context: Bandcamp accepts WAV/AIFF/FLAC only, then transcodes to 8 download formats. MNW currently serves files as uploaded — a musician uploading WAV can't offer MP3 to fans who want it. | |
| 309 | 363 | ||
| @@ -384,8 +438,8 @@ $8/mo consumer subscription. $5 monthly credit, `+` badge, platform polls, dev c | |||
| 384 | 438 | - [ ] Dev community as private MT community (invite-only, restricted to Fan+ subscribers — reuse MT private communities feature) | |
| 385 | 439 | ||
| 386 | 440 | ### DocEngine — Remaining | |
| 387 | - | Extraction done (2026-03-21). `Shared/docengine/`, 100 tests, all 5 projects migrated. | |
| 388 | - | - [ ] Custom section directives (callouts, warnings, code tabs) | |
| 441 | + | Extraction done (2026-03-21). `Shared/docengine/`, 141 tests, all 5 projects migrated. | |
| 442 | + | - [x] Custom section directives: extensible `[!TYPE]` alerts (any uppercase word) + `[!TABS]` code tabs with language-labelled tab bar | |
| 389 | 443 | - [ ] Full-text search index (build at load time, JSON endpoint for client-side search) | |
| 390 | 444 | - [ ] Versioned docs (directory per version, version switcher) | |
| 391 | 445 | ||
| @@ -434,9 +488,10 @@ Archive policy: items on platform 12+ months stay hosted if creator cancels. | |||
| 434 | 488 | - [ ] Verify Postmark DKIM selector `20170907043118pm` in Postmark dashboard (returned empty in Run 6 DNS check) | |
| 435 | 489 | ||
| 436 | 490 | ## Dependencies (blocked on upstream) | |
| 491 | + | - [ ] Monitor yara-x for wasmtime >=42.0.2 (11 CVEs including 2 critical — RUSTSEC-2026-0095, -0096) | |
| 437 | 492 | - [ ] Monitor aws-sdk-s3 for lru fix (RUSTSEC-2026-0002) | |
| 438 | 493 | - [ ] Monitor async-stripe for instant fix (RUSTSEC-2024-0384) | |
| 439 | - | - [ ] rsa (RUSTSEC-2023-0071) via sqlx-mysql — non-issue (MNW uses Postgres) | |
| 494 | + | - [ ] rsa (RUSTSEC-2023-0071) via sqlx-mysql + yara-x — no fix available | |
| 440 | 495 | ||
| 441 | 496 | ## Deferred | |
| 442 | 497 | - [ ] Corporate structure: holding company (Makecreative Holdings LLC) with subsidiary LLCs for liability isolation if itsall.work launches. Setup checklist, Stripe contingency plan, entity details documented. | |
| @@ -470,10 +525,14 @@ MNW/src/ | |||
| 470 | 525 | import/ (CSV converter, pipeline, intermediate format) | |
| 471 | 526 | MNW/tests/ | |
| 472 | 527 | integration.rs, harness/, workflows/*.rs | |
| 473 | - | MNW/migrations/ (001-055) | |
| 528 | + | MNW/migrations/ (001-056) | |
| 474 | 529 | MNW/templates/ | |
| 475 | 530 | MNW/deploy/ | |
| 476 | 531 | MNW/site-docs/public/, MNW/site-docs/unpublished/ | |
| 532 | + | _meta/harness/ (codebase index: SQLite DB + MCP server) | |
| 533 | + | codebase.db, server/src/{index,populate,schema}.ts | |
| 534 | + | MNW/docs/audit.md (cross-project audit instructions, harness-integrated) | |
| 535 | + | MNW/docs/filetype_matrix.md (format compatibility, transcoding rules, tier strategy) | |
| 477 | 536 | ``` | |
| 478 | 537 | ||
| 479 | 538 | ## Deps |
| @@ -0,0 +1,15 @@ | |||
| 1 | + | -- Media library: user-scoped files for embedding in markdown content. | |
| 2 | + | CREATE TABLE media_files ( | |
| 3 | + | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| 4 | + | user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, | |
| 5 | + | folder VARCHAR(100) NOT NULL DEFAULT '', | |
| 6 | + | filename VARCHAR(255) NOT NULL, | |
| 7 | + | s3_key VARCHAR(500) NOT NULL UNIQUE, | |
| 8 | + | content_type VARCHAR(100) NOT NULL, | |
| 9 | + | file_size_bytes BIGINT NOT NULL DEFAULT 0, | |
| 10 | + | media_type VARCHAR(10) NOT NULL CHECK (media_type IN ('image', 'video')), | |
| 11 | + | created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| 12 | + | ); | |
| 13 | + | ||
| 14 | + | CREATE INDEX idx_media_files_user_folder ON media_files(user_id, folder); | |
| 15 | + | CREATE UNIQUE INDEX idx_media_files_user_folder_name ON media_files(user_id, folder, filename); |
| @@ -0,0 +1,87 @@ | |||
| 1 | + | # SyncKit SSE Push Notifications | |
| 2 | + | ||
| 3 | + | SyncKit provides a Server-Sent Events (SSE) endpoint that notifies connected clients in real time when new data is available. Instead of polling, your app can subscribe to a persistent connection and pull only when something changes. | |
| 4 | + | ||
| 5 | + | ## Endpoint | |
| 6 | + | ||
| 7 | + | ``` | |
| 8 | + | GET /api/sync/subscribe?app_id={app_id} | |
| 9 | + | Authorization: Bearer <token> | |
| 10 | + | ``` | |
| 11 | + | ||
| 12 | + | Requires a valid SyncKit JWT token. The `app_id` query parameter specifies which sync app to subscribe to. | |
| 13 | + | ||
| 14 | + | ## Event Format | |
| 15 | + | ||
| 16 | + | When another device pushes changes, all other connected devices for the same app and user receive a `changed` event: | |
| 17 | + | ||
| 18 | + | ``` | |
| 19 | + | event: changed | |
| 20 | + | data: | |
| 21 | + | ``` | |
| 22 | + | ||
| 23 | + | The event carries no payload. Data content is always encrypted end-to-end and never transmitted over the SSE channel. On receiving a `changed` event, the client should initiate a standard pull to fetch the actual changes. | |
| 24 | + | ||
| 25 | + | ## Keepalive | |
| 26 | + | ||
| 27 | + | The server sends a comment line every 30 seconds to keep the connection alive and prevent proxy timeouts: | |
| 28 | + | ||
| 29 | + | ``` | |
| 30 | + | : keepalive | |
| 31 | + | ``` | |
| 32 | + | ||
| 33 | + | This is a standard SSE comment and is ignored by conforming clients. | |
| 34 | + | ||
| 35 | + | ## Client Integration | |
| 36 | + | ||
| 37 | + | ### Basic Flow | |
| 38 | + | ||
| 39 | + | 1. Authenticate and obtain a SyncKit JWT token | |
| 40 | + | 2. Open an SSE connection to `/api/sync/subscribe?app_id={app_id}` | |
| 41 | + | 3. On receiving a `changed` event, call the pull endpoint to fetch new changes | |
| 42 | + | 4. Reconnect on connection drop (most SSE libraries handle this automatically) | |
| 43 | + | ||
| 44 | + | ### With the Rust SDK | |
| 45 | + | ||
| 46 | + | The `synckit-client` crate provides `SyncNotifyStream` for SSE integration: | |
| 47 | + | ||
| 48 | + | ```rust | |
| 49 | + | let stream = client.subscribe_notifications().await?; | |
| 50 | + | ||
| 51 | + | // In your sync loop: | |
| 52 | + | while let Some(notification) = stream.next().await { | |
| 53 | + | match notification { | |
| 54 | + | SyncNotification::Changed => { | |
| 55 | + | client.pull().await?; | |
| 56 | + | } | |
| 57 | + | SyncNotification::Keepalive => {} | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ``` | |
| 61 | + | ||
| 62 | + | The SDK handles reconnection, keepalive parsing, and token refresh automatically. | |
| 63 | + | ||
| 64 | + | ### Selective Sync | |
| 65 | + | ||
| 66 | + | Combine SSE notifications with `PullFilter` to pull only specific tables or time ranges: | |
| 67 | + | ||
| 68 | + | ```rust | |
| 69 | + | let filter = PullFilter::new() | |
| 70 | + | .table("tasks") | |
| 71 | + | .since(last_sync_time); | |
| 72 | + | ||
| 73 | + | let changes = client.pull_filtered(filter).await?; | |
| 74 | + | ``` | |
| 75 | + | ||
| 76 | + | This reduces bandwidth and processing when your app only needs a subset of changes. | |
| 77 | + | ||
| 78 | + | ## Design Notes | |
| 79 | + | ||
| 80 | + | - One SSE connection per (app, user) pair | |
| 81 | + | - The broadcast channel buffers up to 16 messages. If a client falls behind, missed events are skipped — the next pull will catch up regardless. | |
| 82 | + | - SSE works through standard HTTP proxies and load balancers. The 30-second keepalive interval is chosen to stay within common proxy timeout defaults. | |
| 83 | + | ||
| 84 | + | ## See Also | |
| 85 | + | ||
| 86 | + | - [SyncKit Cloud Sync](./synckit.md) — Full sync API reference | |
| 87 | + | - [OAuth2 PKCE](./oauth.md) — Authentication for SyncKit apps |
| @@ -0,0 +1,46 @@ | |||
| 1 | + | # Blog Posts | |
| 2 | + | ||
| 3 | + | Every project can have a blog for updates, announcements, behind-the-scenes writing, and anything you want to share with your audience. Blog posts are separate from items — they are free-form writing, not releases. | |
| 4 | + | ||
| 5 | + | ## Creating a Blog Post | |
| 6 | + | ||
| 7 | + | 1. Go to your project dashboard | |
| 8 | + | 2. Click "New Blog Post" | |
| 9 | + | 3. Enter a title and write your content in Markdown | |
| 10 | + | 4. Choose to publish immediately or schedule for later | |
| 11 | + | ||
| 12 | + | Blog posts support full Markdown: headings, lists, links, images, code blocks, and inline formatting. | |
| 13 | + | ||
| 14 | + | ## Scheduling | |
| 15 | + | ||
| 16 | + | Set a future publish date and time to schedule a post. Scheduled posts are not visible to the public until their publish time arrives. The background scheduler checks periodically and publishes them automatically. | |
| 17 | + | ||
| 18 | + | From the dashboard you can see all scheduled posts and their publish dates. Edit or reschedule at any time before publication. | |
| 19 | + | ||
| 20 | + | ## Discussion Threads | |
| 21 | + | ||
| 22 | + | When a blog post is published, a discussion thread is automatically created on Multithreaded (the integrated forum). A link to the thread appears at the bottom of the blog post, and readers can comment there. | |
| 23 | + | ||
| 24 | + | ## The Changelog | |
| 25 | + | ||
| 26 | + | The platform changelog at `/changelog` is a special blog tied to the Makenot.work project itself. It uses the same blog system. If you navigate to `/changelog`, you are reading the platform's own blog. | |
| 27 | + | ||
| 28 | + | Your project blogs live at `/p/your-project/blog`. | |
| 29 | + | ||
| 30 | + | ## RSS | |
| 31 | + | ||
| 32 | + | Blog posts are included in your project's blog RSS feed at `/p/your-project/blog/rss`. Anyone subscribed to that feed gets notified when you publish a new post. | |
| 33 | + | ||
| 34 | + | The blog RSS feed is separate from the project's item RSS feed at `/p/your-project/rss`. | |
| 35 | + | ||
| 36 | + | ## Visibility | |
| 37 | + | ||
| 38 | + | - **Published posts** are visible to anyone | |
| 39 | + | - **Draft posts** are visible only to you (the project owner) | |
| 40 | + | - **Scheduled posts** are visible only to you until their publish time | |
| 41 | + | ||
| 42 | + | ## See Also | |
| 43 | + | ||
| 44 | + | - [RSS Feeds](./rss.md) — How RSS works for items and blog posts | |
| 45 | + | - [Content & Items](./02-content.md) — Creating and managing items | |
| 46 | + | - [Projects](./projects.md) — Project setup and management |
| @@ -0,0 +1,46 @@ | |||
| 1 | + | # Collections | |
| 2 | + | ||
| 3 | + | Collections let you group items across projects into curated lists. Use them for "Best of" compilations, themed bundles, staff picks, or any other grouping that cuts across your normal project structure. | |
| 4 | + | ||
| 5 | + | ## Creating a Collection | |
| 6 | + | ||
| 7 | + | 1. Go to your dashboard | |
| 8 | + | 2. Click "New Collection" | |
| 9 | + | 3. Enter a title, an optional description, and a URL slug | |
| 10 | + | 4. Choose whether the collection is public or private | |
| 11 | + | ||
| 12 | + | You can create up to 100 collections. | |
| 13 | + | ||
| 14 | + | ## Adding Items | |
| 15 | + | ||
| 16 | + | Add any of your published (public) items to a collection: | |
| 17 | + | ||
| 18 | + | 1. Open the collection from your dashboard | |
| 19 | + | 2. Click "Add Item" | |
| 20 | + | 3. Select from your published items | |
| 21 | + | ||
| 22 | + | Each collection can hold up to 1,000 items. Only public items can be added — draft or unlisted items are not eligible. | |
| 23 | + | ||
| 24 | + | You can also add items to collections from the item edit page, which shows a checklist of your existing collections. | |
| 25 | + | ||
| 26 | + | ## Reordering | |
| 27 | + | ||
| 28 | + | Items within a collection can be reordered by drag-and-drop on the collection edit page. The order you set is the order fans see. | |
| 29 | + | ||
| 30 | + | ## Public URLs | |
| 31 | + | ||
| 32 | + | Public collections are visible at your profile. Fans can browse the collection and access any item in it. Private collections are only visible to you. | |
| 33 | + | ||
| 34 | + | ## Editing and Deleting | |
| 35 | + | ||
| 36 | + | From the collection edit page you can: | |
| 37 | + | ||
| 38 | + | - **Update** the title, description, or visibility | |
| 39 | + | - **Remove** individual items without affecting the items themselves | |
| 40 | + | - **Delete** the entire collection (items are not deleted, only the grouping) | |
| 41 | + | ||
| 42 | + | ## See Also | |
| 43 | + | ||
| 44 | + | - [Content & Items](./02-content.md) — Creating and managing items | |
| 45 | + | - [Tags](./tags.md) — Organizing items with tags | |
| 46 | + | - [Projects](./projects.md) — Project-level organization |
| @@ -0,0 +1,53 @@ | |||
| 1 | + | # Custom Domains | |
| 2 | + | ||
| 3 | + | You can point your own domain at your Makenot.work profile so fans reach you at `yourdomain.com` instead of `makenot.work/u/username`. | |
| 4 | + | ||
| 5 | + | ## Setup | |
| 6 | + | ||
| 7 | + | ### 1. Add Your Domain | |
| 8 | + | ||
| 9 | + | 1. Go to Settings > Domain | |
| 10 | + | 2. Enter your domain (e.g., `shop.yourdomain.com` or `yourdomain.com`) | |
| 11 | + | 3. Click "Add" | |
| 12 | + | ||
| 13 | + | Domain names must be valid hostnames: letters, numbers, hyphens, and at least one TLD. You cannot add `makenot.work` or its subdomains. | |
| 14 | + | ||
| 15 | + | ### 2. Verify Ownership | |
| 16 | + | ||
| 17 | + | Add a DNS TXT record to prove you own the domain: | |
| 18 | + | ||
| 19 | + | | Record Type | Host | Value | | |
| 20 | + | |-------------|------|-------| | |
| 21 | + | | TXT | `_mnw-verify.yourdomain.com` | `mnw-verify-{verification-code}` | | |
| 22 | + | ||
| 23 | + | The verification code is shown on the settings page after adding your domain. | |
| 24 | + | ||
| 25 | + | After adding the DNS record, return to Settings > Domain and click "Verify." The platform checks via DNS-over-HTTPS (Cloudflare resolver) so propagation is usually fast. | |
| 26 | + | ||
| 27 | + | ### 3. SSL Certificate | |
| 28 | + | ||
| 29 | + | Once verified, SSL is handled automatically. The platform uses on-demand TLS — a certificate is issued for your domain the first time a visitor connects. No manual certificate configuration needed. | |
| 30 | + | ||
| 31 | + | ## DNS Configuration | |
| 32 | + | ||
| 33 | + | Point your domain to Makenot.work by adding a CNAME record: | |
| 34 | + | ||
| 35 | + | | Record Type | Host | Value | | |
| 36 | + | |-------------|------|-------| | |
| 37 | + | | CNAME | `yourdomain.com` (or subdomain) | `makenot.work` | | |
| 38 | + | ||
| 39 | + | If your DNS provider does not allow a CNAME on the apex domain, use their CNAME flattening or ALIAS record feature, or use a subdomain like `shop.yourdomain.com`. | |
| 40 | + | ||
| 41 | + | ## Managing Your Domain | |
| 42 | + | ||
| 43 | + | From Settings > Domain you can: | |
| 44 | + | ||
| 45 | + | - **View** your current domain and verification status | |
| 46 | + | - **Remove** your domain to revert to the default `makenot.work/u/username` URL | |
| 47 | + | ||
| 48 | + | One domain per account. To change domains, remove the current one and add the new one. | |
| 49 | + | ||
| 50 | + | ## See Also | |
| 51 | + | ||
| 52 | + | - [Profile](./profile.md) — Editing your public profile | |
| 53 | + | - [Getting Started](./01-getting-started.md) — Initial account setup |
| @@ -0,0 +1,60 @@ | |||
| 1 | + | # Data Export | |
| 2 | + | ||
| 3 | + | You can export all of your data from Makenot.work at any time. No lock-in, no friction — your data is yours. | |
| 4 | + | ||
| 5 | + | ## What You Can Export | |
| 6 | + | ||
| 7 | + | | Export | Format | Contents | | |
| 8 | + | |--------|--------|----------| | |
| 9 | + | | Projects & Items | JSON | All projects, items, tags, chapters, versions, license keys, promo codes, and blog posts | | |
| 10 | + | | Sales | CSV | Date, item, amount, status, and buyer email for every sale | | |
| 11 | + | | Purchases | CSV | Date, item, amount, and status for everything you have bought | | |
| 12 | + | | Followers & Subscribers | CSV | Usernames, display names, types, status, and subscription dates | | |
| 13 | + | | Content Files | ZIP | All uploaded files (audio, video, covers, versions, insertion clips) with a manifest | | |
| 14 | + | ||
| 15 | + | ## How to Export | |
| 16 | + | ||
| 17 | + | 1. Go to Settings > Data | |
| 18 | + | 2. Choose the export type | |
| 19 | + | 3. Click "Export" | |
| 20 | + | ||
| 21 | + | JSON and CSV exports download immediately. Content file exports are assembled into a ZIP archive and provided as a temporary download link (valid for one hour). | |
| 22 | + | ||
| 23 | + | ## Content File Export | |
| 24 | + | ||
| 25 | + | The content file export bundles all files you have uploaded to the platform. It includes a `README.txt` manifest listing every file and its original context. | |
| 26 | + | ||
| 27 | + | Limits: | |
| 28 | + | ||
| 29 | + | - Maximum 500 files per export | |
| 30 | + | - Maximum 2 GB total size | |
| 31 | + | ||
| 32 | + | If you have more content than these limits allow, export in batches by project. | |
| 33 | + | ||
| 34 | + | ## Export Format Details | |
| 35 | + | ||
| 36 | + | ### Projects JSON | |
| 37 | + | ||
| 38 | + | The JSON export is a single file containing a complete snapshot of your creative work: | |
| 39 | + | ||
| 40 | + | - Projects with descriptions, settings, and metadata | |
| 41 | + | - Items with all fields: title, description, pricing, visibility, tags, chapters, versions | |
| 42 | + | - Blog posts with full Markdown content and publish status | |
| 43 | + | - License keys associated with each item | |
| 44 | + | - Promo codes you have created | |
| 45 | + | ||
| 46 | + | This format is designed to be readable and portable. You can parse it with any JSON tool. | |
| 47 | + | ||
| 48 | + | ### CSV Exports | |
| 49 | + | ||
| 50 | + | CSV files use standard comma-separated format with a header row. They open directly in any spreadsheet application. | |
| 51 | + | ||
| 52 | + | ## Platform Portability | |
| 53 | + | ||
| 54 | + | Data export is a core commitment. If you ever decide to leave, you take everything with you — content files, metadata, sales history, and audience data. Nothing is held hostage. | |
| 55 | + | ||
| 56 | + | ## See Also | |
| 57 | + | ||
| 58 | + | - [Content & Items](./02-content.md) — What gets exported | |
| 59 | + | - [Selling & Audience](./03-selling.md) — Sales and revenue data | |
| 60 | + | - [Migration](./migration.md) — Moving to Makenot.work from another platform |
| @@ -0,0 +1,40 @@ | |||
| 1 | + | # Fan+ Subscription | |
| 2 | + | ||
| 3 | + | Fan+ is a platform-wide subscription for fans who want to support the Makenot.work ecosystem and get access to premium features. | |
| 4 | + | ||
| 5 | + | ## What Fans Get | |
| 6 | + | ||
| 7 | + | Fan+ subscribers receive: | |
| 8 | + | ||
| 9 | + | - **Credit system** — Monthly credits that can be used toward purchases on the platform | |
| 10 | + | - **Premium access** — Features and perks that are gated behind Fan+ membership | |
| 11 | + | - **Platform support** — Direct contribution to keeping the platform running with 0% creator fees | |
| 12 | + | ||
| 13 | + | Fan+ is separate from creator subscription tiers. A fan can subscribe to individual creators *and* be a Fan+ member — they serve different purposes. | |
| 14 | + | ||
| 15 | + | ## How It Works | |
| 16 | + | ||
| 17 | + | ### Subscribing | |
| 18 | + | ||
| 19 | + | Fans subscribe to Fan+ from their account settings. Payment is handled through Stripe on a monthly billing cycle. | |
| 20 | + | ||
| 21 | + | ### Managing the Subscription | |
| 22 | + | ||
| 23 | + | From account settings, fans can: | |
| 24 | + | ||
| 25 | + | - View their current subscription status and billing period | |
| 26 | + | - See when the current period started and when it renews | |
| 27 | + | - Cancel at any time | |
| 28 | + | ||
| 29 | + | ### Cancellation | |
| 30 | + | ||
| 31 | + | When a fan cancels, they retain Fan+ benefits until the end of their current billing period. There are no partial refunds — the subscription simply does not renew. | |
| 32 | + | ||
| 33 | + | ## For Creators | |
| 34 | + | ||
| 35 | + | Fan+ subscriptions are managed entirely by the platform. As a creator, you do not need to configure anything. The `is_fan_plus` status is visible in the user's session data, so platform features can check it, but it does not affect your pricing, payouts, or audience management. | |
| 36 | + | ||
| 37 | + | ## See Also | |
| 38 | + | ||
| 39 | + | - [Selling & Audience](./03-selling.md) — Creator monetization overview | |
| 40 | + | - [Pricing Models](./pricing.md) — Creator subscription tiers (separate from Fan+) |
| @@ -0,0 +1,55 @@ | |||
| 1 | + | # Git Source Browser | |
| 2 | + | ||
| 3 | + | Makenot.work includes a built-in git source browser. Host your project's source code alongside your releases — fans and collaborators can browse code, read commit history, and view diffs without leaving the platform. | |
| 4 | + | ||
| 5 | + | ## Creating a Repository | |
| 6 | + | ||
| 7 | + | Repositories are created by pushing to the platform via SSH. There is no web UI for creating empty repos — push and the repository appears automatically. | |
| 8 | + | ||
| 9 | + | ### SSH Setup | |
| 10 | + | ||
| 11 | + | 1. Go to Settings > SSH Keys | |
| 12 | + | 2. Add your public key | |
| 13 | + | 3. Push to your repo: | |
| 14 | + | ||
| 15 | + | ``` | |
| 16 | + | git remote add mnw git@ssh.makenot.work:username/repo-name.git | |
| 17 | + | git push mnw main | |
| 18 | + | ``` | |
| 19 | + | ||
| 20 | + | The remote URL format is `git@ssh.makenot.work:username/repo-name.git`. | |
| 21 | + | ||
| 22 | + | ## Browsing Code | |
| 23 | + | ||
| 24 | + | Your repository is available at `/git/username/repo-name`. The source browser provides: | |
| 25 | + | ||
| 26 | + | - **Tree view** — Browse files and directories at any ref (branch, tag, or commit) | |
| 27 | + | - **File view** — Read individual files with syntax highlighting | |
| 28 | + | - **Commit log** — Paginated history (25 commits per page) for any branch | |
| 29 | + | - **Commit detail** — Full diff with inline additions and deletions (capped at 100 files / 10,000 lines) | |
| 30 | + | - **Blame view** — Per-line commit attribution | |
| 31 | + | - **Per-file history** — Commit log filtered to a single file's changes | |
| 32 | + | ||
| 33 | + | The repository overview shows the tree at HEAD and renders your README if one exists. | |
| 34 | + | ||
| 35 | + | ## Visibility | |
| 36 | + | ||
| 37 | + | Repositories can be public or private: | |
| 38 | + | ||
| 39 | + | - **Public** — Anyone can browse the source. Listed on your profile and the explore page. | |
| 40 | + | - **Private** — Only you (the owner) can view. Does not appear in listings. | |
| 41 | + | ||
| 42 | + | Set visibility from the repository settings page. | |
| 43 | + | ||
| 44 | + | ## Explore Page | |
| 45 | + | ||
| 46 | + | The public explore page at `/git` lists all public repositories across the platform, paginated 15 per page. It serves as a discovery feed for source-available projects. | |
| 47 | + | ||
| 48 | + | ## Linking Releases | |
| 49 | + | ||
| 50 | + | If your repository is associated with a project, releases published through the platform are linked in the repository view. Readers can navigate between the source and the downloadable release. | |
| 51 | + | ||
| 52 | + | ## See Also | |
| 53 | + | ||
| 54 | + | - [Content & Items](./02-content.md) — Publishing releases | |
| 55 | + | - [Projects](./projects.md) — Project setup |
| @@ -0,0 +1,62 @@ | |||
| 1 | + | # Mailing Lists | |
| 2 | + | ||
| 3 | + | Each project on Makenot.work has built-in mailing lists for communicating with your audience. No third-party email service needed. | |
| 4 | + | ||
| 5 | + | ## List Types | |
| 6 | + | ||
| 7 | + | Every project automatically gets two mailing lists: | |
| 8 | + | ||
| 9 | + | | List | Purpose | | |
| 10 | + | |------|---------| | |
| 11 | + | | **Content** | New releases and content updates | | |
| 12 | + | | **Devlog** | Development updates and behind-the-scenes | | |
| 13 | + | ||
| 14 | + | Lists are created automatically when you create a project. No setup required. | |
| 15 | + | ||
| 16 | + | ## Subscribers | |
| 17 | + | ||
| 18 | + | People subscribe to your mailing lists by following your project. When someone follows a project, they are subscribed to its content list by default. | |
| 19 | + | ||
| 20 | + | Subscriber requirements: | |
| 21 | + | ||
| 22 | + | - Verified email address | |
| 23 | + | - Account not suspended | |
| 24 | + | - Email not suppressed (bounced or complained) | |
| 25 | + | ||
| 26 | + | The subscriber cap is 10,000 per list. | |
| 27 | + | ||
| 28 | + | ## Sending a Broadcast | |
| 29 | + | ||
| 30 | + | To send a broadcast email to all your followers: | |
| 31 | + | ||
| 32 | + | 1. Go to your dashboard | |
| 33 | + | 2. Click "Broadcast" | |
| 34 | + | 3. Write a subject (up to 200 characters) and body (up to 5,000 characters) | |
| 35 | + | 4. Send | |
| 36 | + | ||
| 37 | + | Broadcasts are plain-text emails. Each email includes an unsubscribe link. | |
| 38 | + | ||
| 39 | + | Rate limit: one broadcast per 24 hours. | |
| 40 | + | ||
| 41 | + | ## Automatic Notifications | |
| 42 | + | ||
| 43 | + | Beyond manual broadcasts, the platform sends automatic emails through mailing lists when: | |
| 44 | + | ||
| 45 | + | - You publish a new item (content list) | |
| 46 | + | - You publish a blog post (content list) | |
| 47 | + | ||
| 48 | + | These are triggered by the scheduler and do not count against your broadcast rate limit. | |
| 49 | + | ||
| 50 | + | ## Unsubscribing | |
| 51 | + | ||
| 52 | + | Fans can unsubscribe from individual lists via the link in any email, or by unfollowing your project (which unsubscribes from all of that project's lists). | |
| 53 | + | ||
| 54 | + | ## Importing Subscribers | |
| 55 | + | ||
| 56 | + | You can import external email addresses (people without Makenot.work accounts) to your mailing lists. This is useful when migrating from another platform. | |
| 57 | + | ||
| 58 | + | ## See Also | |
| 59 | + | ||
| 60 | + | - [RSS Feeds](./rss.md) — Alternative subscription method | |
| 61 | + | - [Projects](./projects.md) — Project setup and management | |
| 62 | + | - [Selling & Audience](./03-selling.md) — Building your audience |
| @@ -0,0 +1,73 @@ | |||
| 1 | + | # Promo Codes | |
| 2 | + | ||
| 3 | + | Promo codes let you offer discounts, free trials, and free access to your content. Create them from your dashboard and share them with fans, press, collaborators, or anyone you choose. | |
| 4 | + | ||
| 5 | + | ## Code Types | |
| 6 | + | ||
| 7 | + | ### Discount Codes | |
| 8 | + | ||
| 9 | + | Reduce the price of an item, project, or subscription tier. | |
| 10 | + | ||
| 11 | + | - **Percentage discount** — 1% to 100% off | |
| 12 | + | - **Fixed discount** — A specific dollar amount off the price | |
| 13 | + | ||
| 14 | + | ### Free Trial Codes | |
| 15 | + | ||
| 16 | + | Give temporary access to subscription content. | |
| 17 | + | ||
| 18 | + | - Trial length: 1 to 365 days | |
| 19 | + | - Fan subscribes at the end of the trial or access expires | |
| 20 | + | ||
| 21 | + | ### Free Access Codes | |
| 22 | + | ||
| 23 | + | Grant permanent free access to a specific item. These use auto-generated word-based codes (easier to share verbally or in print). | |
| 24 | + | ||
| 25 | + | When a fan claims a free access code for an item that has license keys enabled, a license key is automatically generated for them. | |
| 26 | + | ||
| 27 | + | ## Creating a Promo Code | |
| 28 | + | ||
| 29 | + | 1. Go to your dashboard > Promo Codes | |
| 30 | + | 2. Click "Create Code" | |
| 31 | + | 3. Choose the type (Discount, Free Trial, or Free Access) | |
| 32 | + | 4. Set the parameters: | |
| 33 | + | - **Discount**: percentage or fixed amount | |
| 34 | + | - **Free Trial**: number of days | |
| 35 | + | - **Free Access**: select the item | |
| 36 | + | 5. Optionally set a scope, expiry date, and usage limit | |
| 37 | + | 6. Click "Create" | |
| 38 | + | ||
| 39 | + | Discount and free trial codes use custom alphanumeric codes you define. Free access codes are auto-generated with memorable word combinations. | |
| 40 | + | ||
| 41 | + | ## Scope | |
| 42 | + | ||
| 43 | + | Codes can be scoped to different levels: | |
| 44 | + | ||
| 45 | + | | Scope | Effect | | |
| 46 | + | |-------|--------| | |
| 47 | + | | Creator-wide | Applies to anything you sell | | |
| 48 | + | | Project | Applies to items within a specific project | | |
| 49 | + | | Item | Applies to a single item only | | |
| 50 | + | | Tier | Applies to a specific subscription tier | | |
| 51 | + | ||
| 52 | + | ## Expiry and Limits | |
| 53 | + | ||
| 54 | + | - **Expiry date** — Optional. Set a date (YYYY-MM-DD) after which the code stops working. Expires at end of day UTC. | |
| 55 | + | - **Usage limit** — Optional. Set the maximum number of times the code can be redeemed. | |
| 56 | + | ||
| 57 | + | Codes with no expiry and no limit work indefinitely. | |
| 58 | + | ||
| 59 | + | ## Managing Codes | |
| 60 | + | ||
| 61 | + | From your dashboard you can: | |
| 62 | + | ||
| 63 | + | - **List** all your promo codes with redemption counts | |
| 64 | + | - **Delete** a code to deactivate it immediately | |
| 65 | + | ||
| 66 | + | ## How Fans Use Codes | |
| 67 | + | ||
| 68 | + | Fans enter the promo code during checkout. For free access codes, fans can claim at a dedicated claim page without going through checkout. | |
| 69 | + | ||
| 70 | + | ## See Also | |
| 71 | + | ||
| 72 | + | - [Selling & Audience](./03-selling.md) — Pricing models and monetization overview | |
| 73 | + | - [Pricing Models](./pricing.md) — Detailed pricing guide |
| @@ -0,0 +1,85 @@ | |||
| 1 | + | # Account Security | |
| 2 | + | ||
| 3 | + | Makenot.work supports two-factor authentication (2FA) to protect your account. You can use passkeys, a TOTP authenticator app, or both. | |
| 4 | + | ||
| 5 | + | ## Passkeys | |
| 6 | + | ||
| 7 | + | Passkeys use the WebAuthn standard to let you log in with a fingerprint, face scan, hardware key, or device PIN — no password needed during login. | |
| 8 | + | ||
| 9 | + | ### Setting Up a Passkey | |
| 10 | + | ||
| 11 | + | 1. Go to Settings > Security | |
| 12 | + | 2. Click "Add Passkey" | |
| 13 | + | 3. Follow your browser/device prompt to create the credential | |
| 14 | + | 4. Give it a name (e.g., "MacBook Touch ID", "YubiKey") | |
| 15 | + | ||
| 16 | + | You can register up to 20 passkeys per account. | |
| 17 | + | ||
| 18 | + | ### Logging In with a Passkey | |
| 19 | + | ||
| 20 | + | On the login page, click "Use Passkey" instead of entering your password. Your browser will prompt you to verify with the registered device. Passkey login is inherently two-factor — it proves both identity and device possession — so you will not be prompted for a TOTP code. | |
| 21 | + | ||
| 22 | + | ### Managing Passkeys | |
| 23 | + | ||
| 24 | + | From Settings > Security you can: | |
| 25 | + | ||
| 26 | + | - **List** all registered passkeys with their creation dates | |
| 27 | + | - **Rename** a passkey for easier identification | |
| 28 | + | - **Delete** a passkey (requires password confirmation) | |
| 29 | + | ||
| 30 | + | If you lose access to all your passkeys, you can still log in with your password (plus TOTP if enabled). | |
| 31 | + | ||
| 32 | + | ## TOTP Authenticator App | |
| 33 | + | ||
| 34 | + | Time-based One-Time Password (TOTP) adds a six-digit rotating code from an authenticator app as a second factor after your password. | |
| 35 | + | ||
| 36 | + | ### Setting Up TOTP | |
| 37 | + | ||
| 38 | + | 1. Go to Settings > Security | |
| 39 | + | 2. Click "Enable Authenticator App" | |
| 40 | + | 3. Scan the QR code with your authenticator app (or enter the secret manually) | |
| 41 | + | 4. Enter the six-digit code from your app to confirm setup | |
| 42 | + | 5. Save your backup codes immediately | |
| 43 | + | ||
| 44 | + | Compatible apps include 1Password, Bitwarden, Authy, Google Authenticator, and any TOTP-compliant app (RFC 6238, SHA-1, 6 digits, 30-second interval). | |
| 45 | + | ||
| 46 | + | ### Logging In with TOTP | |
| 47 | + | ||
| 48 | + | 1. Enter your email and password as usual | |
| 49 | + | 2. When prompted, enter the current six-digit code from your authenticator app | |
| 50 | + | ||
| 51 | + | If you registered a passkey, passkey login bypasses the TOTP step entirely. | |
| 52 | + | ||
| 53 | + | ### Disabling TOTP | |
| 54 | + | ||
| 55 | + | Go to Settings > Security and click "Disable Authenticator App." You will need to confirm your password. | |
| 56 | + | ||
| 57 | + | ## Backup Codes | |
| 58 | + | ||
| 59 | + | When you enable TOTP, you receive 10 single-use backup codes. Each code is 8 characters and can be used exactly once in place of a TOTP code. | |
| 60 | + | ||
| 61 | + | **Store backup codes securely.** If you lose access to your authenticator app and have no backup codes, account recovery requires contacting support. | |
| 62 | + | ||
| 63 | + | ### Regenerating Backup Codes | |
| 64 | + | ||
| 65 | + | Go to Settings > Security > "Regenerate Backup Codes." This invalidates all previous codes and generates a fresh set of 10. You must confirm your password. | |
| 66 | + | ||
| 67 | + | ## Login Notifications | |
| 68 | + | ||
| 69 | + | If you have multiple active sessions and a new login occurs, you will receive an email notification. This is automatic — no configuration needed. | |
| 70 | + | ||
| 71 | + | ## Account Lockout | |
| 72 | + | ||
| 73 | + | After 5 consecutive failed password attempts, your account is locked for 15 minutes. Passkey authentication is not affected by password lockout. | |
| 74 | + | ||
| 75 | + | ## Recommendations | |
| 76 | + | ||
| 77 | + | - Enable at least one 2FA method (passkeys are the strongest option) | |
| 78 | + | - Register multiple passkeys on different devices so you are never locked out | |
| 79 | + | - If using TOTP, save your backup codes in a password manager or printed in a secure location | |
| 80 | + | - Use a unique, strong password even if you primarily log in with passkeys | |
| 81 | + | ||
| 82 | + | ## See Also | |
| 83 | + | ||
| 84 | + | - [Getting Started](./01-getting-started.md) — Account creation and initial setup | |
| 85 | + | - [Profile](./profile.md) — Editing your public profile |
| @@ -33,6 +33,7 @@ use crate::config::Config; | |||
| 33 | 33 | use crate::constants; | |
| 34 | 34 | use crate::db::{self, UserId, UserSessionId, Username}; | |
| 35 | 35 | use crate::error::AppError; | |
| 36 | + | use crate::helpers::constant_time_compare; | |
| 36 | 37 | ||
| 37 | 38 | /// Session key for storing user data | |
| 38 | 39 | const USER_SESSION_KEY: &str = "user"; | |
| @@ -222,7 +223,7 @@ impl FromRequestParts<crate::AppState> for ServiceAuth { | |||
| 222 | 223 | .and_then(|v| v.strip_prefix("Bearer ")) | |
| 223 | 224 | .ok_or(AppError::Unauthorized)?; | |
| 224 | 225 | ||
| 225 | - | if header != expected { | |
| 226 | + | if !constant_time_compare(header, expected) { | |
| 226 | 227 | return Err(AppError::Unauthorized); | |
| 227 | 228 | } | |
| 228 | 229 |