UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

286 lines (285 loc) 7.76 kB
import { c as isRecord } from "../../record-coerce-DItp3I4t.js"; import { o as readJsonBodyWithLimit, p as sendHttpRequestRejection } from "../../http-body-D3IMwTJJ.js"; import "../../string-coerce-runtime-GQa0ehRA.js"; import { t as definePluginEntry } from "../../plugin-entry-zfBGJaNO.js"; import { t as dispatchGatewayMethod } from "../../gateway-method-runtime-CyoBzTJz.js"; import { t as WEBHOOK_BODY_READ_DEFAULTS } from "../../webhook-request-guards-BoKMzZgq.js"; import { randomUUID } from "node:crypto"; //#region extensions/admin-http-rpc/src/methods.ts const ADMIN_HTTP_RPC_ALLOWED_METHODS = new Set(Object.values({ gateway: [ "health", "status", "logs.tail", "usage.status", "usage.cost", "gateway.restart.request", "gateway.suspend.prepare", "gateway.suspend.status", "gateway.suspend.resume" ], discovery: ["commands.list"], config: [ "config.get", "config.schema", "config.schema.lookup", "config.set", "config.patch", "config.apply" ], channels: [ "channels.status", "channels.start", "channels.stop", "channels.logout" ], web: ["web.login.start", "web.login.wait"], models: ["models.list", "models.authStatus"], agents: [ "agents.list", "agents.create", "agents.update", "agents.delete" ], approvals: [ "exec.approvals.get", "exec.approvals.set", "exec.approvals.node.get", "exec.approvals.node.set" ], cron: [ "cron.status", "cron.list", "cron.get", "cron.runs", "cron.add", "cron.update", "cron.remove", "cron.run" ], devices: [ "device.pair.list", "device.pair.approve", "device.pair.reject", "device.pair.remove" ], nodes: [ "node.list", "node.describe", "node.pair.list", "node.pair.approve", "node.pair.reject", "node.pair.remove", "node.rename" ], tasks: [ "tasks.list", "tasks.get", "tasks.cancel" ], diagnostics: ["doctor.memory.status", "update.status"] }).flat()); /** Return whether an admin RPC method is exposed over HTTP. */ function isAdminHttpRpcAllowedMethod(method) { return ADMIN_HTTP_RPC_ALLOWED_METHODS.has(method); } /** List all admin RPC methods exposed over HTTP. */ function listAdminHttpRpcAllowedMethods() { return Array.from(ADMIN_HTTP_RPC_ALLOWED_METHODS); } //#endregion //#region extensions/admin-http-rpc/src/handler.ts /** * HTTP handler for the Admin RPC endpoint. It validates JSON requests, enforces * the method allowlist, dispatches gateway methods, and maps errors to HTTP. */ const ErrorCodes = { AGENT_TIMEOUT: "AGENT_TIMEOUT", APPROVAL_NOT_FOUND: "APPROVAL_NOT_FOUND", INVALID_REQUEST: "INVALID_REQUEST", NOT_LINKED: "NOT_LINKED", NOT_PAIRED: "NOT_PAIRED", UNAVAILABLE: "UNAVAILABLE" }; function createError(code, message) { return { code, message }; } function rpcHttpStatus(response) { if (response.ok) return 200; switch (response.error.code) { case ErrorCodes.INVALID_REQUEST: return 400; case ErrorCodes.APPROVAL_NOT_FOUND: return 404; case ErrorCodes.UNAVAILABLE: return 503; case ErrorCodes.AGENT_TIMEOUT: return 504; case ErrorCodes.NOT_LINKED: case ErrorCodes.NOT_PAIRED: return 409; default: return 500; } } function sendJson(res, status, body) { res.statusCode = status; res.setHeader("Cache-Control", "no-store"); res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(body)); } function sendError(res, status, error) { sendJson(res, status, { ok: false, error }); } function statusForBodyErrorCode(code) { switch (code) { case "PAYLOAD_TOO_LARGE": return 413; case "REQUEST_BODY_TIMEOUT": return 408; case "CONNECTION_CLOSED": return 400; } return 400; } async function readAdminJsonBody(req) { const body = await readJsonBodyWithLimit(req, { ...WEBHOOK_BODY_READ_DEFAULTS.postAuthResponseFirst, emptyObjectOnEmpty: false }); if (body.ok) return body; if (body.code === "INVALID_JSON") return { ok: false, status: 400, message: body.error === "empty payload" ? "request body must be JSON" : "request body must be valid JSON" }; return { ok: false, status: statusForBodyErrorCode(body.code), message: body.error, closeAfterResponse: body.code !== "CONNECTION_CLOSED" }; } function readRpcRequestBody(body) { if (!isRecord(body)) return { ok: false, message: "request body must be an object" }; const rpcBody = body; if (typeof rpcBody.method !== "string" || rpcBody.method.trim().length === 0) return { ok: false, message: "method must be a non-empty string" }; return { ok: true, request: { id: typeof rpcBody.id === "string" && rpcBody.id.trim().length > 0 ? rpcBody.id.trim() : randomUUID(), method: rpcBody.method.trim(), ...Object.hasOwn(rpcBody, "params") ? { params: rpcBody.params } : {} } }; } function methodNotAllowed(id, method) { return { id, ok: false, error: createError(ErrorCodes.INVALID_REQUEST, `admin HTTP RPC method is not supported: ${method}`) }; } function commandsList(id) { return { id, ok: true, payload: { methods: listAdminHttpRpcAllowedMethods() } }; } async function dispatchAdminRpc(request) { try { const response = await dispatchGatewayMethod(request.method, request.params); if (response.ok) return { id: request.id, ok: true, payload: response.payload, ...response.meta ? { meta: response.meta } : {} }; return { id: request.id, ok: false, error: response.error ?? createError(ErrorCodes.UNAVAILABLE, "gateway method failed before returning a response"), ...response.meta ? { meta: response.meta } : {} }; } catch { return { id: request.id, ok: false, error: createError(ErrorCodes.UNAVAILABLE, "gateway method failed before returning a response") }; } } /** Handle one gateway-authenticated Admin HTTP RPC request. */ async function handleAdminHttpRpcRequest(req, res) { if ((req.method ?? "GET").toUpperCase() !== "POST") { res.setHeader("Allow", "POST"); sendError(res, 405, { type: "method_not_allowed", message: "Method Not Allowed" }); return true; } const body = await readAdminJsonBody(req); if (!body.ok) { if (body.closeAfterResponse) { if (!res.headersSent) res.setHeader("Cache-Control", "no-store"); await sendHttpRequestRejection(req, res, body.status, JSON.stringify({ ok: false, error: { type: "invalid_request", message: body.message } }), "application/json; charset=utf-8"); } else sendError(res, body.status, { type: "invalid_request", message: body.message }); return true; } const parsed = readRpcRequestBody(body.value); if (!parsed.ok) { sendError(res, 400, { type: "invalid_request", message: parsed.message }); return true; } if (!isAdminHttpRpcAllowedMethod(parsed.request.method)) { const response = methodNotAllowed(parsed.request.id, parsed.request.method); sendJson(res, rpcHttpStatus(response), response); return true; } if (parsed.request.method === "commands.list") { sendJson(res, 200, commandsList(parsed.request.id)); return true; } const response = await dispatchAdminRpc(parsed.request); sendJson(res, rpcHttpStatus(response), response); return true; } //#endregion //#region extensions/admin-http-rpc/index.ts /** * Admin HTTP RPC plugin entry. It exposes a trusted gateway-authenticated HTTP * endpoint for the explicit admin method allowlist. */ var admin_http_rpc_default = definePluginEntry({ id: "admin-http-rpc", name: "Admin HTTP RPC", description: "Expose selected Gateway admin RPC methods over HTTP", register(api) { api.registerHttpRoute({ path: "/api/v1/admin/rpc", auth: "gateway", match: "exact", gatewayRuntimeScopeSurface: "trusted-operator", handler: handleAdminHttpRpcRequest }); } }); //#endregion export { admin_http_rpc_default as default };