UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

247 lines (246 loc) 10.4 kB
import { m as FsSafeError } from "./path-BlG8lhgR.js"; import { t as sameFileIdentity } from "./file-identity-BKNyWMFA.js"; import { c as assertAsyncDirectoryGuard, u as createAsyncDirectoryGuard } from "./write-queue-C9nceBqy.js"; import { f as resolveHomeRelativePath, l as runPinnedWriteHelper } from "./secure-temp-dir-XAWcZnE2.js"; import { t as openPinnedFileSync } from "./pinned-open-BRMz46d0.js"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; //#region node_modules/@openclaw/fs-safe/dist/secret-file.js const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024; const PRIVATE_SECRET_DIR_MODE = 448; const PRIVATE_SECRET_FILE_MODE = 384; function normalizeSecretReadError(error) { return error instanceof Error ? error : new Error(String(error)); } function secretPathErrorCode(error) { const code = error.code; return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path"; } function resolveUserPath(input) { return resolveHomeRelativePath(input); } function readSecretFileOutcomeSync(filePath, label, options = {}) { const resolvedPath = resolveUserPath(filePath.trim()); if (!resolvedPath) return { ok: false, code: "invalid-path", message: `${label} file path is empty.` }; const maxBytes = options.maxBytes ?? 16384; let previewStat; try { previewStat = fs.lstatSync(resolvedPath); } catch (error) { const normalized = normalizeSecretReadError(error); return { ok: false, code: secretPathErrorCode(error), error: normalized, message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}` }; } if (previewStat.isSymbolicLink()) if (!options.rejectSymlink) try { previewStat = fs.statSync(resolvedPath); } catch (error) { const normalized = normalizeSecretReadError(error); return { ok: false, code: secretPathErrorCode(error), error: normalized, message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}` }; } else return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` }; if (!previewStat.isFile()) return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` }; if (options.rejectHardlinks !== false && previewStat.nlink > 1) return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` }; if (previewStat.size > maxBytes) return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` }; const opened = openPinnedFileSync({ filePath: resolvedPath, rejectPathSymlink: options.rejectSymlink, rejectHardlinks: options.rejectHardlinks !== false, maxBytes }); if (!opened.ok) { const error = normalizeSecretReadError(opened.reason === "validation" ? /* @__PURE__ */ new Error("security validation failed") : opened.error); return { ok: false, code: opened.reason === "path" ? "not-found" : "path-mismatch", error, message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}` }; } try { const secret = fs.readFileSync(opened.fd, "utf8").trim(); if (!secret) return { ok: false, code: "invalid-path", message: `${label} file at ${resolvedPath} is empty.` }; return { ok: true, secret }; } catch (error) { const normalized = normalizeSecretReadError(error); return { ok: false, code: "invalid-path", error: normalized, message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}` }; } finally { fs.closeSync(opened.fd); } } function readSecretFileSync(filePath, label, options = {}) { const result = readSecretFileOutcomeSync(filePath, label, options); if (result.ok) return result.secret; throw new FsSafeError(result.code, result.message, { cause: result.error }); } function tryReadSecretFileSync(filePath, label, options = {}) { if (!filePath?.trim()) return; const result = readSecretFileOutcomeSync(filePath, label, options); if (result.ok) return result.secret; if (result.code === "not-found") return; throw new FsSafeError(result.code, result.message, { cause: result.error }); } function isRelativeEscape(relativePath) { return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath); } function assertPathWithinRoot(rootDir, targetPath) { const relative = path.relative(rootDir, targetPath); if (!relative || isRelativeEscape(relative)) throw new Error(`Private secret path must stay under ${rootDir}.`); } function assertRealPathWithinRoot(rootDir, targetPath) { if (isRelativeEscape(path.relative(rootDir, targetPath))) throw new Error(`Private secret path must stay under ${rootDir}.`); } async function enforcePrivatePathMode(resolvedPath, expectedMode, kind) { if (process.platform === "win32") return; await fs$1.chmod(resolvedPath, expectedMode); const actualMode = (await fs$1.stat(resolvedPath)).mode & 511; if (actualMode !== expectedMode) throw new Error(`Private secret ${kind} ${resolvedPath} has insecure permissions ${actualMode.toString(8)}.`); } async function enforcePrivateFileIdentityAndMode(resolvedPath, expectedIdentity, expectedMode) { const noFollowFlag = process.platform !== "win32" && "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0; const handle = await fs$1.open(resolvedPath, fs.constants.O_RDONLY | noFollowFlag); try { const openedStat = await handle.stat(); if (!openedStat.isFile() || !sameFileIdentity(openedStat, expectedIdentity)) throw new FsSafeError("path-mismatch", "private secret file changed during write"); const pathStat = await fs$1.lstat(resolvedPath); if (pathStat.isSymbolicLink() || !sameFileIdentity(pathStat, openedStat)) throw new FsSafeError("path-mismatch", "private secret path changed during write"); if (process.platform !== "win32") { await handle.chmod(expectedMode); const chmodStat = await handle.stat(); const actualMode = chmodStat.mode & 511; if (actualMode !== expectedMode) throw new Error(`Private secret file ${resolvedPath} has insecure permissions ${actualMode.toString(8)}.`); const refreshedPathStat = await fs$1.lstat(resolvedPath); if (refreshedPathStat.isSymbolicLink() || !sameFileIdentity(refreshedPathStat, chmodStat)) throw new FsSafeError("path-mismatch", "private secret path changed during mode check"); } } finally { await handle.close().catch(() => void 0); } } async function ensurePrivateDirectory(rootDir, targetDir, mode) { const resolvedRoot = path.resolve(rootDir); const resolvedTarget = path.resolve(targetDir); await fs$1.mkdir(resolvedRoot, { recursive: true, mode }); const rootStat = await fs$1.lstat(resolvedRoot); if (rootStat.isSymbolicLink()) throw new Error(`Private secret root ${resolvedRoot} must not be a symlink.`); if (!rootStat.isDirectory()) throw new Error(`Private secret root ${resolvedRoot} must be a directory.`); const rootGuard = await createAsyncDirectoryGuard(resolvedRoot); await enforcePrivatePathMode(rootGuard.realPath, mode, "directory"); await assertAsyncDirectoryGuard(rootGuard); if (resolvedTarget === resolvedRoot) return { rootGuard, targetReal: rootGuard.realPath }; assertPathWithinRoot(resolvedRoot, resolvedTarget); const resolvedRootReal = rootGuard.realPath; let current = resolvedRoot; for (const segment of path.relative(resolvedRoot, resolvedTarget).split(path.sep).filter(Boolean)) { current = path.join(current, segment); const parentGuard = await createAsyncDirectoryGuard(path.dirname(current)); await assertAsyncDirectoryGuard(rootGuard); try { const stat = await fs$1.lstat(current); if (stat.isSymbolicLink()) throw new Error(`Private secret directory component ${current} must not be a symlink.`); if (!stat.isDirectory()) throw new Error(`Private secret directory component ${current} must be a directory.`); } catch (error) { if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") throw error; await assertAsyncDirectoryGuard(parentGuard); await fs$1.mkdir(current, { mode }); } const currentReal = await fs$1.realpath(current); assertRealPathWithinRoot(resolvedRootReal, currentReal); await enforcePrivatePathMode(currentReal, mode, "directory"); await assertAsyncDirectoryGuard(parentGuard); await assertAsyncDirectoryGuard(rootGuard); } return { rootGuard, targetReal: await fs$1.realpath(resolvedTarget) }; } async function writeSecretFileAtomic(params) { const mode = params.mode ?? 384; const dirMode = params.dirMode ?? 448; const resolvedRoot = path.resolve(params.rootDir); const resolvedFile = path.resolve(params.filePath); assertPathWithinRoot(resolvedRoot, resolvedFile); const { rootGuard, targetReal } = await ensurePrivateDirectory(resolvedRoot, path.dirname(resolvedFile), dirMode); await assertAsyncDirectoryGuard(rootGuard); assertRealPathWithinRoot(rootGuard.realPath, targetReal); const parentGuard = await createAsyncDirectoryGuard(targetReal); const fileName = path.basename(resolvedFile); const finalFilePath = path.join(targetReal, fileName); try { const stat = await fs$1.lstat(finalFilePath); if (stat.isSymbolicLink()) throw new Error(`Private secret file ${finalFilePath} must not be a symlink.`); if (!stat.isFile()) throw new Error(`Private secret file ${finalFilePath} must be a regular file.`); } catch (error) { if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") throw error; } await assertAsyncDirectoryGuard(rootGuard); await assertAsyncDirectoryGuard(parentGuard); const identity = await runPinnedWriteHelper({ rootPath: parentGuard.realPath, relativeParentPath: "", basename: fileName, mkdir: false, mode, overwrite: true, input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) }, rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino } }); await assertAsyncDirectoryGuard(rootGuard); await assertAsyncDirectoryGuard(parentGuard); await enforcePrivateFileIdentityAndMode(finalFilePath, identity, mode); } //#endregion export { tryReadSecretFileSync as a, readSecretFileSync as i, PRIVATE_SECRET_DIR_MODE as n, writeSecretFileAtomic as o, PRIVATE_SECRET_FILE_MODE as r, DEFAULT_SECRET_FILE_MAX_BYTES as t };