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

@@ -6,7 +6,7 @@
* can `claude --resume <id>` in the right place). Read-only; best-effort.
*/
import fs from 'node:fs';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
@@ -61,27 +61,27 @@ export function parseSessionMeta(jsonlText: string): { cwd: string | null; previ
return { cwd, preview: preview.replace(/\s+/g, ' ').trim().slice(0, 120) };
}
function readHead(file: string, max: number): string {
/** Read up to `max` bytes from the head of a file (async, best-effort). */
async function readHead(file: string, max: number): Promise<string> {
let fh: fs.FileHandle | undefined;
try {
const fd = fs.openSync(file, 'r');
try {
const buf = Buffer.alloc(max);
const n = fs.readSync(fd, buf, 0, max, 0);
return buf.subarray(0, n).toString('utf8');
} finally {
fs.closeSync(fd);
}
fh = await fs.open(file, 'r');
const buf = Buffer.alloc(max);
const { bytesRead } = await fh.read(buf, 0, max, 0);
return buf.subarray(0, bytesRead).toString('utf8');
} catch {
return '';
} finally {
await fh?.close().catch(() => undefined);
}
}
/** Most-recently-modified Claude Code sessions across all projects. */
export function listSessions(limit = 50): HistorySession[] {
/** Most-recently-modified Claude Code sessions across all projects (async). */
export async function listSessions(limit = 50): Promise<HistorySession[]> {
const root = path.join(os.homedir(), '.claude', 'projects');
let dirs: string[];
try {
dirs = fs.readdirSync(root);
dirs = await fs.readdir(root);
} catch {
return [];
}
@@ -91,14 +91,14 @@ export function listSessions(limit = 50): HistorySession[] {
const dirPath = path.join(root, dir);
let names: string[];
try {
names = fs.readdirSync(dirPath);
names = await fs.readdir(dirPath);
} catch {
continue;
}
for (const name of names) {
if (!name.endsWith('.jsonl')) continue;
try {
const st = fs.statSync(path.join(dirPath, name));
const st = await fs.stat(path.join(dirPath, name));
files.push({
id: name.slice(0, -'.jsonl'.length),
file: path.join(dirPath, name),
@@ -112,10 +112,13 @@ export function listSessions(limit = 50): HistorySession[] {
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
return files.slice(0, limit).map((f) => {
const meta = parseSessionMeta(readHead(f.file, 256 * 1024));
const cwd = meta.cwd ?? '';
const project = cwd !== '' ? (cwd.split('/').filter(Boolean).pop() ?? cwd) : 'unknown';
return { id: f.id, cwd, project, mtimeMs: f.mtimeMs, preview: meta.preview };
});
const top = files.slice(0, limit);
return Promise.all(
top.map(async (f) => {
const meta = parseSessionMeta(await readHead(f.file, 256 * 1024));
const cwd = meta.cwd ?? '';
const project = cwd !== '' ? (cwd.split('/').filter(Boolean).pop() ?? cwd) : 'unknown';
return { id: f.id, cwd, project, mtimeMs: f.mtimeMs, preview: meta.preview };
}),
);
}