First Release of Claw3D (#11)

Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Luke The Dev
2026-03-19 23:14:04 -05:00
committed by GitHub
parent 5ea96b2650
commit 4fa4f13558
431 changed files with 105438 additions and 14 deletions
+56
View File
@@ -0,0 +1,56 @@
const parseCookies = (header) => {
const raw = typeof header === "string" ? header : "";
if (!raw.trim()) return {};
const out = {};
for (const part of raw.split(";")) {
const idx = part.indexOf("=");
if (idx === -1) continue;
const key = part.slice(0, idx).trim();
const value = part.slice(idx + 1).trim();
if (!key) continue;
out[key] = value;
}
return out;
};
function createAccessGate(options) {
const token = String(options?.token ?? "").trim();
const cookieName = String(options?.cookieName ?? "studio_access").trim() || "studio_access";
const enabled = Boolean(token);
const isAuthorized = (req) => {
if (!enabled) return true;
const cookieHeader = req.headers?.cookie;
const cookies = parseCookies(cookieHeader);
return cookies[cookieName] === token;
};
const handleHttp = (req, res) => {
if (!enabled) return false;
if (String(req.url || "/").startsWith("/api/")) {
if (!isAuthorized(req)) {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: "Studio access token required. Send the configured Studio access cookie and retry.",
})
);
return true;
}
}
return false;
};
const allowUpgrade = (req) => {
if (!enabled) return true;
return isAuthorized(req);
};
return { enabled, handleHttp, allowUpgrade };
}
module.exports = { createAccessGate };
+308
View File
@@ -0,0 +1,308 @@
const { WebSocket, WebSocketServer } = require("ws");
const buildErrorResponse = (id, code, message) => {
return {
type: "res",
id,
ok: false,
error: { code, message },
};
};
const isObject = (value) => Boolean(value && typeof value === "object");
const safeJsonParse = (raw) => {
try {
return JSON.parse(raw);
} catch {
return null;
}
};
const resolvePathname = (url) => {
const raw = typeof url === "string" ? url : "";
const idx = raw.indexOf("?");
return (idx === -1 ? raw : raw.slice(0, idx)) || "/";
};
const injectAuthToken = (params, token) => {
const next = isObject(params) ? { ...params } : {};
const auth = isObject(next.auth) ? { ...next.auth } : {};
auth.token = token;
next.auth = auth;
return next;
};
const resolveOriginForUpstream = (upstreamUrl) => {
const url = new URL(upstreamUrl);
const proto = url.protocol === "wss:" ? "https:" : "http:";
const hostname =
url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "0.0.0.0"
? "localhost"
: url.hostname;
const host = url.port ? `${hostname}:${url.port}` : hostname;
return `${proto}//${host}`;
};
const hasNonEmptyToken = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.token : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasNonEmptyPassword = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.password : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasNonEmptyDeviceToken = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.deviceToken : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasCompleteDeviceAuth = (params) => {
const device = params && isObject(params) && isObject(params.device) ? params.device : null;
if (!device) {
return false;
}
const id = typeof device.id === "string" ? device.id.trim() : "";
const publicKey = typeof device.publicKey === "string" ? device.publicKey.trim() : "";
const signature = typeof device.signature === "string" ? device.signature.trim() : "";
const nonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
const signedAt = device.signedAt;
return (
id.length > 0 &&
publicKey.length > 0 &&
signature.length > 0 &&
nonce.length > 0 &&
Number.isFinite(signedAt) &&
signedAt >= 0
);
};
function createGatewayProxy(options) {
const {
loadUpstreamSettings,
allowWs = (req) => resolvePathname(req.url) === "/api/gateway/ws",
log = () => {},
logError = (msg, err) => console.error(msg, err),
} = options || {};
if (typeof loadUpstreamSettings !== "function") {
throw new Error("createGatewayProxy requires loadUpstreamSettings().");
}
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (browserWs) => {
let upstreamWs = null;
let upstreamReady = false;
let upstreamUrl = "";
let upstreamToken = "";
let connectRequestId = null;
let connectResponseSent = false;
let pendingConnectFrame = null;
let pendingUpstreamSetupError = null;
let closed = false;
const closeBoth = (code, reason) => {
if (closed) return;
closed = true;
try {
browserWs.close(code, reason);
} catch {}
try {
upstreamWs?.close(code, reason);
} catch {}
};
const sendToBrowser = (frame) => {
if (browserWs.readyState !== WebSocket.OPEN) return;
browserWs.send(JSON.stringify(frame));
};
const sendConnectError = (code, message) => {
if (connectRequestId && !connectResponseSent) {
connectResponseSent = true;
sendToBrowser(buildErrorResponse(connectRequestId, code, message));
}
closeBoth(1011, "connect failed");
};
const forwardConnectFrame = (frame) => {
const browserHasAuth =
hasNonEmptyToken(frame.params) ||
hasNonEmptyPassword(frame.params) ||
hasNonEmptyDeviceToken(frame.params) ||
hasCompleteDeviceAuth(frame.params);
if (!upstreamToken && !browserHasAuth) {
sendConnectError(
"studio.gateway_token_missing",
"Upstream gateway token is not configured on the Studio host."
);
return;
}
const connectFrame = browserHasAuth
? frame
: {
...frame,
params: injectAuthToken(frame.params, upstreamToken),
};
upstreamWs.send(JSON.stringify(connectFrame));
};
const maybeForwardPendingConnect = () => {
if (!pendingConnectFrame || !upstreamReady || upstreamWs?.readyState !== WebSocket.OPEN) {
return;
}
const frame = pendingConnectFrame;
pendingConnectFrame = null;
forwardConnectFrame(frame);
};
const startUpstream = async () => {
try {
const settings = await loadUpstreamSettings();
upstreamUrl = typeof settings?.url === "string" ? settings.url.trim() : "";
upstreamToken = typeof settings?.token === "string" ? settings.token.trim() : "";
} catch (err) {
logError("Failed to load upstream gateway settings.", err);
pendingUpstreamSetupError = {
code: "studio.settings_load_failed",
message: "Failed to load Studio gateway settings.",
};
return;
}
if (!upstreamUrl) {
pendingUpstreamSetupError = {
code: "studio.gateway_url_missing",
message: "Upstream gateway URL is not configured on the Studio host.",
};
return;
}
let upstreamOrigin = "";
try {
upstreamOrigin = resolveOriginForUpstream(upstreamUrl);
} catch {
pendingUpstreamSetupError = {
code: "studio.gateway_url_invalid",
message: "Upstream gateway URL is invalid on the Studio host.",
};
return;
}
upstreamWs = new WebSocket(upstreamUrl, { origin: upstreamOrigin });
upstreamWs.on("open", () => {
upstreamReady = true;
maybeForwardPendingConnect();
});
upstreamWs.on("message", (upRaw) => {
const upParsed = safeJsonParse(String(upRaw ?? ""));
if (upParsed && isObject(upParsed) && upParsed.type === "res") {
const resId = typeof upParsed.id === "string" ? upParsed.id : "";
if (resId && connectRequestId && resId === connectRequestId) {
connectResponseSent = true;
}
}
if (browserWs.readyState === WebSocket.OPEN) {
browserWs.send(String(upRaw ?? ""));
}
});
upstreamWs.on("close", (ev) => {
const reason = typeof ev?.reason === "string" ? ev.reason : "";
if (!connectResponseSent && connectRequestId) {
sendToBrowser(
buildErrorResponse(
connectRequestId,
"studio.upstream_closed",
`Upstream gateway closed (${ev.code}): ${reason}`
)
);
}
closeBoth(1012, "upstream closed");
});
upstreamWs.on("error", (err) => {
logError("Upstream gateway WebSocket error.", err);
sendConnectError(
"studio.upstream_error",
"Failed to connect to upstream gateway WebSocket."
);
});
log("proxy connected");
};
void startUpstream();
browserWs.on("message", async (raw) => {
const parsed = safeJsonParse(String(raw ?? ""));
if (!parsed || !isObject(parsed)) {
closeBoth(1003, "invalid json");
return;
}
if (!connectRequestId) {
if (parsed.type !== "req" || parsed.method !== "connect") {
closeBoth(1008, "connect required");
return;
}
const id = typeof parsed.id === "string" ? parsed.id : "";
if (!id) {
closeBoth(1008, "connect id required");
return;
}
connectRequestId = id;
if (pendingUpstreamSetupError) {
sendConnectError(pendingUpstreamSetupError.code, pendingUpstreamSetupError.message);
return;
}
pendingConnectFrame = parsed;
maybeForwardPendingConnect();
return;
}
if (!upstreamReady || upstreamWs.readyState !== WebSocket.OPEN) {
closeBoth(1013, "upstream not ready");
return;
}
if (parsed.type === "req" && parsed.method === "connect" && !connectResponseSent) {
pendingConnectFrame = null;
forwardConnectFrame(parsed);
return;
}
upstreamWs.send(JSON.stringify(parsed));
});
browserWs.on("close", () => {
closeBoth(1000, "client closed");
});
browserWs.on("error", (err) => {
logError("Browser WebSocket error.", err);
closeBoth(1011, "client error");
});
});
const handleUpgrade = (req, socket, head) => {
if (!allowWs(req)) {
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
};
return { wss, handleUpgrade };
}
module.exports = { createGatewayProxy };
+130
View File
@@ -0,0 +1,130 @@
const http = require("node:http");
const next = require("next");
const { createAccessGate } = require("./access-gate");
const { createGatewayProxy } = require("./gateway-proxy");
const { assertPublicHostAllowed, resolveHosts } = require("./network-policy");
const { loadUpstreamGatewaySettings } = require("./studio-settings");
const resolvePort = () => {
const raw = process.env.PORT?.trim() || "3000";
const port = Number(raw);
if (!Number.isFinite(port) || port <= 0) return 3000;
return port;
};
const resolvePathname = (url) => {
const raw = typeof url === "string" ? url : "";
const idx = raw.indexOf("?");
return (idx === -1 ? raw : raw.slice(0, idx)) || "/";
};
async function main() {
const dev = process.argv.includes("--dev");
const hostnames = Array.from(new Set(resolveHosts(process.env)));
const hostname = hostnames[0] ?? "127.0.0.1";
const port = resolvePort();
for (const host of hostnames) {
assertPublicHostAllowed({
host,
studioAccessToken: process.env.STUDIO_ACCESS_TOKEN,
});
}
const app = next({
dev,
hostname,
port,
...(dev ? { webpack: true } : null),
});
const handle = app.getRequestHandler();
const accessGate = createAccessGate({
token: process.env.STUDIO_ACCESS_TOKEN,
});
const proxy = createGatewayProxy({
loadUpstreamSettings: async () => {
const settings = loadUpstreamGatewaySettings(process.env);
return { url: settings.url, token: settings.token };
},
allowWs: (req) => {
if (resolvePathname(req.url) !== "/api/gateway/ws") return false;
if (!accessGate.allowUpgrade(req)) return false;
return true;
},
});
await app.prepare();
const handleUpgrade = app.getUpgradeHandler();
const handleServerUpgrade = (req, socket, head) => {
if (resolvePathname(req.url) === "/api/gateway/ws") {
proxy.handleUpgrade(req, socket, head);
return;
}
handleUpgrade(req, socket, head);
};
const createServer = () =>
http.createServer((req, res) => {
if (accessGate.handleHttp(req, res)) return;
handle(req, res);
});
const servers = hostnames.map(() => createServer());
const attachUpgradeHandlers = (server) => {
server.on("upgrade", handleServerUpgrade);
server.on("newListener", (eventName, listener) => {
if (eventName !== "upgrade") return;
if (listener === handleServerUpgrade) return;
process.nextTick(() => {
server.removeListener("upgrade", listener);
});
});
};
for (const server of servers) {
attachUpgradeHandlers(server);
}
const listenOnHost = (server, host) =>
new Promise((resolve, reject) => {
const onError = (err) => {
server.off("error", onError);
reject(err);
};
server.once("error", onError);
server.listen(port, host, () => {
server.off("error", onError);
resolve();
});
});
const closeServer = (server) =>
new Promise((resolve) => {
if (!server.listening) return resolve();
server.close(() => resolve());
});
try {
await Promise.all(servers.map((server, index) => listenOnHost(server, hostnames[index])));
} catch (err) {
await Promise.all(servers.map((server) => closeServer(server)));
throw err;
}
const hostForBrowser = hostnames.some((value) => value === "127.0.0.1" || value === "::1")
? "localhost"
: hostname === "0.0.0.0" || hostname === "::"
? "localhost"
: hostname;
const browserUrl = `http://${hostForBrowser}:${port}`;
console.info(`Open in browser: ${browserUrl}`);
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
+84
View File
@@ -0,0 +1,84 @@
const net = require("node:net");
const normalizeHost = (host) => {
let raw = String(host ?? "").trim().toLowerCase();
if (!raw) return "";
if (raw.startsWith("[")) {
const end = raw.indexOf("]");
if (end !== -1) {
return raw.slice(1, end).trim();
}
}
const colonCount = (raw.match(/:/g) || []).length;
if (colonCount === 1) {
const idx = raw.lastIndexOf(":");
const maybePort = raw.slice(idx + 1);
if (/^\d+$/.test(maybePort)) {
raw = raw.slice(0, idx);
}
}
return raw;
};
const resolveHosts = (env = process.env) => {
const host = String(env.HOST ?? "").trim();
if (host) return [host];
return ["127.0.0.1", "::1"];
};
const resolveHost = (env = process.env) => {
const hosts = resolveHosts(env);
return hosts[0] ?? "127.0.0.1";
};
const isIpv4Loopback = (value) => value.startsWith("127.");
const isIpv6Loopback = (value) => {
if (value === "::1" || value === "0:0:0:0:0:0:0:1") return true;
if (!value.startsWith("::ffff:")) return false;
const mapped = value.slice("::ffff:".length);
return net.isIP(mapped) === 4 && isIpv4Loopback(mapped);
};
const isPublicHost = (host) => {
const normalized = normalizeHost(host);
if (!normalized) return false;
if (normalized === "localhost") return false;
if (normalized === "0.0.0.0" || normalized === "::") {
return true;
}
const ipVersion = net.isIP(normalized);
if (ipVersion === 4) {
return !isIpv4Loopback(normalized);
}
if (ipVersion === 6) {
return !isIpv6Loopback(normalized);
}
return true;
};
const assertPublicHostAllowed = ({ host, studioAccessToken }) => {
if (!isPublicHost(host)) return;
const token = String(studioAccessToken ?? "").trim();
if (token) return;
const normalized = normalizeHost(host) || String(host ?? "").trim() || "(unknown)";
throw new Error(
`Refusing to bind Studio to public host "${normalized}" without STUDIO_ACCESS_TOKEN. ` +
"Set STUDIO_ACCESS_TOKEN or bind HOST to 127.0.0.1/::1/localhost."
);
};
module.exports = {
resolveHosts,
resolveHost,
isPublicHost,
assertPublicHostAllowed,
};
+124
View File
@@ -0,0 +1,124 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const LEGACY_STATE_DIRNAMES = [".clawdbot", ".moltbot"];
const NEW_STATE_DIRNAME = ".openclaw";
const resolveUserPath = (input) => {
const trimmed = String(input ?? "").trim();
if (!trimmed) return trimmed;
if (trimmed.startsWith("~")) {
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
return path.resolve(expanded);
}
return path.resolve(trimmed);
};
const resolveDefaultHomeDir = () => {
const home = os.homedir();
if (home) {
try {
if (fs.existsSync(home)) return home;
} catch {}
}
return os.tmpdir();
};
const resolveStateDir = (env = process.env) => {
const override =
env.OPENCLAW_STATE_DIR?.trim() ||
env.MOLTBOT_STATE_DIR?.trim() ||
env.CLAWDBOT_STATE_DIR?.trim();
if (override) return resolveUserPath(override);
const home = resolveDefaultHomeDir();
const newDir = path.join(home, NEW_STATE_DIRNAME);
const legacyDirs = LEGACY_STATE_DIRNAMES.map((dir) => path.join(home, dir));
try {
if (fs.existsSync(newDir)) return newDir;
} catch {}
for (const dir of legacyDirs) {
try {
if (fs.existsSync(dir)) return dir;
} catch {}
}
return newDir;
};
const resolveStudioSettingsPath = (env = process.env) => {
return path.join(resolveStateDir(env), "claw3d", "settings.json");
};
const readJsonFile = (filePath) => {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf8");
return JSON.parse(raw);
};
const DEFAULT_GATEWAY_URL = "ws://localhost:18789";
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
const isRecord = (value) => Boolean(value && typeof value === "object");
const isLocalGatewayUrl = (value) => {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed) return false;
try {
const parsed = new URL(trimmed);
return LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase());
} catch {
return false;
}
};
const readOpenclawGatewayDefaults = (env = process.env) => {
try {
const stateDir = resolveStateDir(env);
const configPath = path.join(stateDir, OPENCLAW_CONFIG_FILENAME);
const parsed = readJsonFile(configPath);
if (!isRecord(parsed)) return null;
const gateway = isRecord(parsed.gateway) ? parsed.gateway : null;
if (!gateway) return null;
const auth = isRecord(gateway.auth) ? gateway.auth : null;
const token = typeof auth?.token === "string" ? auth.token.trim() : "";
const port =
typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null;
if (!token) return null;
const url = port ? `ws://localhost:${port}` : "";
if (!url) return null;
return { url, token };
} catch {
return null;
}
};
const loadUpstreamGatewaySettings = (env = process.env) => {
const settingsPath = resolveStudioSettingsPath(env);
const parsed = readJsonFile(settingsPath);
const gateway = parsed && typeof parsed === "object" ? parsed.gateway : null;
const url = typeof gateway?.url === "string" ? gateway.url.trim() : "";
const token = typeof gateway?.token === "string" ? gateway.token.trim() : "";
if (!token && (!url || isLocalGatewayUrl(url))) {
const defaults = readOpenclawGatewayDefaults(env);
if (defaults) {
return {
url: url || defaults.url,
token: defaults.token,
settingsPath,
};
}
}
return {
url: url || DEFAULT_GATEWAY_URL,
token,
settingsPath,
};
};
module.exports = {
resolveStateDir,
resolveStudioSettingsPath,
loadUpstreamGatewaySettings,
};