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:
@@ -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 };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user