UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

219 lines (218 loc) • 7.92 kB
import { u as asPositiveFiniteNumber } from "./number-coercion-CuVhdCBs.mjs"; import { t as FsSafeError, w as root } from "./fs-safe-BUXBSN-4.mjs"; import { n as detectMime } from "./mime-CGDYrRzL.mjs"; import { o as readFileHandleBounded } from "./file-access-runtime-BMp89X4Y.mjs"; import "./security-runtime-Cl_Gnohk.mjs"; import "./number-runtime-CgyXj1A5.mjs"; import "./media-mime-DLFR6fqC.mjs"; import { a as createDeferred } from "./extension-shared-GoQu59iC.mjs"; import { n as matchesFileIdentity, r as readPathBinding, t as fileIdentity } from "./path-binding-C8xt74EN.mjs"; import { a as resolveCanonicalReadPath, n as readAbsolutePath, r as rejectCanonicalPathChange, t as classifyFsSafeReadError } from "./path-errors-C4umvZFT.mjs"; import { i as readFileFetchBinaryMaxBytes, t as FILE_FETCH_CHUNK_BYTES } from "./file-fetch-protocol-DfFx1zZa.mjs"; import path from "node:path"; import crypto from "node:crypto"; //#region extensions/file-transfer/src/node-host/file-fetch-stream.ts /** Stream the already authorized handle; START and ACK bound receiver-side buffering. */ async function streamFetchedFile(handle, maxBytes, io) { const frames = io.frames; if (!frames) throw new Error("binary file.fetch requires framed duplex IO"); io.signal.throwIfAborted(); let expected = "start"; let receipt = createDeferred(); receipt.promise.catch(() => {}); const onAbort = () => receipt.reject(io.signal.reason); io.signal.addEventListener("abort", onAbort, { once: true }); let unsubscribe; try { unsubscribe = frames.onMessage((message) => { if (!expected || Buffer.from(message).toString("utf8") !== expected) { const error = /* @__PURE__ */ new Error("Unexpected file.fetch stream control"); receipt.reject(error); throw error; } expected = void 0; receipt.resolve(); }); io.signal.throwIfAborted(); await receipt.promise; const chunk = Buffer.allocUnsafe(FILE_FETCH_CHUNK_BYTES); const hash = crypto.createHash("sha256"); let size = 0; while (true) { io.signal.throwIfAborted(); const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, maxBytes - size + 1)); io.signal.throwIfAborted(); if (!bytesRead) return { size, sha256: hash.digest("hex") }; size += bytesRead; if (size > maxBytes) throw Object.assign(/* @__PURE__ */ new Error("file grew beyond the authorized byte limit"), { code: "FILE_TOO_LARGE" }); const bytes = chunk.subarray(0, bytesRead); hash.update(bytes); receipt = createDeferred(); receipt.promise.catch(() => {}); expected = "ack"; await frames.send(bytes); await receipt.promise; } } finally { unsubscribe?.(); io.signal.removeEventListener("abort", onAbort); } } //#endregion //#region extensions/file-transfer/src/node-host/file-fetch.ts const TEXT_SNIFF_MAX_BYTES = 8192; function classifyFsError(err) { if (err instanceof FsSafeError && err.code === "too-large") return "FILE_TOO_LARGE"; const safeCode = classifyFsSafeReadError(err); if (safeCode) return safeCode; const code = err?.code; if (code === "FILE_TOO_LARGE") return code; if (code === "not-file") return "IS_DIRECTORY"; if (code === "ENOENT") return "NOT_FOUND"; if (code === "EACCES" || code === "EPERM") return "PERMISSION_DENIED"; if (code === "EISDIR") return "IS_DIRECTORY"; return "READ_ERROR"; } function isLikelyPlainText(buffer) { if (buffer.byteLength === 0) return true; const sample = buffer.subarray(0, TEXT_SNIFF_MAX_BYTES); if (sample.includes(0)) return false; try { new TextDecoder("utf-8", { fatal: true }).decode(sample); } catch { return false; } let controlBytes = 0; for (const byte of sample) if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) controlBytes += 1; return controlBytes / sample.byteLength < .01; } async function detectFetchedFileMime(params) { const detected = await detectMime(params); if (detected) return detected; return isLikelyPlainText(params.buffer) ? "text/plain" : "application/octet-stream"; } async function handleFileFetch(params, io) { const requestedPath = readAbsolutePath(params.path); if (typeof requestedPath !== "string") return requestedPath; let binaryMax; try { binaryMax = readFileFetchBinaryMaxBytes(params); } catch (error) { return { ok: false, code: "INVALID_PARAMS", message: String(error) }; } const maxBytes = binaryMax ?? Math.min(Math.floor(asPositiveFiniteNumber(params.maxBytes) ?? 8388608), 16777216); const followSymlinks = params.followSymlinks === true; const preflightOnly = params.preflightOnly === true; const requestedRoot = params.rootPath === void 0 ? void 0 : readAbsolutePath(params.rootPath); if (requestedRoot !== void 0 && typeof requestedRoot !== "string") return requestedRoot; const canonical = await resolveCanonicalReadPath({ requestedPath: requestedRoot ?? requestedPath, followSymlinks: requestedRoot === void 0 && followSymlinks, classifyError: classifyFsError, notFoundMessage: "file not found" }); if (typeof canonical !== "string") return canonical; let opened; try { if (requestedRoot !== void 0) opened = await (await root(canonical)).open(path.relative(canonical, requestedPath), { symlinks: followSymlinks ? "follow-parents-within-root" : "reject" }); else opened = await (await root(path.dirname(canonical))).open(path.basename(canonical)); } catch (err) { const code = classifyFsError(err); return { ok: false, code, message: code === "IS_DIRECTORY" ? "path is a directory" : `open failed: ${String(err)}`, canonicalPath: canonical }; } try { const canonicalPathChange = rejectCanonicalPathChange(params.expectedCanonicalPath, opened.realPath); if (canonicalPathChange) return canonicalPathChange; const stats = opened.stat; const identityStats = await opened.handle.stat({ bigint: true }); const identity = fileIdentity(identityStats); const expectedBinding = readPathBinding(params.expectedBinding); if (params.expectedBinding !== void 0 && expectedBinding?.kind !== "existing" || expectedBinding?.kind === "existing" && !matchesFileIdentity(identityStats, expectedBinding)) return { ok: false, code: "CANONICAL_PATH_CHANGED", message: "filesystem identity differs from the authorized target", canonicalPath: opened.realPath }; if (stats.size > maxBytes) return { ok: false, code: "FILE_TOO_LARGE", message: `file size ${stats.size} exceeds limit ${maxBytes}`, canonicalPath: opened.realPath }; if (preflightOnly) return { ok: true, path: opened.realPath, size: stats.size, mimeType: "", base64: "", sha256: "", preflightOnly: true, binding: { kind: "existing", ...identity } }; if (binaryMax !== void 0) { if (!io?.frames || expectedBinding?.kind !== "existing") return { ok: false, code: "INVALID_PARAMS", message: "binary file.fetch requires duplex IO and an authorized filesystem binding" }; const receipt = await streamFetchedFile(opened.handle, maxBytes, io); return { ok: true, path: opened.realPath, ...receipt, transport: "binary", mimeType: "", base64: "", binding: { kind: "existing", ...identity } }; } const buffer = await readFileHandleBounded(opened.handle, maxBytes); const sha256 = crypto.createHash("sha256").update(buffer).digest("hex"); const base64 = buffer.toString("base64"); const mimeType = await detectFetchedFileMime({ buffer, filePath: opened.realPath }); return { ok: true, path: opened.realPath, size: buffer.byteLength, mimeType, base64, sha256, binding: { kind: "existing", ...identity } }; } catch (err) { return { ok: false, code: classifyFsError(err), message: `read failed: ${String(err)}`, canonicalPath: opened.realPath }; } finally { await opened.handle.close().catch(() => void 0); } } //#endregion export { handleFileFetch };