max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
268 files changed,
+796 insertions,
-931 deletions
| @@ -40,11 +40,11 @@ | |||
| 40 | 40 | #[error("Not found")] | |
| 41 | 41 | NotFound, // 404 | |
| 42 | 42 | #[error("Bad request: {0}")] | |
| 43 | - | BadRequest(String), // 400 — message shown to user | |
| 43 | + | BadRequest(String), // 400, message shown to user | |
| 44 | 44 | #[error("Database error: {0}")] | |
| 45 | - | Database(#[from] sqlx::Error), // 500 — generic message to user | |
| 45 | + | Database(#[from] sqlx::Error), // 500, generic message to user | |
| 46 | 46 | #[error("Internal server error")] | |
| 47 | - | Internal(#[from] anyhow::Error), // 500 — generic message to user | |
| 47 | + | Internal(#[from] anyhow::Error), // 500, generic message to user | |
| 48 | 48 | // ... see error.rs for full list | |
| 49 | 49 | } | |
| 50 | 50 | ``` | |
| @@ -93,10 +93,10 @@ | |||
| 93 | 93 | Full-page handlers extend `base.html` and include `session_user`, `csrf_token`, navigation, etc. HTMX handlers return partial templates (HTML fragments without the base layout). | |
| 94 | 94 | ||
| 95 | 95 | ```rust | |
| 96 | - | // Full page — extends base.html | |
| 96 | + | // Full page, extends base.html | |
| 97 | 97 | Ok(FullPageTemplate { csrf_token, session_user: maybe_user, /* ... */ }) | |
| 98 | 98 | ||
| 99 | - | // HTMX fragment — standalone partial, no base layout | |
| 99 | + | // HTMX fragment, standalone partial, no base layout | |
| 100 | 100 | Ok(FilteredEntriesTemplate { items, current_page, total_pages }) | |
| 101 | 101 | ``` | |
| 102 | 102 | ||
| @@ -275,7 +275,7 @@ | |||
| 275 | 275 | ||
| 276 | 276 | HTMX fragment templates do NOT extend `base.html`. They render standalone HTML fragments: | |
| 277 | 277 | ```html | |
| 278 | - | {# No extends — this is a partial #} | |
| 278 | + | {# No extends. This is a partial #} | |
| 279 | 279 | {% for item in items %} | |
| 280 | 280 | <div class="item-row">{{ item.title }}</div> | |
| 281 | 281 | {% endfor %} | |
| @@ -336,17 +336,17 @@ | |||
| 336 | 336 | ||
| 337 | 337 | ``` | |
| 338 | 338 | static/ | |
| 339 | - | mnw.js — core utilities (CSRF, toasts, tabs, shortcuts) — loaded globally | |
| 340 | - | upload.js — S3 upload (S3Uploader, initDropzone) — loaded globally | |
| 341 | - | passkey.js — WebAuthn registration/login | |
| 342 | - | insertions.js — clip management | |
| 343 | - | wizard.js — wizard navigation | |
| 344 | - | docs-search.js — doc search index | |
| 345 | - | item-details.js — bundle, section, tag management | |
| 346 | - | item-upload.js — audio + version upload flows | |
| 347 | - | blog-editor.js — blog save/autosave/publish | |
| 348 | - | style.css — main stylesheet | |
| 349 | - | wizard.css — wizard-specific styles | |
| 339 | + | mnw.js core utilities (CSRF, toasts, tabs, shortcuts), loaded globally | |
| 340 | + | upload.js S3 upload (S3Uploader, initDropzone), loaded globally | |
| 341 | + | passkey.js WebAuthn registration/login | |
| 342 | + | insertions.js clip management | |
| 343 | + | wizard.js wizard navigation | |
| 344 | + | docs-search.js doc search index | |
| 345 | + | item-details.js bundle, section, tag management | |
| 346 | + | item-upload.js audio + version upload flows | |
| 347 | + | blog-editor.js blog save/autosave/publish | |
| 348 | + | style.css main stylesheet | |
| 349 | + | wizard.css wizard-specific styles | |
| 350 | 350 | ``` | |
| 351 | 351 | ||
| 352 | 352 | Only `mnw.js`, `upload.js`, and `htmx.min.js` are loaded globally (in `base.html` / `_head_assets.html`). All other JS files are loaded via `{% block scripts %}` in the page that needs them. |
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | version = "0.11.0" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "LICENSE" | |
| 6 | - | # Server binary — never published to a registry. Marks the crate private so | |
| 6 | + | # Server binary: never published to a registry. Marks the crate private so | |
| 7 | 7 | # supply-chain tooling (cargo-deny) treats its first-party path deps correctly. | |
| 8 | 8 | publish = false | |
| 9 | 9 | ||
| @@ -66,7 +66,7 @@ | |||
| 66 | 66 | webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "conditional-ui"] } | |
| 67 | 67 | webauthn-rs-proto = "0.5" | |
| 68 | 68 | ||
| 69 | - | # OpenSSL (transitive dep from git2, webauthn-rs — vendored for cross-compilation) | |
| 69 | + | # OpenSSL (transitive dep from git2, webauthn-rs: vendored for cross-compilation) | |
| 70 | 70 | openssl = { version = "0.10", features = ["vendored"] } | |
| 71 | 71 | ||
| 72 | 72 | # Security | |
| @@ -136,10 +136,10 @@ | |||
| 136 | 136 | # S3 Storage | |
| 137 | 137 | s3-storage = { path = "../shared/s3-storage" } | |
| 138 | 138 | ||
| 139 | - | # Stripe Payments — async-stripe 1.0.0-rc.6 (split into sub-crates). | |
| 139 | + | # Stripe Payments: async-stripe 1.0.0-rc.6 (split into sub-crates). | |
| 140 | 140 | # The umbrella `async-stripe` provides the HTTP client; resource types live in | |
| 141 | 141 | # per-domain sub-crates. The `deserialize` feature on each resource crate is | |
| 142 | - | # required to derive `serde::Deserialize` on Subscription, Invoice, etc — we | |
| 142 | + | # required to derive `serde::Deserialize` on Subscription, Invoice, etc: we | |
| 143 | 143 | # parse them from webhook payloads ourselves (no built-in webhook helper in the | |
| 144 | 144 | # rc line; see `payments::webhooks::verify_signature` for our HMAC check). | |
| 145 | 145 | async-stripe = { version = "1.0.0-rc.6", features = ["default-tls"] } | |
| @@ -183,7 +183,7 @@ | |||
| 183 | 183 | # on 0.10.19 (25.1%) for no runtime cost. Every tier rsyncs that difference on | |
| 184 | 184 | # every promote. Nothing here reads a symbolized backtrace (no `backtrace` crate, | |
| 185 | 185 | # no RUST_BACKTRACE in the unit file). If a panic ever does need symbolizing, | |
| 186 | - | # rebuild the sha with `--config profile.release.strip=false` — the build is | |
| 186 | + | # rebuild the sha with `--config profile.release.strip=false`: the build is | |
| 187 | 187 | # reproducible from the git sha the release was cut from. | |
| 188 | 188 | strip = true | |
| 189 | 189 |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | # cargo-deny configuration — supply-chain gate for the MNW server. | |
| 1 | + | # cargo-deny configuration: supply-chain gate for the MNW server. | |
| 2 | 2 | # | |
| 3 | 3 | # Run by Sando's `cargo_deny` gate as `cargo deny check` (all four checks). | |
| 4 | 4 | # This complements `cargo audit`: `bans` surfaces duplicate-version clusters | |
| @@ -12,22 +12,22 @@ | |||
| 12 | 12 | ||
| 13 | 13 | [advisories] | |
| 14 | 14 | version = 2 | |
| 15 | - | # Mirror of `.cargo/audit.toml` — every entry is a transitive advisory we cannot | |
| 15 | + | # Mirror of `.cargo/audit.toml`: every entry is a transitive advisory we cannot | |
| 16 | 16 | # resolve by bumping our own direct deps, kept in sync with the cargo-audit | |
| 17 | 17 | # posture. Directly-fixable advisories are fixed in Cargo.toml, never parked here. | |
| 18 | 18 | ignore = [ | |
| 19 | - | "RUSTSEC-2023-0071", # rsa Marvin timing side-channel — only via signature *verification* crates; we never decrypt with rsa. | |
| 20 | - | "RUSTSEC-2025-0141", # bincode unmaintained — transitive tooling, no code change available. | |
| 21 | - | "RUSTSEC-2020-0095", # difference unmaintained — via a dev/test dep. | |
| 22 | - | "RUSTSEC-2024-0436", # paste unmaintained — via proc-macro deps. | |
| 23 | - | "RUSTSEC-2025-0134", # rustls-pemfile unmaintained — via AWS SDK TLS. | |
| 19 | + | "RUSTSEC-2023-0071", # rsa Marvin timing side-channel, only via signature *verification* crates; we never decrypt with rsa. | |
| 20 | + | "RUSTSEC-2025-0141", # bincode unmaintained, transitive tooling, no code change available. | |
| 21 | + | "RUSTSEC-2020-0095", # difference unmaintained, via a dev/test dep. | |
| 22 | + | "RUSTSEC-2024-0436", # paste unmaintained, via proc-macro deps. | |
| 23 | + | "RUSTSEC-2025-0134", # rustls-pemfile unmaintained, via AWS SDK TLS. | |
| 24 | 24 | ] | |
| 25 | 25 | ||
| 26 | 26 | [bans] | |
| 27 | 27 | # Duplicate versions are the supply-chain smell the audits track (x509/crypto | |
| 28 | 28 | # cluster, rustls 0.21/0.23 dual stack via the AWS SDK). Surface them as | |
| 29 | - | # warnings rather than failing the build — they are transitive and not yet | |
| 30 | - | # de-duplicable — so a *new* duplicate is visible in CI output without blocking | |
| 29 | + | # warnings rather than failing the build: they are transitive and not yet | |
| 30 | + | # de-duplicable: so a *new* duplicate is visible in CI output without blocking | |
| 31 | 31 | # a deploy. Promote to "deny" with a `skip` list once the tree is de-duped. | |
| 32 | 32 | multiple-versions = "warn" | |
| 33 | 33 | wildcards = "deny" # a `*` version requirement on any dependency fails the build |
| @@ -1,14 +1,14 @@ | |||
| 1 | - | # cargo-audit configuration — triaged advisory posture for the MNW server. | |
| 1 | + | # cargo-audit configuration, triaged advisory posture for the MNW server. | |
| 2 | 2 | # | |
| 3 | 3 | # Every ignore below is a transitive advisory we cannot resolve by bumping our | |
| 4 | 4 | # own direct deps, each with a rationale and (where relevant) the upstream we're | |
| 5 | - | # waiting on. Directly-fixable advisories are NOT parked here — they get fixed in | |
| 5 | + | # waiting on. Directly-fixable advisories are NOT parked here, they get fixed in | |
| 6 | 6 | # Cargo.toml/Cargo.lock (e.g. the ammonia mXSS RUSTSEC-2026-0193 was closed by | |
| 7 | 7 | # bumping to 4.1.3, not ignored). Re-review this list on every dependency audit. | |
| 8 | 8 | ||
| 9 | 9 | [advisories] | |
| 10 | 10 | ignore = [ | |
| 11 | - | # rsa — Marvin timing side-channel on RSA *decryption*. Pulled only by the | |
| 11 | + | # rsa, Marvin timing side-channel on RSA *decryption*. Pulled only by the | |
| 12 | 12 | # signature-*verification* crates (apple-codesign, authenticode, yara-x); we | |
| 13 | 13 | # never decrypt with rsa, so the decryption-oracle attack does not apply. | |
| 14 | 14 | "RUSTSEC-2023-0071", | |
| @@ -21,9 +21,9 @@ | |||
| 21 | 21 | ||
| 22 | 22 | # Unmaintained-crate warnings, all transitive (no direct dep, no code change | |
| 23 | 23 | # available to us). Tracked for when upstreams migrate off them. | |
| 24 | - | "RUSTSEC-2025-0141", # bincode (unmaintained) — via transitive tooling | |
| 25 | - | "RUSTSEC-2020-0095", # difference (unmaintained) — via a dev/test dep | |
| 26 | - | "RUSTSEC-2024-0436", # paste (unmaintained) — via proc-macro deps | |
| 27 | - | "RUSTSEC-2025-0134", # rustls-pemfile (unmaintained) — via AWS SDK TLS | |
| 28 | - | "RUSTSEC-2026-0173", # proc-macro-error2 (unmaintained) — via a macro dep | |
| 24 | + | "RUSTSEC-2025-0141", # bincode (unmaintained), via transitive tooling | |
| 25 | + | "RUSTSEC-2020-0095", # difference (unmaintained), via a dev/test dep | |
| 26 | + | "RUSTSEC-2024-0436", # paste (unmaintained), via proc-macro deps | |
| 27 | + | "RUSTSEC-2025-0134", # rustls-pemfile (unmaintained), via AWS SDK TLS | |
| 28 | + | "RUSTSEC-2026-0173", # proc-macro-error2 (unmaintained), via a macro dep | |
| 29 | 29 | ] |
| @@ -37,7 +37,7 @@ | |||
| 37 | 37 | # Reverse proxy to application (includes /docs routes) | |
| 38 | 38 | reverse_proxy localhost:3000 | |
| 39 | 39 | ||
| 40 | - | # Security headers (CSP is set by the app — do not duplicate here) | |
| 40 | + | # Security headers (CSP is set by the app, do not duplicate here) | |
| 41 | 41 | header { | |
| 42 | 42 | Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" | |
| 43 | 43 | } | |
| @@ -78,7 +78,7 @@ | |||
| 78 | 78 | } | |
| 79 | 79 | ||
| 80 | 80 | # Creator custom pages (u.makenot.work). | |
| 81 | - | # Same app process as the apex — the server's host-dispatch middleware renders | |
| 81 | + | # Same app process as the apex; the server's host-dispatch middleware renders | |
| 82 | 82 | # sanitized creator HTML/CSS here, keeps this origin cookieless, and sets its own | |
| 83 | 83 | # strict CSP (do not duplicate CSP here). TLS is the wildcard *.makenot.work | |
| 84 | 84 | # Origin CA cert via cloudflare_tls. | |
| @@ -135,7 +135,7 @@ | |||
| 135 | 135 | } | |
| 136 | 136 | } | |
| 137 | 137 | ||
| 138 | - | # CDN for free content downloads — reverse-proxies to Hetzner Object Storage. | |
| 138 | + | # CDN for free content downloads: reverse-proxies to Hetzner Object Storage. | |
| 139 | 139 | # Cloudflare caches responses at the edge (free egress). Origin only hit on cache miss. | |
| 140 | 140 | # Requires: S3 bucket policy allowing public s3:GetObject, Cloudflare DNS A record (proxy ON). | |
| 141 | 141 | cdn.makenot.work { | |
| @@ -167,7 +167,7 @@ | |||
| 167 | 167 | } | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | - | # dl.maxj.phd download host retired 2026-06-09 — MNW now serves all downloads | |
| 170 | + | # dl.maxj.phd download host retired 2026-06-09. MNW now serves all downloads | |
| 171 | 171 | # (creator product pages / makenot.work DMGs). The maxjphd_tls mTLS snippet and | |
| 172 | 172 | # the dl.maxj.phd file_server block were removed with it; the /etc/caddy/maxj-phd-origin* | |
| 173 | 173 | # cert/key are now unused on prod and can be deleted there. | |
| @@ -187,7 +187,7 @@ | |||
| 187 | 187 | redir https://makenot.work{uri} permanent | |
| 188 | 188 | } | |
| 189 | 189 | ||
| 190 | - | # Custom domains — on-demand TLS via Let's Encrypt. | |
| 190 | + | # Custom domains: on-demand TLS via Let's Encrypt. | |
| 191 | 191 | # Caddy calls /api/domains/caddy-ask before issuing a cert for any domain. | |
| 192 | 192 | # makenot.work subdomains are unaffected (matched by explicit blocks above | |
| 193 | 193 | # which use Cloudflare Origin CA + mTLS). | |
| @@ -199,10 +199,10 @@ | |||
| 199 | 199 | # Custom domains connect directly to the origin (no Cloudflare mTLS in front), | |
| 200 | 200 | # so any client-supplied CF-Connecting-IP / X-Forwarded-For is forgeable. The | |
| 201 | 201 | # app trusts CF-Connecting-IP for rate-limiting, lockouts, and audit logs, so | |
| 202 | - | # overwrite it with the real TCP peer and strip XFF before proxying — a client | |
| 202 | + | # overwrite it with the real TCP peer and strip XFF before proxying, a client | |
| 203 | 203 | # can no longer mint fake source IPs to evade per-IP throttles or poison logs. | |
| 204 | 204 | reverse_proxy localhost:3000 { | |
| 205 | - | # Set (replace) CF-Connecting-IP to the real TCP peer — overwrites any | |
| 205 | + | # Set (replace) CF-Connecting-IP to the real TCP peer, overwrites any | |
| 206 | 206 | # value the client sent. Strip X-Forwarded-For so no forged value reaches | |
| 207 | 207 | # the app (the app ignores XFF anyway; this is hygiene). | |
| 208 | 208 | header_up CF-Connecting-IP {http.request.remote.host} | |
| @@ -223,7 +223,7 @@ | |||
| 223 | 223 | } | |
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | - | # HTTP catch-all — redirect to HTTPS (also needed for ACME HTTP-01 challenges) | |
| 226 | + | # HTTP catch-all: redirect to HTTPS (also needed for ACME HTTP-01 challenges) | |
| 227 | 227 | :80 { | |
| 228 | 228 | redir https://{host}{uri} permanent | |
| 229 | 229 | } |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | # SSH access — production server (Hetzner) | |
| 1 | + | # SSH access: production server (Hetzner) | |
| 2 | 2 | ||
| 3 | 3 | Two SSH paths into the production server, with different audiences and | |
| 4 | 4 | different break-glass behavior. Read this before disabling either one. | |
| @@ -12,7 +12,7 @@ | |||
| 12 | 12 | ||
| 13 | 13 | - **Public :22** is intentionally open so creators can `git clone`/`push` | |
| 14 | 14 | over SSH against `ssh.makenot.work`. The sshd config on this port is | |
| 15 | - | locked to git-shell only — see `setup-git-ssh.sh` and `sshd-git.conf`. | |
| 15 | + | locked to git-shell only. See `setup-git-ssh.sh` and `sshd-git.conf`. | |
| 16 | 16 | No interactive shell, no port forwarding, no admin access. | |
| 17 | 17 | - **Tailnet :2200** is the admin path. Full interactive shell, used for | |
| 18 | 18 | every `deploy.sh` invocation and any manual maintenance. Reachable only | |
| @@ -23,8 +23,8 @@ | |||
| 23 | 23 | ||
| 24 | 24 | The audit-flagged risk was disabling Tailscale SSH (the admin path) | |
| 25 | 25 | without first verifying the public sshd was still functional. Tailscale | |
| 26 | - | runs its own SSH server when configured; if that goes down — Tailscale | |
| 27 | - | service crashes, ACL misconfiguration, accidental `tailscale down` — | |
| 26 | + | runs its own SSH server when configured; if that goes down (Tailscale | |
| 27 | + | service crashes, ACL misconfiguration, accidental `tailscale down`) | |
| 28 | 28 | you can lose admin access entirely if you've also locked down public | |
| 29 | 29 | sshd. | |
| 30 | 30 | ||
| @@ -41,7 +41,7 @@ | |||
| 41 | 41 | ||
| 42 | 42 | 1. **Verify public sshd is up** from any machine: | |
| 43 | 43 | `ssh -p 22 root@5.78.144.244 -o BatchMode=yes -o ConnectTimeout=5 true` | |
| 44 | - | Expect a key-based prompt or a refused git-shell — both prove sshd | |
| 44 | + | Expect a key-based prompt or a refused git-shell. Both prove sshd | |
| 45 | 45 | is listening. A timeout or "Connection refused" means public sshd is | |
| 46 | 46 | ALSO down and you need Hetzner Cloud Console. | |
| 47 | 47 | 2. **Edit `/etc/ssh/sshd_config.d/git-shell.conf` from Hetzner Console** | |
| @@ -50,18 +50,18 @@ | |||
| 50 | 50 | 3. **Restart sshd**: `systemctl restart ssh`. Test from your laptop. | |
| 51 | 51 | 4. **Fix the tailnet path** (re-auth `tailscale up`, restore key, etc). | |
| 52 | 52 | 5. **Revert the sshd edit** and restart `ssh` again. Don't leave the | |
| 53 | - | interactive root shell on public :22 — it defeats the whole split. | |
| 53 | + | interactive root shell on public :22. It defeats the whole split. | |
| 54 | 54 | ||
| 55 | 55 | ## What NOT to do | |
| 56 | 56 | ||
| 57 | 57 | - **Do not** disable Tailscale SSH (`tailscale set --ssh=false`) without | |
| 58 | 58 | first proving public :22 is reachable and you have a working root key | |
| 59 | - | for it. Memory rule: `feedback_tailscale_ssh` — getting locked out | |
| 59 | + | for it. Memory rule: `feedback_tailscale_ssh`, getting locked out | |
| 60 | 60 | requires Hetzner Console access, which costs time we don't always | |
| 61 | 61 | have. | |
| 62 | - | - **Do not** restrict public :22 to specific IPs without coordinating — | |
| 62 | + | - **Do not** restrict public :22 to specific IPs without coordinating: | |
| 63 | 63 | the mnw-cli git endpoint serves users worldwide. | |
| 64 | - | - **Do not** open port 2200 to the public — it's the admin shell. | |
| 64 | + | - **Do not** open port 2200 to the public. It's the admin shell. | |
| 65 | 65 | ||
| 66 | 66 | ## Hetzner Console (last resort) | |
| 67 | 67 |
| @@ -57,7 +57,7 @@ | |||
| 57 | 57 | TOTAL=$(find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" | wc -l) | |
| 58 | 58 | echo "[$(date -Iseconds)] Total ${DB_NAME} backups on disk: $TOTAL" | |
| 59 | 59 | ||
| 60 | - | # Sync to offsite host (best-effort — failure here does not fail the backup). | |
| 60 | + | # Sync to offsite host (best-effort: failure here does not fail the backup). | |
| 61 | 61 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | |
| 62 | 62 | OFFSITE_SCRIPT="${SCRIPT_DIR}/sync-backup-offsite.sh" | |
| 63 | 63 | if [ -x "$OFFSITE_SCRIPT" ]; then |
| @@ -22,7 +22,7 @@ | |||
| 22 | 22 | ExecStart=/opt/makenotwork/makenotwork | |
| 23 | 23 | Restart=always | |
| 24 | 24 | RestartSec=5 | |
| 25 | - | # Exit code 2 = migration failure. Don't restart — operator must intervene. | |
| 25 | + | # Exit code 2 = migration failure. Don't restart; the operator must intervene. | |
| 26 | 26 | RestartPreventExitStatus=2 | |
| 27 | 27 | ||
| 28 | 28 | # Environment file with secrets |