diff --git a/README.md b/README.md new file mode 100644 index 0000000..58beb41 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Web Terminal + +A browser-based terminal that exposes the **host machine's local shell** over WebSocket. Open `http://:3000` from any device on your LAN (phone, tablet, another computer) and you get a live, interactive terminal — primarily for **vibe coding**: hand Claude Code a task, walk away, reconnect from anywhere to check on it. + +Sessions survive disconnects: the shell (and whatever's running in it) keeps going when you close the tab; reconnect and the output replays. + +> ⚠️ **This hands a full shell to anyone who can reach the port.** LAN-only, no authentication. **Never** port-forward or tunnel it to the public internet. See [Security](#security). + +## Requirements + +- Node.js ≥ 18 (developed on v24) +- macOS/Linux. On macOS, `node-pty` compiles a native addon — install **Xcode Command Line Tools** (`xcode-select --install`) if `npm install` fails. + +## Install + +```bash +npm install # installs deps; postinstall makes node-pty's spawn-helper executable +``` + +## Run + +```bash +npm run build:web # bundle the frontend (public/main.ts → public/build/main.js) +npm start # serve on 0.0.0.0:3000 +``` + +Then: + +```bash +# find your LAN IP (macOS): +ipconfig getifaddr en0 +# open http://:3000 on any device on the same network +``` + +For frontend development, run `npm run dev:web` (esbuild --watch) alongside `npm start`. + +## Configuration + +All via environment variables (no hardcoding): + +| Var | Default | Meaning | +|-----|---------|---------| +| `PORT` | `3000` | listen port | +| `BIND_HOST` | `0.0.0.0` | listen address | +| `SHELL_PATH` | `$SHELL` or `/bin/zsh` | shell to spawn | +| `IDLE_TTL` | `86400` (s) | reclaim a detached session after this idle time | +| `SCROLLBACK_BYTES` | `2097152` | per-session replay ring buffer (bytes) | +| `MAX_PAYLOAD_BYTES` | `1048576` | max single WS frame | +| `ALLOWED_ORIGINS` | (derived) | extra allowed WS origins, comma-separated | + +`allowedOrigins` is **derived from the host's network-interface IPs** (plus `localhost` and anything in `ALLOWED_ORIGINS`) — never from `BIND_HOST`, since `0.0.0.0` is never a real browser Origin. + +## Security + +This is a no-auth, LAN-only tool by design. The defenses that matter: + +- **Origin check (cannot be skipped):** the WS handshake rejects any `Origin` not on the allow-list (HTTP 401). This blocks Cross-Site WebSocket Hijacking — a malicious page in your browser trying to connect to `ws://:3000`. +- **Path-scoped upgrades:** only `/term` is accepted for WS upgrade. +- **Frame size cap:** oversized frames are rejected (`MAX_PAYLOAD_BYTES`). +- **Never expose to the public internet.** `ws://` is **unencrypted** — on an untrusted network (café/office Wi-Fi) traffic (including keystrokes: passwords, API keys) can be sniffed. The recommended deployment is **[Tailscale](https://tailscale.com/)** (WireGuard-encrypted), which also lets you use `wss://` (the frontend auto-selects `wss` on HTTPS). + +## Development + +```bash +npm test # vitest, all modules +npm run typecheck # tsc (backend + frontend) +npm run build # compile backend to dist/ +``` + +Real-PTY end-to-end tests (`test/integration/`) auto-skip where `posix_spawn` is unavailable (e.g. sandboxes) and run everywhere else. + +## How it works + +The server is a **byte-shuttle, not a terminal**: `node-pty` provides a pseudo-terminal so the shell believes it has a real TTY, and `xterm.js` in the browser interprets the ANSI bytes and renders. The server never parses terminal semantics. PTY lifecycle is decoupled from the WebSocket — a disconnect *detaches* (the PTY keeps running) rather than killing it, which is what makes session survival work. + +Design and rationale: [`docs/TECH_DOC.md`](docs/TECH_DOC.md) (why) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (how).