UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

300 lines (299 loc) 11.3 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { t as asNullableObjectRecord } from "./record-coerce-DHZ4bFlT.js"; import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js"; import "./theme-vjDs9tao.js"; import "./runtime-B4lgFmsS.js"; import { r as logVerbose } from "./globals-GTrXU4s9.js"; import "./prompt-DN3iQ5gF.js"; import { i as runExec } from "./exec-DubsSJS2.js"; import "./binaries-B6QIViwo.js"; import { existsSync } from "node:fs"; //#region src/infra/tailscale.ts function parsePossiblyNoisyJsonObject(stdout) { const trimmed = stdout.trim(); const start = trimmed.indexOf("{"); const end = trimmed.lastIndexOf("}"); if (start >= 0 && end > start) return JSON.parse(trimmed.slice(start, end + 1)); return JSON.parse(trimmed); } /** * Locate Tailscale binary using multiple strategies: * 1. PATH lookup (via which command) * 2. Known macOS app path * 3. find /Applications for Tailscale.app * 4. locate database (if available) * * @returns Path to Tailscale binary or null if not found */ async function findTailscaleBinary() { const checkBinary = async (path) => { if (!path || !existsSync(path)) return false; try { await Promise.race([runExec(path, ["--version"], { timeoutMs: 3e3 }), new Promise((_, reject) => { setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), 3e3); })]); return true; } catch { return false; } }; try { const { stdout } = await runExec("which", ["tailscale"]); const fromPath = stdout.trim(); if (fromPath && await checkBinary(fromPath)) return fromPath; } catch {} const macAppPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"; if (await checkBinary(macAppPath)) return macAppPath; try { const { stdout } = await runExec("find", [ "/Applications", "-maxdepth", "3", "-name", "Tailscale", "-path", "*/Tailscale.app/Contents/MacOS/Tailscale" ], { timeoutMs: 5e3 }); const found = stdout.trim().split("\n")[0]; if (found && await checkBinary(found)) return found; } catch {} try { const { stdout } = await runExec("locate", ["Tailscale.app"]); const candidates = stdout.trim().split("\n").filter((line) => line.includes("/Tailscale.app/Contents/MacOS/Tailscale")); for (const candidate of candidates) if (await checkBinary(candidate)) return candidate; } catch {} return null; } async function getTailnetHostname(exec = runExec, detectedBinary) { const candidates = detectedBinary ? [detectedBinary] : ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"]; let lastError; for (const candidate of candidates) { if (candidate.startsWith("/") && !existsSync(candidate)) continue; try { const { stdout } = await exec(candidate, ["status", "--json"], { timeoutMs: 5e3, maxBuffer: 4e5 }); const parsed = stdout ? parsePossiblyNoisyJsonObject(stdout) : {}; const self = typeof parsed.Self === "object" && parsed.Self !== null ? parsed.Self : void 0; const dns = typeof self?.DNSName === "string" ? self.DNSName : void 0; const ips = Array.isArray(self?.TailscaleIPs) ? parsed.Self.TailscaleIPs ?? [] : []; if (dns && dns.length > 0) return dns.replace(/\.$/, ""); if (ips.length > 0) return ips[0]; throw new Error("Could not determine Tailscale DNS or IP"); } catch (err) { lastError = err; } } throw toLintErrorObject(lastError ?? /* @__PURE__ */ new Error("Could not determine Tailscale DNS or IP"), "Non-Error thrown"); } /** * Get the Tailscale binary command to use. * Returns a cached detected binary or the default "tailscale" command. */ let cachedTailscaleBinary = null; function getTestTailscaleBinaryOverride(env = process.env) { const forcedBinary = env.OPENCLAW_TEST_TAILSCALE_BINARY?.trim(); if (!forcedBinary) return null; if (env.VITEST || env.NODE_ENV === "test") return forcedBinary; return null; } async function getTailscaleBinary() { const forcedBinary = getTestTailscaleBinaryOverride(); if (forcedBinary) { cachedTailscaleBinary = forcedBinary; return forcedBinary; } if (cachedTailscaleBinary) return cachedTailscaleBinary; cachedTailscaleBinary = await findTailscaleBinary(); return cachedTailscaleBinary ?? "tailscale"; } const whoisCache = /* @__PURE__ */ new Map(); function extractExecErrorText(err) { const errOutput = err; return { stdout: typeof errOutput.stdout === "string" ? errOutput.stdout : "", stderr: typeof errOutput.stderr === "string" ? errOutput.stderr : "", message: typeof errOutput.message === "string" ? errOutput.message : "", code: typeof errOutput.code === "string" ? errOutput.code : "" }; } function isPermissionDeniedError(err) { const { stdout, stderr, message, code } = extractExecErrorText(err); if (code.toUpperCase() === "EACCES") return true; const combined = normalizeLowercaseStringOrEmpty(`${stdout}\n${stderr}\n${message}`); return combined.includes("permission denied") || combined.includes("access denied") || combined.includes("operation not permitted") || combined.includes("not permitted") || combined.includes("requires root") || combined.includes("must be run as root") || combined.includes("must be run with sudo") || combined.includes("requires sudo") || combined.includes("need sudo"); } async function execWithSudoFallback(exec, bin, args, opts) { try { return await exec(bin, args, opts); } catch (err) { if (!isPermissionDeniedError(err)) throw err; logVerbose(`Command failed, retrying with sudo: ${bin} ${args.join(" ")}`); try { return await exec("sudo", [ "-n", bin, ...args ], opts); } catch (sudoErr) { const { stderr, message } = extractExecErrorText(sudoErr); const detail = (stderr || message).trim(); if (detail) logVerbose(`Sudo retry failed: ${detail}`); throw err; } } } async function enableTailscaleServe(port, exec = runExec, serviceName) { await execWithSudoFallback(exec, await getTailscaleBinary(), [ "serve", ...serviceName ? [`--service=${serviceName}`] : [], "--bg", "--yes", `${port}` ], { maxBuffer: 2e5, timeoutMs: 15e3 }); } async function hasTailscaleFunnelRouteForPort(port, exec = runExec) { try { const { stdout } = await exec(await getTailscaleBinary(), [ "funnel", "status", "--json" ], { maxBuffer: 2e5, timeoutMs: 5e3 }); return tailscaleFunnelStatusCoversPort(stdout ? parsePossiblyNoisyJsonObject(stdout) : {}, port); } catch { return false; } } const TAILSCALE_LOOPBACK_PROXY_HOSTS = new Set([ "127.0.0.1", "localhost", "[::1]", "::1" ]); function tailscaleFunnelStatusCoversPort(status, port) { for (const proxy of funnelStatusBackendsForPort(status)) if (tailscaleProxyMatchesLoopbackPort(proxy, port)) return true; return false; } function tailscaleProxyMatchesLoopbackPort(proxy, port) { const stripped = proxy.replace(/^[a-z][a-z0-9+\-.]*:\/\//i, "").replace(/\/.*$/, ""); if (stripped === String(port)) return true; const sep = stripped.lastIndexOf(":"); if (sep < 0) return false; const host = stripped.slice(0, sep); if (stripped.slice(sep + 1) !== String(port)) return false; return TAILSCALE_LOOPBACK_PROXY_HOSTS.has(host); } function funnelStatusBackendsForPort(status) { const backends = /* @__PURE__ */ new Set(); const allowFunnel = status.AllowFunnel ?? {}; const enabledHosts = new Set(Object.entries(allowFunnel).filter(([, value]) => value === true).map(([host]) => host)); if (enabledHosts.size === 0) return backends; const web = status.Web; if (!web || typeof web !== "object") return backends; for (const [host, handlers] of Object.entries(web)) { if (!enabledHosts.has(host)) continue; if (!handlers || typeof handlers !== "object") continue; const handlerEntries = handlers.Handlers; if (!handlerEntries || typeof handlerEntries !== "object") continue; for (const handler of Object.values(handlerEntries)) { const proxy = handler?.Proxy; if (typeof proxy === "string" && proxy.length > 0) backends.add(proxy); } } return backends; } async function disableTailscaleServe(exec = runExec, serviceName) { await execWithSudoFallback(exec, await getTailscaleBinary(), serviceName ? [ "serve", "clear", serviceName ] : ["serve", "reset"], { maxBuffer: 2e5, timeoutMs: 15e3 }); } async function enableTailscaleFunnel(port, exec = runExec) { await execWithSudoFallback(exec, await getTailscaleBinary(), [ "funnel", "--bg", "--yes", `${port}` ], { maxBuffer: 2e5, timeoutMs: 15e3 }); } async function disableTailscaleFunnel(exec = runExec) { await execWithSudoFallback(exec, await getTailscaleBinary(), ["funnel", "reset"], { maxBuffer: 2e5, timeoutMs: 15e3 }); } function parseWhoisIdentity(payload) { const userProfile = asNullableObjectRecord(payload.UserProfile) ?? asNullableObjectRecord(payload.userProfile) ?? asNullableObjectRecord(payload.User); const login = normalizeOptionalString(userProfile?.LoginName) ?? normalizeOptionalString(userProfile?.Login) ?? normalizeOptionalString(userProfile?.login) ?? normalizeOptionalString(payload.LoginName) ?? normalizeOptionalString(payload.login); if (!login) return null; return { login, name: normalizeOptionalString(userProfile?.DisplayName) ?? normalizeOptionalString(userProfile?.Name) ?? normalizeOptionalString(userProfile?.displayName) ?? normalizeOptionalString(payload.DisplayName) ?? normalizeOptionalString(payload.name) }; } function readCachedWhois(ip, now) { const validNow = asDateTimestampMs(now); if (validNow === void 0) return; const cached = whoisCache.get(ip); if (!cached) return; const expiresAt = asDateTimestampMs(cached.expiresAt); if (expiresAt === void 0 || expiresAt <= validNow) { whoisCache.delete(ip); return; } return cached.value; } function writeCachedWhois(ip, value, ttlMs) { const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs); if (expiresAt !== void 0) whoisCache.set(ip, { value, expiresAt }); } async function readTailscaleWhoisIdentity(ip, exec = runExec, opts) { const normalized = ip.trim(); if (!normalized) return null; const cached = readCachedWhois(normalized, Date.now()); if (cached !== void 0) return cached; const cacheTtlMs = opts?.cacheTtlMs ?? 6e4; const errorTtlMs = opts?.errorTtlMs ?? 5e3; try { const { stdout } = await exec(await getTailscaleBinary(), [ "whois", "--json", normalized ], { timeoutMs: opts?.timeoutMs ?? 5e3, maxBuffer: 2e5 }); const identity = parseWhoisIdentity(stdout ? parsePossiblyNoisyJsonObject(stdout) : {}); writeCachedWhois(normalized, identity, cacheTtlMs); return identity; } catch { writeCachedWhois(normalized, null, errorTtlMs); return null; } } 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 export { findTailscaleBinary as a, readTailscaleWhoisIdentity as c, enableTailscaleServe as i, disableTailscaleServe as n, getTailnetHostname as o, enableTailscaleFunnel as r, hasTailscaleFunnelRouteForPort as s, disableTailscaleFunnel as t };