# mnw-cli -- Architecture ## Overview 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. ## Module Map ``` src/ main.rs Entry point: config, host key, SSH server, signal handling config.rs Environment variable configuration (6 vars) api.rs HTTP client for MNW internal API (~50 methods, ~890 LOC) commands.rs Non-interactive command handlers (8 commands) format.rs Display formatting (prices, tiers, project types) staging.rs Per-user upload staging (1 GB quota, 24h TTL) ssh/ mod.rs Server factory (russh::server::Server impl) handler.rs Per-connection handler: auth, PTY, channel dispatch (~400 LOC) terminal.rs TerminalHandle: adapts ratatui's Write to SSH channel via mpsc sftp.rs SFTP subsystem for file uploads git.rs Git proxy: parses git commands, spawns subprocesses (~80 LOC) tui/ mod.rs App state, event loop, screen dispatch, data loading (~82 KB) home.rs Project list, revenue/sales/follower stats project.rs Items in a project, publish/unpublish upload.rs Staged files, metadata editor, presign + S3 upload item.rs Item details, versions, edit fields, delete blog.rs Blog posts, create/edit markdown, publish/draft promo.rs Promo codes, create/delete keys.rs License keys, generate/revoke analytics.rs Timeseries revenue, period comparison settings.rs SSH keys, storage usage, profile widgets.rs Shared table rendering widget ``` ## Design Decisions ### SSH-first (not HTTP) The CLI authenticates via SSH public keys, not passwords or API tokens. This means: - Users don't need to manage API keys or copy tokens - Authentication reuses existing SSH key infrastructure (`ssh-keygen`, `~/.ssh/`) - Git operations work natively through the same connection - Non-interactive commands work from any SSH client (`ssh cli.makenot.work projects`) ### Per-connection isolation Each SSH connection spawns an independent `MnwHandler`. No shared mutable state between connections. The handler owns: - Authenticated user identity (from fingerprint lookup) - Terminal channel (for TUI rendering) - SFTP channel (for file uploads) - Per-user staging directory ### TUI as primary interface 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. ### Service-to-service auth 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. ### Staging-based uploads File uploads go through a staging directory rather than streaming directly to S3: 1. SFTP lands files in `/var/lib/mnw-cli/staging/{user_id}/` 2. TUI classifies files by extension, lets creator fill in metadata 3. Server issues presigned S3 URL, CLI uploads with reqwest 4. Background task cleans up staged files after 24 hours This avoids partial uploads to S3 and gives creators a chance to review metadata before publishing. ## Data Flow ### Authentication ``` SSH client -> SSH handshake -> public key offered -> MnwHandler computes SHA-256 fingerprint -> GET /api/internal/ssh-key-lookup?fingerprint=... -> MNW server returns UserInfo (or 404) -> accept/reject connection ``` ### Interactive TUI ``` SSH PTY allocated -> TerminalHandle wraps channel -> ratatui renders to TerminalHandle -> crossterm parses raw input bytes -> AppEvent dispatched (Input/Resize/DataLoaded) -> Screen handlers update state + trigger API calls -> API calls load data async via mpsc -> DataLoaded events ``` ### Non-interactive commands ``` SSH exec request -> parse command string -> commands.rs handler runs -> API calls to MNW server -> format output (table or JSON) -> write to channel -> close ``` ### SFTP upload ``` SSH subsystem "sftp" -> russh-sftp handler -> file written to staging/{user_id}/{filename} -> TUI upload screen reads staging directory -> creator fills metadata -> presign -> upload to S3 -> confirm ``` ### Git proxy ``` SSH exec "git-receive-pack repo.git" -> parse command -> POST /api/internal/git/authorize (verify access, auto-register new repos in DB) -> if repo path doesn't exist on disk: git init --bare --shared=group (direct, no sudo) -> install post-receive hook if BUILD_TRIGGER_TOKEN set -> spawn git subprocess with sudo -u GIT_SUDO_USER -> wire subprocess stdin/stdout to SSH channel ``` 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. ## Key Dependencies | Crate | Role | |-------|------| | russh | SSH server protocol | | russh-sftp | SFTP subsystem | | ratatui | Terminal UI rendering | | crossterm | Terminal input handling | | tokio | Async runtime | | reqwest (rustls-tls) | HTTP client for MNW API | | serde/serde_json | API serialization | | tracing | Structured logging | | anyhow | Error handling | ## Deployment 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. Target: port 22 on hetzner (after migrating sshd to port 2200 on Tailscale only). ## Key Paths | What | Where | |------|-------| | SSH handler + auth | `src/ssh/handler.rs` | | TUI app + event loop | `src/tui/mod.rs` | | API client | `src/api.rs` | | Commands | `src/commands.rs` | | Config | `src/config.rs` | | Deploy | `deploy/deploy.sh` | | systemd unit | `deploy/mnw-cli.service` |