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:
@@ -35,6 +35,16 @@ vi.mock('node:os', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ── mock tmux availability so resolveUseTmux's auto-detect is deterministic ──
|
||||
const mockTmuxAvailable = vi.fn<[], boolean>()
|
||||
mockTmuxAvailable.mockReturnValue(false)
|
||||
vi.mock('../src/session/tmux.js', () => ({
|
||||
tmuxAvailable: () => mockTmuxAvailable(),
|
||||
tmuxName: (id: string) => `web_${id}`,
|
||||
hasSession: () => false,
|
||||
killSession: () => undefined,
|
||||
}))
|
||||
|
||||
// Import config AFTER mock is set up (dynamic import ensures mock applies)
|
||||
const { loadConfig } = await import('../src/config.js')
|
||||
|
||||
@@ -270,6 +280,93 @@ describe('loadConfig — allowedOrigins (M1)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ── MAX_SESSIONS + new operational constants ──────────────────────────────────
|
||||
describe('loadConfig — session/rate/timer constants', () => {
|
||||
beforeEach(() => {
|
||||
mockNetworkInterfaces.mockReturnValue({})
|
||||
mockHomedir.mockReturnValue('/home/testuser')
|
||||
})
|
||||
|
||||
it('defaults maxSessions to 50', () => {
|
||||
expect(loadConfig({}).maxSessions).toBe(50)
|
||||
})
|
||||
|
||||
it('reads MAX_SESSIONS from env', () => {
|
||||
expect(loadConfig({ MAX_SESSIONS: '20' }).maxSessions).toBe(20)
|
||||
})
|
||||
|
||||
it('throws for a non-integer MAX_SESSIONS', () => {
|
||||
expect(() => loadConfig({ MAX_SESSIONS: 'lots' })).toThrow()
|
||||
})
|
||||
|
||||
it('defaults maxMsgsPerSec to 2000 and reads MAX_MSGS_PER_SEC', () => {
|
||||
expect(loadConfig({}).maxMsgsPerSec).toBe(2000)
|
||||
expect(loadConfig({ MAX_MSGS_PER_SEC: '500' }).maxMsgsPerSec).toBe(500)
|
||||
})
|
||||
|
||||
it('defaults permTimeoutMs / reapIntervalMs / previewBytes and reads overrides', () => {
|
||||
const def = loadConfig({})
|
||||
expect(def.permTimeoutMs).toBe(5 * 60_000)
|
||||
expect(def.reapIntervalMs).toBe(60_000)
|
||||
expect(def.previewBytes).toBe(24 * 1024)
|
||||
const over = loadConfig({ PERM_TIMEOUT_MS: '1000', REAP_INTERVAL_MS: '2000', PREVIEW_BYTES: '4096' })
|
||||
expect(over.permTimeoutMs).toBe(1000)
|
||||
expect(over.reapIntervalMs).toBe(2000)
|
||||
expect(over.previewBytes).toBe(4096)
|
||||
})
|
||||
})
|
||||
|
||||
// ── resolveUseTmux branches (USE_TMUX env) ────────────────────────────────────
|
||||
describe('loadConfig — resolveUseTmux', () => {
|
||||
beforeEach(() => {
|
||||
mockNetworkInterfaces.mockReturnValue({})
|
||||
mockHomedir.mockReturnValue('/home/testuser')
|
||||
mockTmuxAvailable.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it.each(['1', 'true', 'on', 'ON', ' True '])('forces tmux ON for %j', (v) => {
|
||||
expect(loadConfig({ USE_TMUX: v }).useTmux).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['0', 'false', 'off'])('forces tmux OFF for %j', (v) => {
|
||||
expect(loadConfig({ USE_TMUX: v }).useTmux).toBe(false)
|
||||
})
|
||||
|
||||
it('auto-detects (unset) → true when tmux is available', () => {
|
||||
mockTmuxAvailable.mockReturnValue(true)
|
||||
expect(loadConfig({}).useTmux).toBe(true)
|
||||
})
|
||||
|
||||
it('auto-detects (unset / "auto") → false when tmux is unavailable', () => {
|
||||
mockTmuxAvailable.mockReturnValue(false)
|
||||
expect(loadConfig({}).useTmux).toBe(false)
|
||||
expect(loadConfig({ USE_TMUX: 'auto' }).useTmux).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ── ALLOWED_ORIGINS scheme validation (M1) ────────────────────────────────────
|
||||
describe('loadConfig — ALLOWED_ORIGINS scheme validation', () => {
|
||||
beforeEach(() => {
|
||||
mockNetworkInterfaces.mockReturnValue({})
|
||||
mockHomedir.mockReturnValue('/home/testuser')
|
||||
})
|
||||
|
||||
it('rejects non-http(s) scheme entries (file:, javascript:, bare host)', () => {
|
||||
const cfg = loadConfig({
|
||||
ALLOWED_ORIGINS: 'file:///etc/passwd,javascript:alert(1),notaurl,http://ok.local:3000',
|
||||
})
|
||||
expect(cfg.allowedOrigins).toContain('http://ok.local:3000')
|
||||
expect(cfg.allowedOrigins.some((o) => o.startsWith('file:'))).toBe(false)
|
||||
expect(cfg.allowedOrigins.some((o) => o.startsWith('javascript:'))).toBe(false)
|
||||
expect(cfg.allowedOrigins).not.toContain('notaurl')
|
||||
})
|
||||
|
||||
it('keeps valid https entries', () => {
|
||||
const cfg = loadConfig({ ALLOWED_ORIGINS: 'https://phone.local:3000' })
|
||||
expect(cfg.allowedOrigins).toContain('https://phone.local:3000')
|
||||
})
|
||||
})
|
||||
|
||||
// ── Immutability ──────────────────────────────────────────────────────────────
|
||||
describe('loadConfig — returned object is frozen', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user