Skip to main content

max / mnw-cli

5.6 KB · 153 lines History Blame Raw
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-upload-pack repo.git" -> parse command
116 -> lookup repo via API (verify user access)
117 -> spawn git subprocess with sudo as GIT_SUDO_USER
118 -> wire subprocess stdin/stdout to SSH channel
119 ```
120
121 ## Key Dependencies
122
123 | Crate | Role |
124 |-------|------|
125 | russh | SSH server protocol |
126 | russh-sftp | SFTP subsystem |
127 | ratatui | Terminal UI rendering |
128 | crossterm | Terminal input handling |
129 | tokio | Async runtime |
130 | reqwest (rustls-tls) | HTTP client for MNW API |
131 | serde/serde_json | API serialization |
132 | tracing | Structured logging |
133 | anyhow | Error handling |
134
135 ## Deployment
136
137 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.
138
139 Target: port 22 on hetzner (after migrating sshd to port 2200 on Tailscale only).
140
141 ## Key Paths
142
143 | What | Where |
144 |------|-------|
145 | SSH handler + auth | `src/ssh/handler.rs` |
146 | TUI app + event loop | `src/tui/mod.rs` |
147 | API client | `src/api.rs` |
148 | Commands | `src/commands.rs` |
149 | Config | `src/config.rs` |
150 | Deploy | `deploy/deploy.sh` |
151 | systemd unit | `deploy/mnw-cli.service` |
152 | Todo | `docs/todo.md` |
153