Repository Public Research preview
1import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";2import { relative, resolve, sep } from "node:path";34/** File access for the audit engine. Every read is confined to one root directory. */5export class AuditError extends Error {6 constructor(code, message) {7 super(message);8 this.code = code;9 }10}1112export const MAX_FILE_BYTES = 5 * 1024 * 1024;1314/** Resolve `p` under `root` and refuse anything that lands outside it, symlinks included. */15export function resolveInside(root, p) {16 if (typeof p !== "string" || p.length === 0) throw new AuditError("path_invalid", "A file path is required.");17 let rootReal;18 try {19 rootReal = realpathSync(resolve(root));20 } catch {21 throw new AuditError("root_invalid", `The allowed root does not exist: ${root}`);22 }23 const target = resolve(rootReal, p);24 if (!existsSync(target)) throw new AuditError("not_found", `File not found: ${p}`);25 const real = realpathSync(target);26 if (real !== rootReal && !real.startsWith(rootReal + sep)) {27 throw new AuditError("path_forbidden", `Path is outside the allowed root (${rootReal}). To read it, change the root: MANUSCRIPT_AUDIT_ROOT for the MCP server, --root for the command line.`);28 }29 const st = statSync(real);30 if (!st.isFile()) throw new AuditError("not_a_file", `Not a regular file: ${p}`);31 if (st.size > MAX_FILE_BYTES) throw new AuditError("file_too_large", `${p} is larger than ${MAX_FILE_BYTES} bytes.`);32 return { real, rel: relative(rootReal, real) || p, rootReal };33}3435export function readInside(root, p) {36 const r = resolveInside(root, p);37 const buf = readFileSync(r.real);38 if (buf.subarray(0, 8192).includes(0)) throw new AuditError("binary_file", `${p} looks like a binary file, not a text manuscript.`);39 return { ...r, text: buf.toString("utf8").replace(/^\uFEFF/, "") };40}41