fix: address review report across security, architecture, quality, tests

Implements the fixes from docs/REVIEW_REPORT.md (4-agent parallel review).
typecheck clean; 341 tests pass (16 files, +113); build:web ok; coverage
thresholds (80%) enforced in vitest.config.ts.

Critical:
- multi-device approval race: release held approval only when the last
  client detaches (closing one mirror no longer cancels another's prompt)
- unbounded session creation (DoS): Config.maxSessions cap (env MAX_SESSIONS),
  enforced in manager via the existing M4 exit(-1) path
- signal-handler leak: named SIGINT/SIGTERM/uncaughtException refs removed in close()
- terminal-session initialInput timer tracked + cleared on dispose
- tabs.addEntry null-as-cast type hole removed (build session before entry)

Should-fix:
- security-headers middleware + Origin/CSRF guard on DELETE /live-sessions[/:id]
- history.ts converted to fs/promises (async /sessions handler)
- removed dead clientDims map + blur protocol message end-to-end
- per-connection WS message rate limit (Config.maxMsgsPerSec)
- /sessions behavior kept; documented as accepted LAN risk (TECH_DOC §7)

Tests:
- new tmux / preview-grid / terminal-session (jsdom) / tabs (jsdom) suites
- extended history/config/manager/integration coverage incl. regressions

Hygiene:
- parsePositiveInt -> parseNonNegativeInt; ALLOWED_ORIGINS scheme validation
- log-injection sanitize; isLoopback handles 127.0.0.0/8 + IPv4-mapped
- operational constants moved into Config
- extracted public/preview-grid.ts (DRY launcher/manage)
- doc sweeps: ARCHITECTURE §8 runtime-handle exception, stale comments
This commit is contained in:
Yaojia Wang
2026-06-20 18:27:45 +02:00
parent 97d57326fd
commit d22dcd24f7
30 changed files with 2900 additions and 400 deletions

View File

@@ -79,7 +79,7 @@ Data flow: `keypress → xterm onData → WS → pty.write() → shell stdin`, a
- `attach(null)` → spawn a new PTY + shell; PTY output goes into a ~2MB **ring buffer** and (if a WS is attached) forwards live.
- `attach(sessionId)` → look up the session, **replay the ring buffer**, then resume the live stream. This is what makes "refresh the page and the Claude session is still there" work.
- WS close → `detach` one client (PTY keeps running), not kill; the idle clock starts only when the **last** client leaves.
- **Multi-device sharing (v0.4):** a session may have **many concurrent WS clients** — a new attach **JOINS (mirror)**, it does **not** kick. Output/exit/status broadcast to all; any client can type (shared control). **PTY sizing = latest-writer-wins:** the device that most recently fit/focused drives the size, so whichever device you're actively using is full-screen (a shared PTY can only be one size; min-sizing letterboxed the bigger screen). Attach/detach/`blur` never resize — the active device keeps its size; the frontend re-sends dims on pane-show and window-focus so switching devices reclaims full-screen. This relaxes the original "one WS per session" invariant (#5). `GET /live-sessions` lists running sessions so any device **auto-shows them as tabs**; `?join=<id>` and the 🔗 share-QR open a specific shared session; the **🗂 manage page** (`/manage.html`) is a full-page **grid of live preview thumbnails** — each card renders the session's current screen via a read-only xterm (fed by `GET /live-sessions/:id/preview``RingBuffer.tail()`, no attach) so you can see what each session is doing, with open/kill (`DELETE /live-sessions[/:id]`).
- **Multi-device sharing (v0.4):** a session may have **many concurrent WS clients** — a new attach **JOINS (mirror)**, it does **not** kick. Output/exit/status broadcast to all; any client can type (shared control). **PTY sizing = latest-writer-wins:** the device that most recently fit/focused drives the size, so whichever device you're actively using is full-screen (a shared PTY can only be one size; min-sizing letterboxed the bigger screen). Attach/detach never resize — the active device keeps its size; the frontend re-sends dims on pane-show and window-focus so switching devices reclaims full-screen. (The earlier `blur` size-vote message was removed in the min→latest-writer pivot.) This relaxes the original "one WS per session" invariant (#5). `GET /live-sessions` lists running sessions so the **v0.5 home launcher (session chooser)** shows them as live preview thumbnails to pick from; `?join=<id>` and the 🔗 share-QR open a specific shared session; the **🗂 manage page** (`/manage.html`) is a full-page **grid of live preview thumbnails** — each card renders the session's current screen via a read-only xterm (fed by `GET /live-sessions/:id/preview``RingBuffer.tail()`, no attach) so you can see what each session is doing, with open/kill (`DELETE /live-sessions[/:id]`).
- Orphan reclaim: detached longer than `IDLE_TTL` (default 24h) **with no new output since detach** → reclaim. (node-pty can't reliably detect a foreground child, so liveness is approximated via last-output time — see ARCHITECTURE §3.5 / M3.) On server exit, `pty.kill()` all sessions (no cross-restart persistence in v0.1; tmux backend is a v0.2 idea).
Represent session state as immutable snapshots (id/start-time fixed at creation); hold mutable runtime handles (pty/ws) separately.