import { ErrorCodes, errorShape } from "./protocol/index.js"; import { agentHandlers } from "./server-methods/agent.js"; import { agentsHandlers } from "./server-methods/agents.js"; import { channelsHandlers } from "./server-methods/channels.js"; import { chatHandlers } from "./server-methods/chat.js"; import { configHandlers } from "./server-methods/config.js"; import { connectHandlers } from "./server-methods/connect.js"; import { cronHandlers } from "./server-methods/cron.js"; import { deviceHandlers } from "./server-methods/devices.js"; import { execApprovalsHandlers } from "./server-methods/exec-approvals.js"; import { healthHandlers } from "./server-methods/health.js"; import { logsHandlers } from "./server-methods/logs.js"; import { modelsHandlers } from "./server-methods/models.js"; import { nodeHandlers } from "./server-methods/nodes.js"; import { sendHandlers } from "./server-methods/send.js"; import { sessionsHandlers } from "./server-methods/sessions.js"; import { skillsHandlers } from "./server-methods/skills.js"; import { systemHandlers } from "./server-methods/system.js"; import { talkHandlers } from "./server-methods/talk.js"; import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js"; import { updateHandlers } from "./server-methods/update.js"; import { usageHandlers } from "./server-methods/usage.js"; import { voicewakeHandlers } from "./server-methods/voicewake.js"; import { webHandlers } from "./server-methods/web.js"; import { wizardHandlers } from "./server-methods/wizard.js"; const ADMIN_SCOPE = "operator.admin"; const READ_SCOPE = "operator.read"; const WRITE_SCOPE = "operator.write"; const APPROVALS_SCOPE = "operator.approvals"; const PAIRING_SCOPE = "operator.pairing"; const APPROVAL_METHODS = new Set(["exec.approval.request", "exec.approval.resolve"]); const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]); const PAIRING_METHODS = new Set([ "node.pair.request", "node.pair.list", "node.pair.approve", "node.pair.reject", "node.pair.verify", "device.pair.list", "device.pair.approve", "device.pair.reject", "device.token.rotate", "device.token.revoke", "node.rename", ]); const ADMIN_METHOD_PREFIXES = ["exec.approvals."]; const READ_METHODS = new Set([ "health", "logs.tail", "channels.status", "status", "usage.status", "usage.cost", "models.list", "agents.list", "skills.status", "voicewake.get", "sessions.list", "cron.list", "cron.status", "cron.runs", "system-presence", "last-heartbeat", "node.list", "node.describe", "chat.history", ]); const WRITE_METHODS = new Set([ "send", "agent", "agent.wait", "wake", "talk.mode", "voicewake.set", "node.invoke", "chat.send", "chat.abort", ]); function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) { if (!client?.connect) return null; const role = client.connect.role ?? "operator"; const scopes = client.connect.scopes ?? []; if (NODE_ROLE_METHODS.has(method)) { if (role === "node") return null; return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`); } if (role === "node") { return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`); } if (role !== "operator") { return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`); } if (scopes.includes(ADMIN_SCOPE)) return null; if (APPROVAL_METHODS.has(method) && !scopes.includes(APPROVALS_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.approvals"); } if (PAIRING_METHODS.has(method) && !scopes.includes(PAIRING_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.pairing"); } if (READ_METHODS.has(method) && !(scopes.includes(READ_SCOPE) || scopes.includes(WRITE_SCOPE))) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.read"); } if (WRITE_METHODS.has(method) && !scopes.includes(WRITE_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.write"); } if (APPROVAL_METHODS.has(method)) return null; if (PAIRING_METHODS.has(method)) return null; if (READ_METHODS.has(method)) return null; if (WRITE_METHODS.has(method)) return null; if (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin"); } if ( method.startsWith("config.") || method.startsWith("wizard.") || method.startsWith("update.") || method === "channels.logout" || method === "skills.install" || method === "skills.update" || method === "cron.add" || method === "cron.update" || method === "cron.remove" || method === "cron.run" || method === "sessions.patch" || method === "sessions.reset" || method === "sessions.delete" || method === "sessions.compact" ) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin"); } return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin"); } export const coreGatewayHandlers: GatewayRequestHandlers = { ...connectHandlers, ...logsHandlers, ...voicewakeHandlers, ...healthHandlers, ...channelsHandlers, ...chatHandlers, ...cronHandlers, ...deviceHandlers, ...execApprovalsHandlers, ...webHandlers, ...modelsHandlers, ...configHandlers, ...wizardHandlers, ...talkHandlers, ...skillsHandlers, ...sessionsHandlers, ...systemHandlers, ...updateHandlers, ...nodeHandlers, ...sendHandlers, ...usageHandlers, ...agentHandlers, ...agentsHandlers, }; export async function handleGatewayRequest( opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers }, ): Promise { const { req, respond, client, isWebchatConnect, context } = opts; const authError = authorizeGatewayMethod(req.method, client); if (authError) { respond(false, undefined, authError); return; } const handler = opts.extraHandlers?.[req.method] ?? coreGatewayHandlers[req.method]; if (!handler) { respond( false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `unknown method: ${req.method}`), ); return; } await handler({ req, params: (req.params ?? {}) as Record, client, isWebchatConnect, respond, context, }); }