UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

225 lines (224 loc) 9.23 kB
import { p as readStringValue } from "./string-coerce-mnp54Vah.js"; import { a as asRecord } from "./record-coerce-DHZ4bFlT.js"; import { s as asFiniteNumber, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { n as resolveCliName } from "./cli-name-CAJoj2J5.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js"; import { t as normalizeHostname } from "./hostname-D8MH4bbf.js"; import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js"; import { t as asBoolean } from "./boolean-By0bZpFH.js"; import fs from "node:fs"; import * as path$1 from "node:path"; import * as fs$2 from "node:fs/promises"; import { randomUUID } from "node:crypto"; //#region src/cli/nodes-media-utils.ts const asString = readStringValue; function resolveTempPathParts(opts) { const tmpDir = opts.tmpDir ?? resolvePreferredOpenClawTmpDir(); const rawExt = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`; if (!/^\.[A-Za-z0-9][A-Za-z0-9_-]{0,15}$/u.test(rawExt)) throw new Error("invalid media format"); if (!opts.tmpDir) fs.mkdirSync(tmpDir, { recursive: true, mode: 448 }); return { tmpDir, id: opts.id ?? randomUUID(), ext: rawExt }; } //#endregion //#region src/cli/nodes-camera.ts const MAX_CAMERA_URL_DOWNLOAD_BYTES = 250 * 1024 * 1024; const MAX_CAMERA_BASE64_BYTES = MAX_CAMERA_URL_DOWNLOAD_BYTES; /** Validate and normalize an unknown camera still-image payload. */ function parseCameraSnapPayload(value) { const obj = asRecord(value); const format = asString(obj.format); const base64 = asString(obj.base64); const url = asString(obj.url); const width = asFiniteNumber(obj.width); const height = asFiniteNumber(obj.height); if (!format || !base64 && !url || width === void 0 || height === void 0) throw new Error("invalid camera.snap payload"); return { format, ...base64 ? { base64 } : {}, ...url ? { url } : {}, width, height }; } /** Validate and normalize an unknown camera clip payload. */ function parseCameraClipPayload(value) { const obj = asRecord(value); const format = asString(obj.format); const base64 = asString(obj.base64); const url = asString(obj.url); const durationMs = asFiniteNumber(obj.durationMs); const hasAudio = asBoolean(obj.hasAudio); if (!format || !base64 && !url || durationMs === void 0 || hasAudio === void 0) throw new Error("invalid camera.clip payload"); return { format, ...base64 ? { base64 } : {}, ...url ? { url } : {}, durationMs, hasAudio }; } /** Build a deterministic temp path for a camera artifact. */ function cameraTempPath(opts) { const { tmpDir, id, ext } = resolveTempPathParts({ tmpDir: opts.tmpDir, id: opts.id, ext: opts.ext }); const facingPart = opts.facing ? `-${opts.facing}` : ""; const cliName = resolveCliName(); return path$1.join(tmpDir, `${cliName}-camera-${opts.kind}${facingPart}-${id}${ext}`); } /** Download a node-hosted media URL to disk after HTTPS, host, redirect, and size checks. */ async function writeUrlToFile(filePath, url, opts) { const parsed = new URL(url); if (parsed.protocol !== "https:") throw new Error(`writeUrlToFile: only https URLs are allowed, got ${parsed.protocol}`); const expectedHost = normalizeHostname(opts.expectedHost); if (!expectedHost) throw new Error("writeUrlToFile: expectedHost is required"); if (normalizeHostname(parsed.hostname) !== expectedHost) throw new Error(`writeUrlToFile: url host ${parsed.hostname} must match node host ${opts.expectedHost}`); const policy = { allowPrivateNetwork: true, allowedHostnames: [expectedHost], hostnameAllowlist: [expectedHost] }; let release = async () => {}; let bytes = 0; try { const guarded = await fetchWithSsrFGuard({ url, auditContext: "writeUrlToFile", policy }); release = guarded.release; const finalUrl = new URL(guarded.finalUrl); if (finalUrl.protocol !== "https:") throw new Error(`writeUrlToFile: redirect resolved to non-https URL ${guarded.finalUrl}`); if (normalizeHostname(finalUrl.hostname) !== expectedHost) throw new Error(`writeUrlToFile: redirect host ${finalUrl.hostname} must match node host ${opts.expectedHost}`); const res = guarded.response; if (!res.ok) throw new Error(`failed to download ${url}: ${res.status} ${res.statusText}`); const contentLength = parseStrictNonNegativeInteger(res.headers.get("content-length")); if (typeof contentLength === "number" && Number.isFinite(contentLength) && contentLength > MAX_CAMERA_URL_DOWNLOAD_BYTES) throw new Error(`writeUrlToFile: content-length ${contentLength} exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`); const body = res.body; if (!body) throw new Error(`failed to download ${url}: empty response body`); const fileHandle = await fs$2.open(filePath, "w"); let thrown; try { const reader = body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; if (!value || value.byteLength === 0) continue; bytes += value.byteLength; if (bytes > MAX_CAMERA_URL_DOWNLOAD_BYTES) throw new Error(`writeUrlToFile: downloaded ${bytes} bytes, exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`); await fileHandle.write(value); } } catch (err) { thrown = err; } finally { await fileHandle.close(); } if (thrown) { await fs$2.unlink(filePath).catch(() => {}); throw toLintErrorObject(thrown, "Non-Error thrown"); } } finally { await release(); } return { path: filePath, bytes }; } function estimateDecodedBase64Bytes(base64) { const normalized = base64.replace(/\s+/g, ""); const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0; return Math.floor(normalized.length * 3 / 4) - padding; } /** Decode a base64 media payload to disk with preflight and post-decode size checks. */ async function writeBase64ToFile(filePath, base64, opts = {}) { const maxBytes = opts.maxBytes ?? MAX_CAMERA_BASE64_BYTES; if (estimateDecodedBase64Bytes(base64) > maxBytes) throw new Error(`writeBase64ToFile: decoded payload exceeds max ${maxBytes}`); const buf = Buffer.from(base64, "base64"); if (buf.length > maxBytes) throw new Error(`writeBase64ToFile: decoded ${buf.length} bytes, exceeds max ${maxBytes}`); await fs$2.writeFile(filePath, buf); return { path: filePath, bytes: buf.length }; } /** Require the node remote IP needed to validate URL-backed camera payloads. */ function requireNodeRemoteIp(remoteIp) { const normalized = remoteIp?.trim(); if (!normalized) throw new Error("camera URL payload requires node remoteIp"); return normalized; } /** Write either a URL-backed or base64-backed camera payload to disk. */ async function writeCameraPayloadToFile(params) { if (params.payload.url) { await writeUrlToFile(params.filePath, params.payload.url, { expectedHost: requireNodeRemoteIp(params.expectedHost) }); return; } if (params.payload.base64) { await writeBase64ToFile(params.filePath, params.payload.base64); return; } throw new Error(params.invalidPayloadMessage ?? "invalid camera payload"); } /** Write a camera clip payload to a generated temp file and return its path. */ async function writeCameraClipPayloadToFile(params) { const filePath = cameraTempPath({ kind: "clip", facing: params.facing, ext: params.payload.format, tmpDir: params.tmpDir, id: params.id }); await writeCameraPayloadToFile({ filePath, payload: params.payload, expectedHost: params.expectedHost, invalidPayloadMessage: "invalid camera.clip payload" }); return filePath; } function toLintErrorObject(value, fallbackMessage) { if (value instanceof Error) return value; if (typeof value === "string") return new Error(value); const error = new Error(fallbackMessage, { cause: value }); if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value); return error; } //#endregion //#region src/cli/nodes-screen.ts /** Validate and normalize an unknown screen-record payload. */ function parseScreenRecordPayload(value) { const obj = asRecord(value); const format = asString(obj.format); const base64 = asString(obj.base64); if (!format || !base64) throw new Error("invalid screen.record payload"); return { format, base64, durationMs: typeof obj.durationMs === "number" ? obj.durationMs : void 0, fps: typeof obj.fps === "number" ? obj.fps : void 0, screenIndex: typeof obj.screenIndex === "number" ? obj.screenIndex : void 0, hasAudio: typeof obj.hasAudio === "boolean" ? obj.hasAudio : void 0 }; } /** Build the temp output path for a screen recording artifact. */ function screenRecordTempPath(opts) { const { tmpDir, id, ext } = resolveTempPathParts(opts); return path$1.join(tmpDir, `openclaw-screen-record-${id}${ext}`); } /** Decode and write a screen recording payload to disk. */ async function writeScreenRecordToFile(filePath, base64, opts) { return writeBase64ToFile(filePath, base64, opts); } //#endregion export { parseCameraClipPayload as a, writeCameraPayloadToFile as c, cameraTempPath as i, screenRecordTempPath as n, parseCameraSnapPayload as o, writeScreenRecordToFile as r, writeCameraClipPayloadToFile as s, parseScreenRecordPayload as t };