openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
955 lines (954 loc) • 38.8 kB
JavaScript
import { i as isPathInside, m as FsSafeError, r as isNotFoundPathError, u as resolveSafeBaseDir } from "./path-BlG8lhgR.js";
import { t as sameFileIdentity } from "./file-identity-BKNyWMFA.js";
import { t as normalizeLowercaseStringOrEmpty } from "./string-coerce-6TL5VVOL.js";
import { c as assertAsyncDirectoryGuard, n as registerTempPathForExit, u as createAsyncDirectoryGuard } from "./write-queue-C9nceBqy.js";
import { t as writeSiblingTempFile } from "./sibling-temp-CLpkwDtX.js";
import { a as root, o as getFsSafeTestHooks, t as resolveSecureTempRoot, u as resolveOpenedFileRealPathForHandle } from "./secure-temp-dir-XAWcZnE2.js";
import { n as sanitizeSafePathSegment } from "./safe-path-segment-BEYqpzSe.js";
import fs, { constants } from "node:fs";
import path from "node:path";
import fs$1, { mkdtemp, rm } from "node:fs/promises";
import "node:crypto";
import { pipeline } from "node:stream/promises";
import { Readable, Transform } from "node:stream";
//#region node_modules/@openclaw/fs-safe/dist/archive-entry.js
function isWindowsDrivePath(value) {
return /^[a-zA-Z]:[\\/]/.test(value);
}
function normalizeArchiveEntryPath(raw) {
return raw.replaceAll("\\", "/");
}
function validateArchiveEntryPath(entryPath, params) {
if (!entryPath || entryPath === "." || entryPath === "./") return;
if (isWindowsDrivePath(entryPath)) throw new Error(`archive entry uses a drive path: ${entryPath}`);
const normalized = path.posix.normalize(normalizeArchiveEntryPath(entryPath));
const escapeLabel = params?.escapeLabel ?? "destination";
if (normalized === ".." || normalized.startsWith("../")) throw new Error(`archive entry escapes ${escapeLabel}: ${entryPath}`);
if (path.posix.isAbsolute(normalized) || normalized.startsWith("//")) throw new Error(`archive entry is absolute: ${entryPath}`);
}
function stripArchivePath(entryPath, stripComponents) {
const raw = normalizeArchiveEntryPath(entryPath);
if (!raw || raw === "." || raw === "./") return null;
const parts = raw.split("/").filter((part) => part.length > 0 && part !== ".");
const strip = Math.max(0, Math.floor(stripComponents));
const stripped = strip === 0 ? parts.join("/") : parts.slice(strip).join("/");
const result = path.posix.normalize(stripped);
if (!result || result === "." || result === "./") return null;
return result;
}
function resolveArchiveOutputPath(params) {
const safeBase = resolveSafeBaseDir(params.rootDir);
const outPath = path.resolve(params.rootDir, params.relPath);
const escapeLabel = params.escapeLabel ?? "destination";
if (!outPath.startsWith(safeBase)) throw new Error(`archive entry escapes ${escapeLabel}: ${params.originalPath}`);
return outPath;
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-deadline.js
function signalReason(signal, fallback) {
const reason = signal.reason;
return reason instanceof Error ? reason : fallback ?? new Error(String(reason));
}
function deadlineReason(deadline) {
return signalReason(deadline.signal);
}
function createPipelineTimeoutError(err, deadline) {
if (deadline.signal.aborted && err instanceof Error && (err.name === "AbortError" || err.message === "The operation was aborted")) return deadlineReason(deadline);
return err;
}
async function waitForDeadline(promise, deadline) {
deadline.check();
if (deadline.signal.aborted) throw deadlineReason(deadline);
return await Promise.race([promise, new Promise((_, reject) => {
const abort = () => reject(deadlineReason(deadline));
deadline.signal.addEventListener("abort", abort, { once: true });
const cleanup = () => {
deadline.signal.removeEventListener("abort", abort);
};
promise.then(cleanup, cleanup);
})]);
}
function createExtractionDeadline(timeoutMs, label) {
const controller = new AbortController();
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return {
signal: controller.signal,
check: () => void 0,
dispose: () => void 0
};
const timeoutError = /* @__PURE__ */ new Error(`${label} timed out after ${timeoutMs}ms`);
const timeoutId = setTimeout(() => {
controller.abort(timeoutError);
}, timeoutMs);
return {
signal: controller.signal,
check: () => {
if (controller.signal.aborted) throw signalReason(controller.signal, timeoutError);
},
dispose: () => {
clearTimeout(timeoutId);
}
};
}
async function withExtractionDeadline(timeoutMs, label, run) {
const deadline = createExtractionDeadline(timeoutMs, label);
try {
deadline.check();
return await waitForDeadline(run(deadline), deadline);
} finally {
deadline.dispose();
}
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-file-io.js
async function writeFileHandleFully(params) {
let offset = 0;
while (offset < params.bytes) {
params.deadline.check();
const { bytesWritten } = await params.handle.write(params.buffer, offset, params.bytes - offset);
if (bytesWritten <= 0) throw new Error("archive staging write made no progress");
offset += bytesWritten;
}
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-limits.js
const DEFAULT_MAX_ARCHIVE_BYTES_ZIP = 256 * 1024 * 1024;
const DEFAULT_MAX_ENTRIES = 5e4;
const DEFAULT_MAX_EXTRACTED_BYTES = 512 * 1024 * 1024;
const DEFAULT_MAX_ENTRY_BYTES = 256 * 1024 * 1024;
const ARCHIVE_LIMIT_ERROR_CODE = {
ARCHIVE_SIZE_EXCEEDS_LIMIT: "archive-size-exceeds-limit",
ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit",
ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit",
EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit"
};
const ARCHIVE_LIMIT_ERROR_MESSAGE = {
[ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT]: "archive size exceeds limit",
[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT]: "archive entry count exceeds limit",
[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive entry extracted size exceeds limit",
[ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive extracted size exceeds limit"
};
var ArchiveLimitError = class extends Error {
code;
constructor(code) {
super(ARCHIVE_LIMIT_ERROR_MESSAGE[code]);
this.name = "ArchiveLimitError";
this.code = code;
}
};
function clampLimit(value) {
if (typeof value !== "number" || !Number.isFinite(value)) return;
const v = Math.floor(value);
return v > 0 ? v : void 0;
}
function resolveExtractLimits(limits) {
return {
maxArchiveBytes: clampLimit(limits?.maxArchiveBytes) ?? 268435456,
maxEntries: clampLimit(limits?.maxEntries) ?? 5e4,
maxExtractedBytes: clampLimit(limits?.maxExtractedBytes) ?? 536870912,
maxEntryBytes: clampLimit(limits?.maxEntryBytes) ?? 268435456
};
}
function assertArchiveEntryCountWithinLimit(entryCount, limits) {
if (entryCount > limits.maxEntries) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT);
}
function createByteBudgetTracker(limits) {
let entryBytes = 0;
let extractedBytes = 0;
const addBytes = (bytes) => {
const b = Math.max(0, Math.floor(bytes));
if (b === 0) return;
entryBytes += b;
if (entryBytes > limits.maxEntryBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
extractedBytes += b;
if (extractedBytes > limits.maxExtractedBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT);
};
return {
startEntry() {
entryBytes = 0;
},
addBytes,
addEntrySize(size) {
const s = Math.max(0, Math.floor(size));
if (s > limits.maxEntryBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
addBytes(s);
}
};
}
function createExtractBudgetTransform(params) {
return new Transform({ transform(chunk, _encoding, callback) {
try {
const buf = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
params.onChunkBytes(buf.byteLength);
callback(null, buf);
} catch (err) {
callback(err instanceof Error ? err : new Error(String(err)));
}
} });
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-kind.js
const TAR_SUFFIXES = [
".tgz",
".tar.gz",
".tar"
];
function resolveArchiveKind(filePath) {
const lower = normalizeLowercaseStringOrEmpty(filePath);
if (lower.endsWith(".zip")) return "zip";
if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return "tar";
return null;
}
async function hasPackedRootMarker(extractDir, rootMarkers) {
for (const marker of rootMarkers) {
const trimmed = marker.trim();
if (!trimmed) continue;
try {
await fs$1.stat(path.join(extractDir, trimmed));
return true;
} catch {}
}
return false;
}
async function resolvePackedRootDir(extractDir, options) {
const direct = path.join(extractDir, "package");
try {
if ((await fs$1.stat(direct)).isDirectory()) return direct;
} catch {}
if ((options?.rootMarkers?.length ?? 0) > 0) {
if (await hasPackedRootMarker(extractDir, options?.rootMarkers ?? [])) return extractDir;
}
const dirs = (await fs$1.readdir(extractDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
if (dirs.length !== 1) throw new Error(`unexpected archive layout (dirs: ${dirs.join(", ")})`);
const onlyDir = dirs[0];
if (!onlyDir) throw new Error("unexpected archive layout (no package dir found)");
return path.join(extractDir, onlyDir);
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-staging.js
const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
const ARCHIVE_STAGING_MODE = 448;
var ArchiveSecurityError = class extends Error {
code;
constructor(code, message, options) {
super(message, options);
this.code = code;
this.name = "ArchiveSecurityError";
}
};
function symlinkTraversalError(originalPath) {
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${originalPath}`);
}
async function createDirectoryIdentityGuard(dir) {
try {
return await createAsyncDirectoryGuard(dir);
} catch (err) {
if (err instanceof FsSafeError && err.code === "not-file") throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
throw err;
}
}
async function assertDirectoryIdentityGuard(guard) {
try {
await assertAsyncDirectoryGuard(guard);
} catch (err) {
if (err instanceof FsSafeError) throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
throw err;
}
}
async function prepareArchiveDestinationDir(destDir) {
const stat = await fs$1.lstat(destDir);
if (stat.isSymbolicLink()) throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
if (!stat.isDirectory()) throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
const realPath = await fs$1.realpath(destDir);
const realStat = await fs$1.stat(realPath);
const postStat = await fs$1.lstat(destDir);
if (realStat.dev !== stat.dev || realStat.ino !== stat.ino || postStat.isSymbolicLink() || !postStat.isDirectory() || postStat.dev !== stat.dev || postStat.ino !== stat.ino) throw new ArchiveSecurityError("destination-symlink-traversal", "archive destination changed during extraction");
return realPath;
}
async function assertNoSymlinkTraversal(params) {
const parts = params.relPath.split(/[\\/]+/).filter(Boolean);
let current = path.resolve(params.rootDir);
for (const part of parts) {
current = path.join(current, part);
let stat;
try {
stat = await fs$1.lstat(current);
} catch (err) {
if (isNotFoundPathError(err)) continue;
throw err;
}
if (stat.isSymbolicLink()) throw symlinkTraversalError(params.originalPath);
}
}
async function assertResolvedInsideDestination(params) {
let resolved;
try {
resolved = await fs$1.realpath(params.targetPath);
} catch (err) {
if (isNotFoundPathError(err)) return;
throw err;
}
if (!isPathInside(params.destinationRealDir, resolved)) throw symlinkTraversalError(params.originalPath);
}
async function prepareArchiveOutputPath(params) {
const targetRoot = await root(params.destinationRealDir);
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
const relPath = params.relPath.split(path.sep).join(path.posix.sep);
await assertNoSymlinkTraversal({
rootDir: params.destinationDir,
relPath,
originalPath: params.originalPath
});
if (params.isDirectory) {
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
await assertDirectoryIdentityGuard(destinationGuard);
await targetRoot.mkdir(relPath);
await assertDirectoryIdentityGuard(destinationGuard);
await assertResolvedInsideDestination({
destinationRealDir: params.destinationRealDir,
targetPath: params.outPath,
originalPath: params.originalPath
});
return;
}
const parentRel = path.posix.dirname(relPath);
if (parentRel !== ".") {
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
await assertDirectoryIdentityGuard(destinationGuard);
await targetRoot.mkdir(parentRel);
await assertDirectoryIdentityGuard(destinationGuard);
}
await assertResolvedInsideDestination({
destinationRealDir: params.destinationRealDir,
targetPath: path.dirname(params.outPath),
originalPath: params.originalPath
});
}
async function chmodInsideDestinationBestEffort(params) {
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", params.destinationPath);
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
await assertDirectoryIdentityGuard(destinationGuard);
const noFollowFlag = process.platform !== "win32" && "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0;
const handle = await fs$1.open(params.destinationPath, fs.constants.O_RDONLY | noFollowFlag).catch(() => null);
if (!handle) {
if ((await fs$1.lstat(params.destinationPath).catch(() => null))?.isSymbolicLink()) throw symlinkTraversalError(params.originalPath);
return;
}
try {
const stat = await handle.stat();
if (!stat.isDirectory() && !stat.isFile()) return;
const realPath = await resolveOpenedFileRealPathForHandle(handle, params.destinationPath);
if (!isPathInside(params.destinationRealDir, realPath)) throw symlinkTraversalError(params.originalPath);
await handle.chmod(params.mode).catch(() => void 0);
await assertDirectoryIdentityGuard(destinationGuard);
} finally {
await handle.close().catch(() => void 0);
}
}
async function applyStagedEntryMode(params) {
const destinationPath = path.join(params.destinationRealDir, params.relPath);
await assertResolvedInsideDestination({
destinationRealDir: params.destinationRealDir,
targetPath: destinationPath,
originalPath: params.originalPath
});
if (params.mode !== 0) await chmodInsideDestinationBestEffort({
destinationRealDir: params.destinationRealDir,
destinationPath,
mode: params.mode,
originalPath: params.originalPath
});
}
async function assertExtractedFileHasNoHardlinkAlias(params) {
const destinationPath = path.join(params.destinationRealDir, params.relPath);
await assertResolvedInsideDestination({
destinationRealDir: params.destinationRealDir,
targetPath: destinationPath,
originalPath: params.originalPath
});
const stat = await fs$1.lstat(destinationPath);
if (stat.isFile() && stat.nlink > 1) throw symlinkTraversalError(params.originalPath);
}
async function removeExtractedDestinationFile(params) {
const destinationPath = path.join(params.destinationRealDir, params.relPath);
let stat;
try {
stat = await fs$1.lstat(destinationPath);
} catch {
return;
}
if (!stat.isFile()) return;
let resolved;
try {
resolved = await fs$1.realpath(destinationPath);
} catch {
return;
}
if (!isPathInside(params.destinationRealDir, resolved)) return;
await (await root(params.destinationRealDir)).remove(params.relPath).catch(() => void 0);
}
function assertSafeArchiveStagingPrefix(prefix) {
if (!prefix || prefix === "." || prefix === ".." || prefix.includes("/") || prefix.includes("\\") || path.basename(prefix) !== prefix) throw new Error("archive staging prefix must be a single path segment");
return prefix;
}
async function withStagedArchiveDestination(params) {
const stagingRoot = resolveSecureTempRoot({
fallbackPrefix: "fs-safe-archive",
unsafeFallbackLabel: "archive staging temp dir",
warn: () => void 0
});
if (isPathInside(params.destinationRealDir, stagingRoot)) throw new Error(`archive staging root must be outside destination: ${stagingRoot}`);
const stagingPrefix = assertSafeArchiveStagingPrefix(params.stagingDirPrefix ?? "fs-safe-archive-");
const stagingDir = await fs$1.mkdtemp(path.join(stagingRoot, stagingPrefix));
const stagingGuard = await createDirectoryIdentityGuard(stagingDir);
try {
await fs$1.chmod(stagingDir, ARCHIVE_STAGING_MODE).catch(() => void 0);
await assertDirectoryIdentityGuard(stagingGuard);
return await params.run(stagingDir);
} finally {
try {
await assertDirectoryIdentityGuard(stagingGuard);
await fs$1.rm(stagingDir, {
recursive: true,
force: true
}).catch(() => void 0);
} catch {}
}
}
async function mergeExtractedTreeIntoDestination(params) {
const targetRoot = await root(params.destinationRealDir);
const sourceRootGuard = await createDirectoryIdentityGuard(params.sourceDir);
const sourceRootReal = sourceRootGuard.realPath;
const walk = async (currentSourceDir) => {
await assertDirectoryIdentityGuard(sourceRootGuard);
const entries = await fs$1.readdir(currentSourceDir, { withFileTypes: true });
for (const entry of entries) {
await assertDirectoryIdentityGuard(sourceRootGuard);
const sourcePath = path.join(currentSourceDir, entry.name);
const relPath = path.relative(params.sourceDir, sourcePath);
const originalPath = relPath.split(path.sep).join("/");
const destinationPath = path.join(params.destinationDir, relPath);
const sourceStat = await fs$1.lstat(sourcePath);
if (sourceStat.isSymbolicLink()) throw symlinkTraversalError(originalPath);
if (!isPathInside(sourceRootReal, await fs$1.realpath(sourcePath))) throw symlinkTraversalError(originalPath);
if (sourceStat.isDirectory()) {
await prepareArchiveOutputPath({
destinationDir: params.destinationDir,
destinationRealDir: params.destinationRealDir,
relPath,
outPath: destinationPath,
originalPath,
isDirectory: true
});
await walk(sourcePath);
await applyStagedEntryMode({
destinationRealDir: params.destinationRealDir,
relPath,
mode: sourceStat.mode & 511,
originalPath
});
continue;
}
if (!sourceStat.isFile()) throw new Error(`archive staging contains unsupported entry: ${originalPath}`);
await prepareArchiveOutputPath({
destinationDir: params.destinationDir,
destinationRealDir: params.destinationRealDir,
relPath,
outPath: destinationPath,
originalPath,
isDirectory: false
});
try {
await targetRoot.copyIn(relPath, sourcePath, { mkdir: true });
await assertExtractedFileHasNoHardlinkAlias({
destinationRealDir: params.destinationRealDir,
relPath,
originalPath
});
await applyStagedEntryMode({
destinationRealDir: params.destinationRealDir,
relPath,
mode: sourceStat.mode & 511,
originalPath
});
} catch (err) {
await removeExtractedDestinationFile({
destinationRealDir: params.destinationRealDir,
relPath
});
if (err instanceof FsSafeError && (err.code === "hardlink" || err.code === "path-alias")) throw symlinkTraversalError(originalPath);
throw err;
}
}
};
await walk(params.sourceDir);
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-tar.js
const BLOCKED_TAR_ENTRY_TYPES = new Set([
"SymbolicLink",
"Link",
"BlockDevice",
"CharacterDevice",
"FIFO",
"Socket"
]);
function readTarEntryInfo(entry) {
return {
path: typeof entry === "object" && entry !== null && "path" in entry ? String(entry.path) : "",
type: typeof entry === "object" && entry !== null && "type" in entry ? String(entry.type) : "",
size: typeof entry === "object" && entry !== null && "size" in entry && typeof entry.size === "number" && Number.isFinite(entry.size) ? Math.max(0, Math.floor(entry.size)) : 0
};
}
function createTarEntryPreflightChecker(params) {
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
const limits = resolveExtractLimits(params.limits);
let entryCount = 0;
const budget = createByteBudgetTracker(limits);
return (entry) => {
validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
const relPath = stripArchivePath(entry.path, strip);
if (!relPath) return;
validateArchiveEntryPath(relPath, { escapeLabel: params.escapeLabel });
resolveArchiveOutputPath({
rootDir: params.rootDir,
relPath,
originalPath: entry.path,
escapeLabel: params.escapeLabel
});
if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) throw new Error(`tar entry is a link: ${entry.path}`);
entryCount += 1;
assertArchiveEntryCountWithinLimit(entryCount, limits);
budget.addEntrySize(entry.size);
};
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js
const ZIP_EOCD_SIGNATURE = 101010256;
const ZIP64_EOCD_SIGNATURE = 101075792;
const ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
const ZIP_EOCD_MIN_BYTES = 22;
const ZIP_EOCD_MAX_COMMENT_BYTES = 65535;
const ZIP64_ENTRY_COUNT_SENTINEL = 65535;
const ZIP64_UINT32_SENTINEL = 4294967295;
const ZIP_CENTRAL_FILE_HEADER_SIGNATURE = 33639248;
const ZIP_CENTRAL_FILE_HEADER_MIN_BYTES = 46;
const ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET = 28;
const ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET = 30;
const ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET = 32;
const ZIP_EOCD_TOTAL_ENTRIES_OFFSET = 10;
const ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 12;
const ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 16;
const ZIP_EOCD_COMMENT_LENGTH_OFFSET = 20;
const ZIP64_EOCD_LOCATOR_BYTES = 20;
const ZIP64_EOCD_OFFSET_OFFSET = 8;
const ZIP64_EOCD_TOTAL_ENTRIES_OFFSET = 32;
const ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 40;
const ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 48;
function asBufferView(buffer) {
if (Buffer.isBuffer(buffer)) return buffer;
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
function readSafeUInt64LE(buffer, offset) {
const value = buffer.readBigUInt64LE(offset);
if (value > BigInt(Number.MAX_SAFE_INTEGER)) return Number.MAX_SAFE_INTEGER;
return Number(value);
}
function findZipEndOfCentralDirectory(buffer) {
if (buffer.byteLength < ZIP_EOCD_MIN_BYTES) return -1;
const minOffset = Math.max(0, buffer.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_EOCD_MAX_COMMENT_BYTES);
for (let offset = buffer.byteLength - ZIP_EOCD_MIN_BYTES; offset >= minOffset; offset -= 1) {
if (buffer.readUInt32LE(offset) !== ZIP_EOCD_SIGNATURE) continue;
const commentLength = buffer.readUInt16LE(offset + ZIP_EOCD_COMMENT_LENGTH_OFFSET);
if (offset + ZIP_EOCD_MIN_BYTES + commentLength === buffer.byteLength) return offset;
}
return -1;
}
function readZip64CentralDirectoryInfo(buffer, eocdOffset) {
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_BYTES;
if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) return null;
const zip64EocdOffset = readSafeUInt64LE(buffer, locatorOffset + ZIP64_EOCD_OFFSET_OFFSET);
if (zip64EocdOffset < 0 || zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET + 8 > buffer.byteLength || buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE) return null;
return {
declaredEntryCount: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_TOTAL_ENTRIES_OFFSET),
centralDirectorySize: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET),
centralDirectoryOffset: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET),
endOfCentralDirectoryOffset: eocdOffset
};
}
function readZipCentralDirectoryInfo(buffer) {
const eocdOffset = findZipEndOfCentralDirectory(buffer);
if (eocdOffset < 0) return null;
const declaredEntryCount = buffer.readUInt16LE(eocdOffset + ZIP_EOCD_TOTAL_ENTRIES_OFFSET);
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET);
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET);
if (declaredEntryCount === ZIP64_ENTRY_COUNT_SENTINEL || centralDirectorySize === ZIP64_UINT32_SENTINEL || centralDirectoryOffset === ZIP64_UINT32_SENTINEL) return readZip64CentralDirectoryInfo(buffer, eocdOffset) ?? {
declaredEntryCount,
centralDirectoryOffset,
centralDirectorySize,
endOfCentralDirectoryOffset: eocdOffset
};
return {
declaredEntryCount,
centralDirectoryOffset,
centralDirectorySize,
endOfCentralDirectoryOffset: eocdOffset
};
}
function countZipCentralDirectoryHeaders(buffer, info) {
const start = info.centralDirectoryOffset;
const declaredEnd = start + info.centralDirectorySize;
const scanEnd = info.endOfCentralDirectoryOffset;
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(declaredEnd) || !Number.isSafeInteger(scanEnd) || start < 0 || declaredEnd < start || scanEnd < start || scanEnd > buffer.byteLength) return null;
let offset = start;
let count = 0;
while (offset < scanEnd) {
if (scanEnd - offset < ZIP_CENTRAL_FILE_HEADER_MIN_BYTES) break;
if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_FILE_HEADER_SIGNATURE) break;
const nameLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET);
const extraLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET);
const commentLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET);
const nextOffset = offset + ZIP_CENTRAL_FILE_HEADER_MIN_BYTES + nameLength + extraLength + commentLength;
if (nextOffset <= offset || nextOffset > scanEnd) return null;
count += 1;
offset = nextOffset;
}
return count > 0 || info.declaredEntryCount === 0 ? count : null;
}
function readZipCentralDirectoryEntryCount(buffer) {
const view = asBufferView(buffer);
const info = readZipCentralDirectoryInfo(view);
if (!info) return null;
const countedEntryCount = countZipCentralDirectoryHeaders(view, info);
return countedEntryCount === null ? info.declaredEntryCount : Math.max(info.declaredEntryCount, countedEntryCount);
}
async function loadZipArchiveWithPreflight(buffer, limits) {
const resolvedLimits = resolveExtractLimits(limits);
if (buffer.byteLength > resolvedLimits.maxArchiveBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
const entryCount = readZipCentralDirectoryEntryCount(buffer);
if (entryCount !== null) assertArchiveEntryCountWithinLimit(entryCount, resolvedLimits);
return await (await importOptionalJsZip()).loadAsync(buffer);
}
async function importOptionalJsZip() {
try {
const module = await import("jszip");
const candidate = typeof module === "function" ? module : module.default;
if (typeof candidate !== "object" && typeof candidate !== "function" || candidate === null || typeof candidate.loadAsync !== "function") throw new Error("Optional archive dependency \"jszip\" does not expose loadAsync().");
return candidate;
} catch (err) {
throw missingOptionalArchiveDependencyError$1("jszip", err);
}
}
function missingOptionalArchiveDependencyError$1(packageName, cause) {
return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use ZIP archive helpers from @openclaw/fs-safe/archive.`, { cause });
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/temp-target.js
function sanitizePrefix(prefix) {
return prefix.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "tmp";
}
function sanitizeTempFileName(fileName) {
return sanitizeSafePathSegment(path.basename(fileName), "download.bin", { allowDotPrefix: true });
}
function isNodeErrorWithCode(err, code) {
return typeof err === "object" && err !== null && "code" in err && err.code === code;
}
async function cleanupTempDir(dir, onCleanupError) {
try {
await rm(dir, {
recursive: true,
force: true
});
} catch (err) {
if (!isNodeErrorWithCode(err, "ENOENT")) onCleanupError?.(err);
}
}
function resolveTempRoot(rootDir) {
return path.resolve(rootDir ?? resolveSecureTempRoot({ fallbackPrefix: "fs-safe" }));
}
async function tempFile(params) {
const rootDir = resolveTempRoot(params.rootDir);
const prefix = `${sanitizePrefix(params.prefix)}-`;
const dir = await mkdtemp(path.join(rootDir, prefix));
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
const file = (fileName) => path.join(dir, sanitizeTempFileName(fileName ?? params.fileName ?? "download.bin"));
const cleanup = async () => {
try {
await cleanupTempDir(dir, params.onCleanupError);
} finally {
unregisterTempDir();
}
};
return {
dir,
path: file(),
file,
cleanup,
[Symbol.asyncDispose]: cleanup
};
}
//#endregion
//#region node_modules/@openclaw/fs-safe/dist/archive.js
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in constants;
const OPEN_WRITE_CREATE_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (SUPPORTS_NOFOLLOW ? constants.O_NOFOLLOW : 0);
const ZIP_UNIX_FILE_TYPE_MASK = 61440;
const ZIP_UNIX_SYMLINK_TYPE = 40960;
function isZipSymlinkEntry(entry) {
return typeof entry.unixPermissions === "number" && (entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE;
}
function zipEntryFileMode(entry) {
if (typeof entry.unixPermissions !== "number") return;
const mode = entry.unixPermissions & 511;
return mode === 0 ? void 0 : mode;
}
async function cleanupStagedArchiveFile(staged) {
if (staged) await staged.cleanup().catch(() => void 0);
}
async function closeFileHandle(handle) {
if (handle) await handle.close().catch(() => void 0);
}
async function stageArchiveFileForExtraction(params) {
params.deadline.check();
const sourcePath = path.resolve(params.archivePath);
const initialStat = await fs$1.lstat(sourcePath);
if (initialStat.isSymbolicLink() || !initialStat.isFile()) throw new Error(`archive is not a regular file: ${params.archivePath}`);
if (initialStat.size > params.limits.maxArchiveBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
const handle = await fs$1.open(sourcePath, constants.O_RDONLY | noFollow);
let staged;
let output;
try {
staged = await tempFile({
prefix: "fs-safe-archive-input",
fileName: path.basename(sourcePath)
});
const openedStat = await handle.stat();
const pathStat = await fs$1.lstat(sourcePath);
if (!openedStat.isFile() || pathStat.isSymbolicLink() || !pathStat.isFile() || !sameFileIdentity(initialStat, openedStat) || !sameFileIdentity(pathStat, openedStat)) throw new Error("archive changed during validation");
output = await fs$1.open(staged.path, OPEN_WRITE_CREATE_FLAGS, 384);
const buffer = Buffer.allocUnsafe(64 * 1024);
let written = 0;
while (true) {
params.deadline.check();
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
if (bytesRead === 0) break;
written += bytesRead;
if (written > params.limits.maxArchiveBytes) throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
await writeFileHandleFully({
handle: output,
buffer,
bytes: bytesRead,
deadline: params.deadline
});
params.deadline.check();
}
await output.close();
output = void 0;
return staged;
} catch (err) {
await closeFileHandle(output);
await cleanupStagedArchiveFile(staged);
throw err;
} finally {
await closeFileHandle(handle);
}
}
async function readZipEntryStream(entry) {
if (typeof entry.nodeStream === "function") return entry.nodeStream();
const buf = await entry.async("nodebuffer");
return Readable.from(buf);
}
function resolveZipOutputPath(params) {
validateArchiveEntryPath(params.entryPath);
const relPath = stripArchivePath(params.entryPath, params.strip);
if (!relPath) return null;
validateArchiveEntryPath(relPath);
return {
relPath,
outPath: resolveArchiveOutputPath({
rootDir: params.destinationDir,
relPath,
originalPath: params.entryPath
})
};
}
async function prepareZipOutputPath(params) {
await prepareArchiveOutputPath(params);
}
async function writeZipFileEntry(params) {
params.deadline.check();
params.budget.startEntry();
const readable = await readZipEntryStream(params.entry);
const destinationPath = params.outPath;
let tempHandle = null;
let handleClosedByStream = false;
try {
await writeSiblingTempFile({
dir: path.dirname(destinationPath),
tempPrefix: `.${path.basename(destinationPath)}.fs-safe-archive`,
chmodDir: false,
mode: zipEntryFileMode(params.entry),
writeTemp: async (tempPath) => {
tempHandle = await fs$1.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 438);
const writable = tempHandle.createWriteStream();
writable.once("close", () => {
handleClosedByStream = true;
});
try {
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), writable, { signal: params.deadline.signal });
} catch (err) {
throw createPipelineTimeoutError(err, params.deadline);
}
params.deadline.check();
if (!handleClosedByStream) {
await tempHandle.close().catch(() => void 0);
handleClosedByStream = true;
}
tempHandle = null;
return destinationPath;
},
resolveFinalPath: (filePath) => filePath
});
} catch (err) {
throw err;
} finally {
const openTempHandle = tempHandle;
if (openTempHandle && !handleClosedByStream) await openTempHandle.close().catch(() => void 0);
}
}
async function extractZip(params) {
const limits = resolveExtractLimits(params.limits);
const stagedArchive = await stageArchiveFileForExtraction({
archivePath: params.archivePath,
limits,
deadline: params.deadline
});
try {
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
params.deadline.check();
const buffer = await fs$1.readFile(stagedArchive.path, { signal: params.deadline.signal });
params.deadline.check();
const zip = await waitForDeadline(loadZipArchiveWithPreflight(buffer, limits), params.deadline);
params.deadline.check();
const entries = Object.values(zip.files);
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
assertArchiveEntryCountWithinLimit(entries.length, limits);
const budget = createByteBudgetTracker(limits);
await withStagedArchiveDestination({
destinationRealDir,
run: async (stagingDir) => {
const stagingRealDir = await fs$1.realpath(stagingDir);
for (const entry of entries) {
params.deadline.check();
const output = resolveZipOutputPath({
entryPath: entry.name,
strip,
destinationDir: stagingRealDir
});
if (!output) continue;
await prepareZipOutputPath({
destinationDir: stagingRealDir,
destinationRealDir: stagingRealDir,
relPath: output.relPath,
outPath: output.outPath,
originalPath: entry.name,
isDirectory: entry.dir
});
if (entry.dir) continue;
if (isZipSymlinkEntry(entry)) throw new Error(`zip entry is a link: ${entry.name}`);
await writeZipFileEntry({
entry,
outPath: output.outPath,
budget,
deadline: params.deadline
});
}
params.deadline.check();
await mergeExtractedTreeIntoDestination({
sourceDir: stagingRealDir,
destinationDir: params.destDir,
destinationRealDir
});
params.deadline.check();
}
});
} finally {
await stagedArchive.cleanup();
}
}
async function extractArchive(params) {
const kind = params.kind ?? resolveArchiveKind(params.archivePath);
if (!kind) throw new Error(`unsupported archive: ${params.archivePath}`);
const label = kind === "zip" ? "extract zip" : "extract tar";
if (kind === "tar") {
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
const tar = await importOptionalTar();
const limits = resolveExtractLimits(params.limits);
const stagedArchive = await stageArchiveFileForExtraction({
archivePath: params.archivePath,
limits,
deadline
});
try {
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
await withStagedArchiveDestination({
destinationRealDir,
run: async (stagingDir) => {
deadline.check();
const checkTarEntrySafety = createTarEntryPreflightChecker({
rootDir: destinationRealDir,
stripComponents: params.stripComponents,
limits
});
await tar.x({
file: stagedArchive.path,
cwd: stagingDir,
strip: Math.max(0, Math.floor(params.stripComponents ?? 0)),
gzip: params.tarGzip,
signal: deadline.signal,
preservePaths: false,
strict: true,
onReadEntry(entry) {
try {
deadline.check();
checkTarEntrySafety(readTarEntryInfo(entry));
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.abort?.(error);
}
}
});
deadline.check();
await mergeExtractedTreeIntoDestination({
sourceDir: stagingDir,
destinationDir: params.destDir,
destinationRealDir
});
deadline.check();
}
});
} finally {
await stagedArchive.cleanup();
}
});
return;
}
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractZip({
archivePath: params.archivePath,
destDir: params.destDir,
stripComponents: params.stripComponents,
limits: params.limits,
deadline
}));
}
async function importOptionalTar() {
try {
return await import("tar");
} catch (err) {
throw missingOptionalArchiveDependencyError("tar", err);
}
}
function missingOptionalArchiveDependencyError(packageName, cause) {
return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use TAR archive helpers from @openclaw/fs-safe/archive.`, { cause });
}
//#endregion
export { prepareArchiveDestinationDir as a, resolvePackedRootDir as c, DEFAULT_MAX_ARCHIVE_BYTES_ZIP as d, DEFAULT_MAX_ENTRIES as f, isWindowsDrivePath as h, mergeExtractedTreeIntoDestination as i, ARCHIVE_LIMIT_ERROR_CODE as l, DEFAULT_MAX_EXTRACTED_BYTES as m, loadZipArchiveWithPreflight as n, withStagedArchiveDestination as o, DEFAULT_MAX_ENTRY_BYTES as p, createTarEntryPreflightChecker as r, resolveArchiveKind as s, extractArchive as t, ArchiveLimitError as u };