| 1 |
# mnw-cli -- Architecture |
| 2 |
|
| 3 |
## Overview |
| 4 |
|
| 5 |
SSH server that authenticates MNW users via SSH key fingerprint lookup, then dispatches to either an interactive TUI (ratatui) or non-interactive text commands. Also handles SFTP file uploads and git proxy for SSH-based git operations. |
| 6 |
|
| 7 |
## Module Map |
| 8 |
|
| 9 |
``` |
| 10 |
src/ |
| 11 |
main.rs Entry point: config, host key, SSH server, signal handling |
| 12 |
config.rs Environment variable configuration (6 vars) |
| 13 |
api.rs HTTP client for MNW internal API (~50 methods, ~890 LOC) |
| 14 |
commands.rs Non-interactive command handlers (8 commands) |
| 15 |
format.rs Display formatting (prices, tiers, project types) |
| 16 |
staging.rs Per-user upload staging (1 GB quota, 24h TTL) |
| 17 |
|
| 18 |
ssh/ |
| 19 |
mod.rs Server factory (russh::server::Server impl) |
| 20 |
handler.rs Per-connection handler: auth, PTY, channel dispatch (~400 LOC) |
| 21 |
terminal.rs TerminalHandle: adapts ratatui's Write to SSH channel via mpsc |
| 22 |
sftp.rs SFTP subsystem for file uploads |
| 23 |
git.rs Git proxy: parses git commands, spawns subprocesses (~80 LOC) |
| 24 |
|
| 25 |
tui/ |
| 26 |
mod.rs App state, event loop, screen dispatch, data loading (~82 KB) |
| 27 |
home.rs Project list, revenue/sales/follower stats |
| 28 |
project.rs Items in a project, publish/unpublish |
| 29 |
upload.rs Staged files, metadata editor, presign + S3 upload |
| 30 |
item.rs Item details, versions, edit fields, delete |
| 31 |
blog.rs Blog posts, create/edit markdown, publish/draft |
| 32 |
promo.rs Promo codes, create/delete |
| 33 |
keys.rs License keys, generate/revoke |
| 34 |
analytics.rs Timeseries revenue, period comparison |
| 35 |
settings.rs SSH keys, storage usage, profile |
| 36 |
widgets.rs Shared table rendering widget |
| 37 |
``` |
| 38 |
|
| 39 |
## Design Decisions |
| 40 |
|
| 41 |
### SSH-first (not HTTP) |
| 42 |
|
| 43 |
The CLI authenticates via SSH public keys, not passwords or API tokens. This means: |
| 44 |
- Users don't need to manage API keys or copy tokens |
| 45 |
- Authentication reuses existing SSH key infrastructure (`ssh-keygen`, `~/.ssh/`) |
| 46 |
- Git operations work natively through the same connection |
| 47 |
- Non-interactive commands work from any SSH client (`ssh cli.makenot.work projects`) |
| 48 |
|
| 49 |
### Per-connection isolation |
| 50 |
|
| 51 |
Each SSH connection spawns an independent `MnwHandler`. No shared mutable state between connections. The handler owns: |
| 52 |
- Authenticated user identity (from fingerprint lookup) |
| 53 |
- Terminal channel (for TUI rendering) |
| 54 |
- SFTP channel (for file uploads) |
| 55 |
- Per-user staging directory |
| 56 |
|
| 57 |
### TUI as primary interface |
| 58 |
|
| 59 |
The interactive TUI is the default mode (launched when no command is specified). It provides full CRUD for projects, items, uploads, blog posts, promo codes, and license keys. The non-interactive commands are a subset for scripting. |
| 60 |
|
| 61 |
### Service-to-service auth |
| 62 |
|
| 63 |
mnw-cli authenticates to the MNW server via a bearer token (`MNW_SERVICE_TOKEN`). All internal API calls include the authenticated user's ID so the server can enforce authorization. The CLI itself is trusted infrastructure, not a third-party client. |
| 64 |
|
| 65 |
### Staging-based uploads |
| 66 |
|
| 67 |
File uploads go through a staging directory rather than streaming directly to S3: |
| 68 |
1. SFTP lands files in `/var/lib/mnw-cli/staging/{user_id}/` |
| 69 |
2. TUI classifies files by extension, lets creator fill in metadata |
| 70 |
3. Server issues presigned S3 URL, CLI uploads with reqwest |
| 71 |
4. Background task cleans up staged files after 24 hours |
| 72 |
|
| 73 |
This avoids partial uploads to S3 and gives creators a chance to review metadata before publishing. |
| 74 |
|
| 75 |
## Data Flow |
| 76 |
|
| 77 |
### Authentication |
| 78 |
``` |
| 79 |
SSH client -> SSH handshake -> public key offered |
| 80 |
-> MnwHandler computes SHA-256 fingerprint |
| 81 |
-> GET /api/internal/ssh-key-lookup?fingerprint=... |
| 82 |
-> MNW server returns UserInfo (or 404) |
| 83 |
-> accept/reject connection |
| 84 |
``` |
| 85 |
|
| 86 |
### Interactive TUI |
| 87 |
``` |
| 88 |
SSH PTY allocated -> TerminalHandle wraps channel |
| 89 |
-> ratatui renders to TerminalHandle |
| 90 |
-> crossterm parses raw input bytes |
| 91 |
-> AppEvent dispatched (Input/Resize/DataLoaded) |
| 92 |
-> Screen handlers update state + trigger API calls |
| 93 |
-> API calls load data async via mpsc -> DataLoaded events |
| 94 |
``` |
| 95 |
|
| 96 |
### Non-interactive commands |
| 97 |
``` |
| 98 |
SSH exec request -> parse command string |
| 99 |
-> commands.rs handler runs |
| 100 |
-> API calls to MNW server |
| 101 |
-> format output (table or JSON) |
| 102 |
-> write to channel -> close |
| 103 |
``` |
| 104 |
|
| 105 |
### SFTP upload |
| 106 |
``` |
| 107 |
SSH subsystem "sftp" -> russh-sftp handler |
| 108 |
-> file written to staging/{user_id}/{filename} |
| 109 |
-> TUI upload screen reads staging directory |
| 110 |
-> creator fills metadata -> presign -> upload to S3 -> confirm |
| 111 |
``` |
| 112 |
|
| 113 |
### Git proxy |
| 114 |
``` |
| 115 |
SSH exec "git-receive-pack repo.git" -> parse command |
| 116 |
-> POST /api/internal/git/authorize (verify access, auto-register new repos in DB) |
| 117 |
-> if repo path doesn't exist on disk: git init --bare --shared=group (direct, no sudo) |
| 118 |
-> install post-receive hook if BUILD_TRIGGER_TOKEN set |
| 119 |
-> spawn git subprocess with sudo -u GIT_SUDO_USER |
| 120 |
-> wire subprocess stdin/stdout to SSH channel |
| 121 |
``` |
| 122 |
|
| 123 |
Repo auto-create runs as the mnw-cli user (in the git group). Parent dirs have setgid, so new repos inherit git group ownership. `--shared=group` makes repos group-writable so the git user can write via git-receive-pack. The server only handles DB registration — all filesystem operations happen in mnw-cli. |
| 124 |
|
| 125 |
## Key Dependencies |
| 126 |
|
| 127 |
|
| 128 |
|
| 129 |
| russh | SSH server protocol | |
| 130 |
| russh-sftp | SFTP subsystem | |
| 131 |
| ratatui | Terminal UI rendering | |
| 132 |
| crossterm | Terminal input handling | |
| 133 |
| tokio | Async runtime | |
| 134 |
| reqwest (rustls-tls) | HTTP client for MNW API | |
| 135 |
| serde/serde_json | API serialization | |
| 136 |
| tracing | Structured logging | |
| 137 |
| anyhow | Error handling | |
| 138 |
|
| 139 |
## Deployment |
| 140 |
|
| 141 |
Cross-compiled on macOS via `cargo zigbuild`, deployed to hetzner as a systemd service. The service runs as a dedicated `mnw-cli` user with filesystem and privilege restrictions. |
| 142 |
|
| 143 |
Target: port 22 on hetzner (after migrating sshd to port 2200 on Tailscale only). |
| 144 |
|
| 145 |
## Key Paths |
| 146 |
|
| 147 |
|
| 148 |
|
| 149 |
| SSH handler + auth | `src/ssh/handler.rs` | |
| 150 |
| TUI app + event loop | `src/tui/mod.rs` | |
| 151 |
| API client | `src/api.rs` | |
| 152 |
| Commands | `src/commands.rs` | |
| 153 |
| Config | `src/config.rs` | |
| 154 |
| Deploy | `deploy/deploy.sh` | |
| 155 |
| systemd unit | `deploy/mnw-cli.service` | |
| 156 |
|