openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
322 lines (321 loc) • 10.6 kB
JavaScript
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import "./file-access-runtime-CfcyqU8y.js";
import "./state-paths-2IWTvznc.js";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
//#region extensions/browser/src/browser/extension-native-protocol.ts
const BROWSER_NATIVE_REQUEST_MAX_BYTES = 4096;
const BROWSER_NATIVE_RESPONSE_MAX_BYTES = 1048576;
const NONCE_PATTERN = /^[A-Za-z0-9_-]+$/;
function readNativeUint32(buffer, offset = 0) {
return os.endianness() === "LE" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
}
function writeNativeUint32(buffer, value, offset = 0) {
if (os.endianness() === "LE") buffer.writeUInt32LE(value, offset);
else buffer.writeUInt32BE(value, offset);
}
function rootJsonKeys(raw) {
const keys = [];
let index = 0;
const skipWhitespace = () => {
while (/\s/u.test(raw[index] ?? "")) index += 1;
};
const readJsonString = () => {
if (raw[index] !== "\"") return null;
const start = index++;
while (index < raw.length) {
const char = raw[index++];
if (char === "\\") index += 1;
else if (char === "\"") try {
return JSON.parse(raw.slice(start, index));
} catch {
return null;
}
}
return null;
};
const skipValue = () => {
let depth = 0;
let inString = false;
let escaped = false;
while (index < raw.length) {
const char = raw[index];
if (inString) {
index += 1;
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === "\"") inString = false;
continue;
}
if (char === "\"") {
inString = true;
index += 1;
continue;
}
if (char === "{" || char === "[") depth += 1;
else if (char === "}" || char === "]") {
if (depth === 0) return true;
depth -= 1;
} else if (char === "," && depth === 0) return true;
index += 1;
}
return true;
};
skipWhitespace();
if (raw[index++] !== "{") return null;
for (;;) {
skipWhitespace();
if (raw[index] === "}") return keys;
const key = readJsonString();
if (key === null) return null;
keys.push(key);
skipWhitespace();
if (raw[index++] !== ":") return null;
skipWhitespace();
if (!skipValue()) return null;
skipWhitespace();
if (raw[index] === ",") {
index += 1;
continue;
}
return raw[index] === "}" ? keys : null;
}
}
function isCanonicalNonce(value) {
if (typeof value !== "string" || !NONCE_PATTERN.test(value)) return false;
const bytes = Buffer.from(value, "base64url");
return bytes.length >= 16 && bytes.length <= 32 && bytes.toString("base64url") === value;
}
/** Strictly validate one decoded native bootstrap request. */
function parseBrowserNativeRequest(raw) {
const keys = rootJsonKeys(raw);
if (!keys || new Set(keys).size !== keys.length) return null;
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
const record = asNullableRecord(parsed);
if (record?.v !== 1 || !isCanonicalNonce(record.nonce)) return null;
const expected = record.op === "ensure_relay" ? [
"v",
"op",
"nonce",
"relayPort"
] : [
"v",
"op",
"nonce"
];
if (keys.length !== expected.length || !expected.every((key) => keys.includes(key))) return null;
if (record.op === "bootstrap") return {
v: 1,
op: "bootstrap",
nonce: record.nonce
};
return record.op === "ensure_relay" && typeof record.relayPort === "number" && Number.isInteger(record.relayPort) && record.relayPort >= 1 && record.relayPort <= 65535 ? {
v: 1,
op: "ensure_relay",
nonce: record.nonce,
relayPort: record.relayPort
} : null;
}
function decodeBrowserNativeFrame(frame) {
if (frame.length < 4) return {
ok: false,
code: "invalid_frame"
};
const length = readNativeUint32(frame);
if (length === 0 || length > BROWSER_NATIVE_REQUEST_MAX_BYTES || frame.length !== length + 4) return {
ok: false,
code: "invalid_frame"
};
let raw;
try {
raw = new TextDecoder("utf-8", { fatal: true }).decode(frame.subarray(4));
} catch {
return {
ok: false,
code: "invalid_utf8"
};
}
const request = parseBrowserNativeRequest(raw);
return request ? {
ok: true,
request
} : {
ok: false,
code: "invalid_request"
};
}
/** Read exactly one small frame without allocating from an untrusted length. */
async function readBrowserNativeFrame(input) {
let buffered = Buffer.alloc(0);
let expected = 4;
for await (const chunk of input) {
if (buffered.length + chunk.length > 4100) throw new Error("invalid_frame");
buffered = Buffer.concat([buffered, chunk], buffered.length + chunk.length);
if (expected === 4 && buffered.length >= 4) {
const length = readNativeUint32(buffered);
if (length === 0 || length > BROWSER_NATIVE_REQUEST_MAX_BYTES) throw new Error("invalid_frame");
expected = length + 4;
}
if (buffered.length >= expected) {
if (buffered.length !== expected) throw new Error("invalid_frame");
return buffered;
}
}
throw new Error("invalid_frame");
}
function encodeBrowserNativeResponse(response) {
const payload = Buffer.from(JSON.stringify(response), "utf8");
if (payload.length >= BROWSER_NATIVE_RESPONSE_MAX_BYTES) throw new Error("native response exceeds Chrome's 1 MiB limit");
const frame = Buffer.allocUnsafe(payload.length + 4);
writeNativeUint32(frame, payload.length);
payload.copy(frame, 4);
return frame;
}
//#endregion
//#region extensions/browser/src/browser/extension-native-host.ts
const BROWSER_NATIVE_HOST_NAME = "ai.openclaw.browser_bootstrap";
const EXTENSION_ORIGIN_PATTERN = /^chrome-extension:\/\/[a-p]{32}\/$/;
function validateExpectedOrigins(origins) {
const canonical = [...new Set(origins)].toSorted();
if (origins.length === 0 || origins.length !== canonical.length || origins.some((origin, index) => origin !== canonical[index]) || origins.some((origin) => !EXTENSION_ORIGIN_PATTERN.test(origin))) throw new Error("invalid expected origins");
return canonical;
}
function parseBrowserNativeHostOrigins(argv) {
const expectedOrigins = [];
let callerOrigin = "";
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--expected-origin") {
const value = argv[index + 1];
if (!value || callerOrigin) throw new Error("invalid expected-origin arguments");
expectedOrigins.push(value);
index += 1;
} else if (argument?.startsWith("chrome-extension://")) {
if (callerOrigin) throw new Error("multiple Chrome extension origins");
callerOrigin = argument;
}
}
validateExpectedOrigins(expectedOrigins);
if (!EXTENSION_ORIGIN_PATTERN.test(callerOrigin)) throw new Error("missing Chrome extension origin");
return {
expectedOrigins,
callerOrigin
};
}
async function validateOwnedFile(filePath, executable) {
const resolved = path.resolve(filePath);
const info = await fs.lstat(resolved);
if (!info.isFile() || info.isSymbolicLink()) throw new Error("unsafe file type");
if (process.platform !== "win32") {
const uid = process.getuid?.();
if (uid !== void 0 && info.uid !== uid) throw new Error("foreign file owner");
const mode = info.mode & 511;
if ((mode & 63) !== 0 || executable && (mode & 64) === 0) throw new Error("unsafe file mode");
}
const canonical = await fs.realpath(resolved);
if (canonical !== resolved) throw new Error("non-canonical file path");
return canonical;
}
async function validateNativeManifest(params) {
const manifestPath = await validateOwnedFile(params.manifestPath, false);
const launcherPath = await validateOwnedFile(params.launcherPath, true);
const managedRoot = path.resolve(params.stateDir ?? resolveStateDir(), "browser", "native-messaging");
if (!isPathInside(managedRoot, launcherPath)) throw new Error("launcher is outside the managed root");
const parsed = JSON.parse(await fs.readFile(manifestPath, "utf8"));
const manifestRecord = asNullableRecord(parsed);
if (!manifestRecord) throw new Error("invalid manifest");
const manifest = manifestRecord;
const expectedOrigins = validateExpectedOrigins(params.expectedOrigins);
const keys = [
"name",
"description",
"path",
"type",
"allowed_origins"
];
if (Object.keys(manifest).length !== keys.length || !keys.every((key) => Object.hasOwn(manifest, key)) || manifest.name !== "ai.openclaw.browser_bootstrap" || manifest.type !== "stdio" || manifest.path !== launcherPath || !Array.isArray(manifest.allowed_origins) || JSON.stringify(manifest.allowed_origins) !== JSON.stringify(expectedOrigins)) throw new Error("invalid manifest");
if (!expectedOrigins.includes(params.callerOrigin)) throw new Error("origin forbidden");
}
/** Run one request/response native host process. */
async function runBrowserNativeHost(params) {
let response;
try {
const decoded = decodeBrowserNativeFrame(await readBrowserNativeFrame(params.input));
if (!decoded.ok) response = {
v: 1,
ok: false,
code: decoded.code
};
else if ((params.platform ?? process.platform) === "win32") response = {
v: 1,
ok: false,
code: "manual_required"
};
else {
try {
await validateNativeManifest(params);
} catch (error) {
response = {
v: 1,
ok: false,
code: error instanceof Error && error.message === "origin forbidden" ? "origin_forbidden" : "manifest_invalid"
};
params.write(encodeBrowserNativeResponse(response));
return response;
}
if (decoded.request.op === "ensure_relay") try {
const relay = await params.ensureRelay(decoded.request.relayPort);
response = {
v: 1,
ok: true,
nonce: decoded.request.nonce,
relay
};
} catch {
response = {
v: 1,
ok: false,
code: "relay_unavailable"
};
}
else try {
const pairing = await params.buildPairing();
response = pairing.topology === "direct-remote" ? {
v: 1,
ok: false,
code: "manual_required"
} : {
v: 1,
ok: true,
nonce: decoded.request.nonce,
pairingString: pairing.pairingString
};
} catch (error) {
response = {
v: 1,
ok: false,
code: error instanceof Error && error.message.includes("--gateway-url") ? "manual_required" : "pairing_unavailable"
};
}
}
} catch {
response = {
v: 1,
ok: false,
code: "invalid_frame"
};
}
params.write(encodeBrowserNativeResponse(response));
return response;
}
//#endregion
export { parseBrowserNativeHostOrigins as n, runBrowserNativeHost as r, BROWSER_NATIVE_HOST_NAME as t };