Skip to main content

max / makenotwork

7.2 KB · 176 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 currency.rs Settlement currencies: codes, symbols, multi-currency totals
16 format.rs Display formatting (prices, tiers, project types)
17 staging.rs Per-user upload staging (1 GB quota, 24h TTL)
18
19 ssh/
20 mod.rs Server factory (russh::server::Server impl)
21 handler.rs Per-connection handler: auth, PTY, channel dispatch (~400 LOC)
22 terminal.rs TerminalHandle: adapts ratatui's Write to SSH channel via mpsc
23 sftp.rs SFTP subsystem for file uploads
24 git.rs Git proxy: parses git commands, spawns subprocesses (~80 LOC)
25
26 tui/
27 mod.rs App state, event loop, screen dispatch, data loading (~82 KB)
28 home.rs Project list, revenue/sales/follower stats
29 project.rs Items in a project, publish/unpublish
30 upload.rs Staged files, metadata editor, presign + S3 upload
31 item.rs Item details, versions, edit fields, delete
32 blog.rs Blog posts, create/edit markdown, publish/draft
33 promo.rs Promo codes, create/delete
34 keys.rs License keys, generate/revoke
35 analytics.rs Timeseries revenue, period comparison
36 settings.rs SSH keys, storage usage, profile
37 widgets.rs Shared table rendering widget
38 ```
39
40 ## Design Decisions
41
42 ### SSH-first (not HTTP)
43
44 The CLI authenticates via SSH public keys, not passwords or API tokens. This means:
45 - Users don't need to manage API keys or copy tokens
46 - Authentication reuses existing SSH key infrastructure (`ssh-keygen`, `~/.ssh/`)
47 - Git operations work natively through the same connection
48 - Non-interactive commands work from any SSH client (`ssh cli.makenot.work projects`)
49
50 ### Money is never converted, and never rendered without a currency
51
52 Six settlement currencies (USD, CAD, GBP, AUD, NZD, EUR), all two-decimal, so
53 every amount stays an integer number of cents. Two rules hold everywhere:
54
55 - Every amount renders with the currency it is denominated in. The viewer's own
56 settlement currency arrives on the fingerprint lookup at login and covers
57 their prices and their period totals. Per-project revenue carries its own
58 currency and can differ, because a revenue split is paid in the currency of
59 the project that earned it.
60 - Amounts in different currencies are listed, never added. There is no exchange
61 rate anywhere in MNW, so a single figure spanning currencies would be
62 invented. A project that spans two shows both (`£900.00 + $120.00`) in a
63 detail line, and the leading amount plus a `+N` marker in a table cell.
64
65 `currency.rs` is the display half of `server/src/currency.rs`. The symbols must
66 stay identical to the server's or a creator sees two different marks for the
67 same money on the web dashboard and in the TUI.
68
69 ### Per-connection isolation
70
71 Each SSH connection spawns an independent `MnwHandler`. No shared mutable state between connections. The handler owns:
72 - Authenticated user identity (from fingerprint lookup)
73 - Terminal channel (for TUI rendering)
74 - SFTP channel (for file uploads)
75 - Per-user staging directory
76
77 ### TUI as primary interface
78
79 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.
80
81 ### Service-to-service auth
82
83 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.
84
85 ### Staging-based uploads
86
87 File uploads go through a staging directory rather than streaming directly to S3:
88 1. SFTP lands files in `/var/lib/mnw-cli/staging/{user_id}/`
89 2. TUI classifies files by extension, lets creator fill in metadata
90 3. Server issues presigned S3 URL, CLI uploads with reqwest
91 4. Background task cleans up staged files after 24 hours
92
93 This avoids partial uploads to S3 and gives creators a chance to review metadata before publishing.
94
95 ## Data Flow
96
97 ### Authentication
98 ```
99 SSH client -> SSH handshake -> public key offered
100 -> MnwHandler computes SHA-256 fingerprint
101 -> GET /api/internal/ssh-key-lookup?fingerprint=...
102 -> MNW server returns UserInfo (or 404)
103 -> accept/reject connection
104 ```
105
106 ### Interactive TUI
107 ```
108 SSH PTY allocated -> TerminalHandle wraps channel
109 -> ratatui renders to TerminalHandle
110 -> crossterm parses raw input bytes
111 -> AppEvent dispatched (Input/Resize/DataLoaded)
112 -> Screen handlers update state + trigger API calls
113 -> API calls load data async via mpsc -> DataLoaded events
114 ```
115
116 ### Non-interactive commands
117 ```
118 SSH exec request -> parse command string
119 -> commands.rs handler runs
120 -> API calls to MNW server
121 -> format output (table or JSON)
122 -> write to channel -> close
123 ```
124
125 ### SFTP upload
126 ```
127 SSH subsystem "sftp" -> russh-sftp handler
128 -> file written to staging/{user_id}/{filename}
129 -> TUI upload screen reads staging directory
130 -> creator fills metadata -> presign -> upload to S3 -> confirm
131 ```
132
133 ### Git proxy
134 ```
135 SSH exec "git-receive-pack repo.git" -> parse command
136 -> POST /api/internal/git/authorize (verify access, auto-register new repos in DB)
137 -> if repo path doesn't exist on disk: git init --bare --shared=group (direct, no sudo)
138 -> install post-receive hook if BUILD_TRIGGER_TOKEN set
139 -> spawn git subprocess with sudo -u GIT_SUDO_USER
140 -> wire subprocess stdin/stdout to SSH channel
141 ```
142
143 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.
144
145 ## Key Dependencies
146
147 | Crate | Role |
148 |-------|------|
149 | russh | SSH server protocol |
150 | russh-sftp | SFTP subsystem |
151 | ratatui | Terminal UI rendering |
152 | crossterm | Terminal input handling |
153 | tokio | Async runtime |
154 | reqwest (rustls-tls) | HTTP client for MNW API |
155 | serde/serde_json | API serialization |
156 | tracing | Structured logging |
157 | anyhow | Error handling |
158
159 ## Deployment
160
161 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.
162
163 Target: port 22 on hetzner (after migrating sshd to port 2200 on Tailscale only).
164
165 ## Key Paths
166
167 | What | Where |
168 |------|-------|
169 | SSH handler + auth | `src/ssh/handler.rs` |
170 | TUI app + event loop | `src/tui/mod.rs` |
171 | API client | `src/api.rs` |
172 | Commands | `src/commands.rs` |
173 | Config | `src/config.rs` |
174 | Deploy | Sando companion, in lockstep with the server promote (`../../sando/deploy/README.md`) |
175 | systemd unit | `deploy/mnw-cli.service` |
176