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:
@@ -16,6 +16,10 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import type { ClaudeStatus, ClientMessage, ServerMessage } from '../src/types.js'
|
||||
import { folderFromTitle, cwdFromOsc7 } from './title-util.js'
|
||||
|
||||
// Delay after the shell is ready before typing a session's initial command
|
||||
// (e.g. `claude --resume …`), giving the shell time to finish its prompt.
|
||||
const INITIAL_INPUT_DELAY_MS = 700
|
||||
|
||||
const RESET = '\x1b[0m'
|
||||
const BOLD = '\x1b[1m'
|
||||
const DIM = '\x1b[2m'
|
||||
@@ -84,6 +88,7 @@ export class TerminalSession {
|
||||
private reconnectDelay = 1000 // ms; doubles each attempt, capped at 30 000
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private initialInputTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private exitListener: { dispose(): void } | null = null
|
||||
private isConnecting = false
|
||||
private disposed = false
|
||||
@@ -232,7 +237,8 @@ export class TerminalSession {
|
||||
})
|
||||
|
||||
socket.addEventListener('error', () => {
|
||||
this.isConnecting = false
|
||||
// Do NOT reset isConnecting here — the 'close' event always follows 'error'
|
||||
// and owns the cleanup (resetting it twice could race a reconnect).
|
||||
})
|
||||
}
|
||||
|
||||
@@ -247,7 +253,11 @@ export class TerminalSession {
|
||||
if (this.initialInput !== undefined && !this.initialSent) {
|
||||
this.initialSent = true
|
||||
const cmd = this.initialInput
|
||||
setTimeout(() => this.send(cmd), 700)
|
||||
this.initialInputTimer = setTimeout(() => {
|
||||
this.initialInputTimer = null
|
||||
if (this.disposed) return
|
||||
this.send(cmd)
|
||||
}, INITIAL_INPUT_DELAY_MS)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -364,12 +374,9 @@ export class TerminalSession {
|
||||
|
||||
hide(): void {
|
||||
this.el.style.display = 'none'
|
||||
// v0.4: withdraw our size vote so a backgrounded mirror doesn't clamp the
|
||||
// shared PTY to this device's size. Output still streams (background mirror).
|
||||
if (this.ws !== null && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(buildMessage({ type: 'blur' }))
|
||||
}
|
||||
// Force show() to re-cast our dims even if unchanged.
|
||||
// v0.4 (latest-writer-wins): hiding does NOT resize the shared PTY — the
|
||||
// device still actively viewing keeps its size. Output still streams
|
||||
// (background mirror). Force show()/refit() to re-cast our dims on return.
|
||||
this.lastCols = -1
|
||||
this.lastRows = -1
|
||||
}
|
||||
@@ -385,6 +392,10 @@ export class TerminalSession {
|
||||
clearTimeout(this.resizeTimer)
|
||||
this.resizeTimer = null
|
||||
}
|
||||
if (this.initialInputTimer !== null) {
|
||||
clearTimeout(this.initialInputTimer)
|
||||
this.initialInputTimer = null
|
||||
}
|
||||
this.resizeObserver.disconnect()
|
||||
if (this.ws !== null) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user