"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.a = openWritableFileWithinRoot;exports.c = writeFileFromPathWithinRoot;exports.i = openFileWithinRoot;exports.l = writeFileWithinRoot;exports.n = copyFileWithinRoot;exports.o = readFileWithinRoot;exports.r = createRootScopedReadFile;exports.s = readLocalFileSafely;exports.t = void 0;var _runWithConcurrency2ga3CMk = require("./run-with-concurrency-2ga3-CMk.js"); var _pathsEFexkPEh = require("./paths-eFexkPEh.js"); var _loggerU3s76KST = require("./logger-U3s76KST.js"); var _pathAliasGuardsDFv45kR = require("./path-alias-guards-DFv45kR8.js"); var _nodeFs = require("node:fs"); var _nodePath = _interopRequireDefault(require("node:path")); var _nodeOs = _interopRequireDefault(require("node:os")); var _promises = _interopRequireDefault(require("node:fs/promises")); var _nodeCrypto = require("node:crypto"); var _promises2 = require("node:stream/promises");function _interopRequireDefault(e) {return e && e.__esModule ? e : { default: e };} //#region src/infra/fs-safe.ts var SafeOpenError = class extends Error { constructor(code, message, options) { super(message, options); this.code = code; this.name = "SafeOpenError"; } };exports.t = SafeOpenError; const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in _nodeFs.constants; const OPEN_READ_FLAGS = _nodeFs.constants.O_RDONLY | (SUPPORTS_NOFOLLOW ? _nodeFs.constants.O_NOFOLLOW : 0); const OPEN_WRITE_EXISTING_FLAGS = _nodeFs.constants.O_WRONLY | (SUPPORTS_NOFOLLOW ? _nodeFs.constants.O_NOFOLLOW : 0); const OPEN_WRITE_CREATE_FLAGS = _nodeFs.constants.O_WRONLY | _nodeFs.constants.O_CREAT | _nodeFs.constants.O_EXCL | (SUPPORTS_NOFOLLOW ? _nodeFs.constants.O_NOFOLLOW : 0); const ensureTrailingSep = (value) => value.endsWith(_nodePath.default.sep) ? value : value + _nodePath.default.sep; async function expandRelativePathWithHome(relativePath) { let home = process.env.HOME || process.env.USERPROFILE || _nodeOs.default.homedir(); try { home = await _promises.default.realpath(home); } catch {} return (0, _pathsEFexkPEh.l)(relativePath, { home }); } async function openVerifiedLocalFile(filePath, options) { try { if ((await _promises.default.lstat(filePath)).isDirectory()) throw new SafeOpenError("not-file", "not a file"); } catch (err) { if (err instanceof SafeOpenError) throw err; } let handle; try { handle = await _promises.default.open(filePath, OPEN_READ_FLAGS); } catch (err) { if ((0, _runWithConcurrency2ga3CMk.R)(err)) throw new SafeOpenError("not-found", "file not found"); if ((0, _runWithConcurrency2ga3CMk.B)(err)) throw new SafeOpenError("symlink", "symlink open blocked", { cause: err }); if ((0, _runWithConcurrency2ga3CMk.L)(err, "EISDIR")) throw new SafeOpenError("not-file", "not a file"); throw err; } try { const [stat, lstat] = await Promise.all([handle.stat(), _promises.default.lstat(filePath)]); if (lstat.isSymbolicLink()) throw new SafeOpenError("symlink", "symlink not allowed"); if (!stat.isFile()) throw new SafeOpenError("not-file", "not a file"); if (options?.rejectHardlinks && stat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed"); if (!(0, _runWithConcurrency2ga3CMk.N)(stat, lstat)) throw new SafeOpenError("path-mismatch", "path changed during read"); const realPath = await _promises.default.realpath(filePath); const realStat = await _promises.default.stat(realPath); if (options?.rejectHardlinks && realStat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed"); if (!(0, _runWithConcurrency2ga3CMk.N)(stat, realStat)) throw new SafeOpenError("path-mismatch", "path mismatch"); return { handle, realPath, stat }; } catch (err) { await handle.close().catch(() => {}); if (err instanceof SafeOpenError) throw err; if ((0, _runWithConcurrency2ga3CMk.R)(err)) throw new SafeOpenError("not-found", "file not found"); throw err; } } async function resolvePathWithinRoot(params) { let rootReal; try { rootReal = await _promises.default.realpath(params.rootDir); } catch (err) { if ((0, _runWithConcurrency2ga3CMk.R)(err)) throw new SafeOpenError("not-found", "root dir not found"); throw err; } const rootWithSep = ensureTrailingSep(rootReal); const expanded = await expandRelativePathWithHome(params.relativePath); const resolved = _nodePath.default.resolve(rootWithSep, expanded); if (!(0, _runWithConcurrency2ga3CMk.z)(rootWithSep, resolved)) throw new SafeOpenError("outside-workspace", "file is outside workspace root"); return { rootReal, rootWithSep, resolved }; } async function openFileWithinRoot(params) { const { rootWithSep, resolved } = await resolvePathWithinRoot(params); let opened; try { opened = await openVerifiedLocalFile(resolved); } catch (err) { if (err instanceof SafeOpenError) { if (err.code === "not-found") throw err; throw new SafeOpenError("invalid-path", "path is not a regular file under root", { cause: err }); } throw err; } if (params.rejectHardlinks !== false && opened.stat.nlink > 1) { await opened.handle.close().catch(() => {}); throw new SafeOpenError("invalid-path", "hardlinked path not allowed"); } if (!(0, _runWithConcurrency2ga3CMk.z)(rootWithSep, opened.realPath)) { await opened.handle.close().catch(() => {}); throw new SafeOpenError("outside-workspace", "file is outside workspace root"); } return opened; } async function readFileWithinRoot(params) { const opened = await openFileWithinRoot({ rootDir: params.rootDir, relativePath: params.relativePath, rejectHardlinks: params.rejectHardlinks }); try { return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes }); } finally { await opened.handle.close().catch(() => {}); } } async function readPathWithinRoot(params) { const rootDir = _nodePath.default.resolve(params.rootDir); const candidatePath = _nodePath.default.isAbsolute(params.filePath) ? _nodePath.default.resolve(params.filePath) : _nodePath.default.resolve(rootDir, params.filePath); return await readFileWithinRoot({ rootDir, relativePath: _nodePath.default.relative(rootDir, candidatePath), rejectHardlinks: params.rejectHardlinks, maxBytes: params.maxBytes }); } function createRootScopedReadFile(params) { const rootDir = _nodePath.default.resolve(params.rootDir); return async (filePath) => { return (await readPathWithinRoot({ rootDir, filePath, rejectHardlinks: params.rejectHardlinks, maxBytes: params.maxBytes })).buffer; }; } async function readLocalFileSafely(params) { const opened = await openVerifiedLocalFile(params.filePath); try { return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes }); } finally { await opened.handle.close().catch(() => {}); } } async function readOpenedFileSafely(params) { if (params.maxBytes !== void 0 && params.opened.stat.size > params.maxBytes) throw new SafeOpenError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`); return { buffer: await params.opened.handle.readFile(), realPath: params.opened.realPath, stat: params.opened.stat }; } function emitWriteBoundaryWarning(reason) { (0, _loggerU3s76KST.i)(`security: fs-safe write boundary warning (${reason})`); } function buildAtomicWriteTempPath(targetPath) { const dir = _nodePath.default.dirname(targetPath); const base = _nodePath.default.basename(targetPath); return _nodePath.default.join(dir, `.${base}.${process.pid}.${(0, _nodeCrypto.randomUUID)()}.tmp`); } async function writeTempFileForAtomicReplace(params) { const tempHandle = await _promises.default.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode); try { if (typeof params.data === "string") await tempHandle.writeFile(params.data, params.encoding ?? "utf8");else await tempHandle.writeFile(params.data); return await tempHandle.stat(); } finally { await tempHandle.close().catch(() => {}); } } async function verifyAtomicWriteResult(params) { const rootWithSep = ensureTrailingSep(await _promises.default.realpath(params.rootDir)); const opened = await openVerifiedLocalFile(params.targetPath, { rejectHardlinks: true }); try { if (!(0, _runWithConcurrency2ga3CMk.N)(opened.stat, params.expectedStat)) throw new SafeOpenError("path-mismatch", "path changed during write"); if (!(0, _runWithConcurrency2ga3CMk.z)(rootWithSep, opened.realPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root"); } finally { await opened.handle.close().catch(() => {}); } } async function resolveOpenedFileRealPathForHandle(handle, ioPath) { try { return await _promises.default.realpath(ioPath); } catch (err) { if (!(0, _runWithConcurrency2ga3CMk.R)(err)) throw err; } const fdCandidates = process.platform === "linux" ? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`] : process.platform === "win32" ? [] : [`/dev/fd/${handle.fd}`]; for (const fdPath of fdCandidates) try { return await _promises.default.realpath(fdPath); } catch {} throw new SafeOpenError("path-mismatch", "unable to resolve opened file path"); } async function openWritableFileWithinRoot(params) { const { rootReal, rootWithSep, resolved } = await resolvePathWithinRoot(params); try { await (0, _pathAliasGuardsDFv45kR.n)({ absolutePath: resolved, rootPath: rootReal, boundaryLabel: "root" }); } catch (err) { throw new SafeOpenError("invalid-path", "path alias escape blocked", { cause: err }); } if (params.mkdir !== false) await _promises.default.mkdir(_nodePath.default.dirname(resolved), { recursive: true }); let ioPath = resolved; try { const resolvedRealPath = await _promises.default.realpath(resolved); if (!(0, _runWithConcurrency2ga3CMk.z)(rootWithSep, resolvedRealPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root"); ioPath = resolvedRealPath; } catch (err) { if (err instanceof SafeOpenError) throw err; if (!(0, _runWithConcurrency2ga3CMk.R)(err)) throw err; } const fileMode = params.mode ?? 384; let handle; let createdForWrite = false; try { try { handle = await _promises.default.open(ioPath, OPEN_WRITE_EXISTING_FLAGS, fileMode); } catch (err) { if (!(0, _runWithConcurrency2ga3CMk.R)(err)) throw err; handle = await _promises.default.open(ioPath, OPEN_WRITE_CREATE_FLAGS, fileMode); createdForWrite = true; } } catch (err) { if ((0, _runWithConcurrency2ga3CMk.R)(err)) throw new SafeOpenError("not-found", "file not found"); if ((0, _runWithConcurrency2ga3CMk.B)(err)) throw new SafeOpenError("invalid-path", "symlink open blocked", { cause: err }); throw err; } let openedRealPath = null; try { const stat = await handle.stat(); if (!stat.isFile()) throw new SafeOpenError("invalid-path", "path is not a regular file under root"); if (stat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed"); try { const lstat = await _promises.default.lstat(ioPath); if (lstat.isSymbolicLink() || !lstat.isFile()) throw new SafeOpenError("invalid-path", "path is not a regular file under root"); if (!(0, _runWithConcurrency2ga3CMk.N)(stat, lstat)) throw new SafeOpenError("path-mismatch", "path changed during write"); } catch (err) { if (!(0, _runWithConcurrency2ga3CMk.R)(err)) throw err; } const realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath); openedRealPath = realPath; const realStat = await _promises.default.stat(realPath); if (!(0, _runWithConcurrency2ga3CMk.N)(stat, realStat)) throw new SafeOpenError("path-mismatch", "path mismatch"); if (realStat.nlink > 1) throw new SafeOpenError("invalid-path", "hardlinked path not allowed"); if (!(0, _runWithConcurrency2ga3CMk.z)(rootWithSep, realPath)) throw new SafeOpenError("outside-workspace", "file is outside workspace root"); if (params.truncateExisting !== false && !createdForWrite) await handle.truncate(0); return { handle, createdForWrite, openedRealPath: realPath, openedStat: stat }; } catch (err) { const cleanupCreatedPath = createdForWrite && err instanceof SafeOpenError; const cleanupPath = openedRealPath ?? ioPath; await handle.close().catch(() => {}); if (cleanupCreatedPath) await _promises.default.rm(cleanupPath, { force: true }).catch(() => {}); throw err; } } async function writeFileWithinRoot(params) { const target = await openWritableFileWithinRoot({ rootDir: params.rootDir, relativePath: params.relativePath, mkdir: params.mkdir, truncateExisting: false }); const destinationPath = target.openedRealPath; const targetMode = target.openedStat.mode & 511; await target.handle.close().catch(() => {}); let tempPath = null; try { tempPath = buildAtomicWriteTempPath(destinationPath); const writtenStat = await writeTempFileForAtomicReplace({ tempPath, data: params.data, encoding: params.encoding, mode: targetMode || 384 }); await _promises.default.rename(tempPath, destinationPath); tempPath = null; try { await verifyAtomicWriteResult({ rootDir: params.rootDir, targetPath: destinationPath, expectedStat: writtenStat }); } catch (err) { emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`); throw err; } } finally { if (tempPath) await _promises.default.rm(tempPath, { force: true }).catch(() => {}); } } async function copyFileWithinRoot(params) { const source = await openVerifiedLocalFile(params.sourcePath, { rejectHardlinks: params.rejectSourceHardlinks }); if (params.maxBytes !== void 0 && source.stat.size > params.maxBytes) { await source.handle.close().catch(() => {}); throw new SafeOpenError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`); } let target = null; let sourceClosedByStream = false; let targetClosedByUs = false; let tempHandle = null; let tempPath = null; let tempClosedByStream = false; try { target = await openWritableFileWithinRoot({ rootDir: params.rootDir, relativePath: params.relativePath, mkdir: params.mkdir, truncateExisting: false }); const destinationPath = target.openedRealPath; const targetMode = target.openedStat.mode & 511; await target.handle.close().catch(() => {}); targetClosedByUs = true; tempPath = buildAtomicWriteTempPath(destinationPath); tempHandle = await _promises.default.open(tempPath, OPEN_WRITE_CREATE_FLAGS, targetMode || 384); const sourceStream = source.handle.createReadStream(); const targetStream = tempHandle.createWriteStream(); sourceStream.once("close", () => { sourceClosedByStream = true; }); targetStream.once("close", () => { tempClosedByStream = true; }); await (0, _promises2.pipeline)(sourceStream, targetStream); const writtenStat = await _promises.default.stat(tempPath); if (!tempClosedByStream) { await tempHandle.close().catch(() => {}); tempClosedByStream = true; } tempHandle = null; await _promises.default.rename(tempPath, destinationPath); tempPath = null; try { await verifyAtomicWriteResult({ rootDir: params.rootDir, targetPath: destinationPath, expectedStat: writtenStat }); } catch (err) { emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`); throw err; } } catch (err) { if (target?.createdForWrite) await _promises.default.rm(target.openedRealPath, { force: true }).catch(() => {}); throw err; } finally { if (tempPath) await _promises.default.rm(tempPath, { force: true }).catch(() => {}); if (!sourceClosedByStream) await source.handle.close().catch(() => {}); if (tempHandle && !tempClosedByStream) await tempHandle.close().catch(() => {}); if (target && !targetClosedByUs) await target.handle.close().catch(() => {}); } } async function writeFileFromPathWithinRoot(params) { await copyFileWithinRoot({ sourcePath: params.sourcePath, rootDir: params.rootDir, relativePath: params.relativePath, mkdir: params.mkdir, rejectSourceHardlinks: true }); } //#endregion /* v9-769d128410a49563 */