import { formatControlPlaneActor, resolveControlPlaneActor } from "./control-plane-audit.js"; import { consumeControlPlaneWriteBudget } from "./control-plane-rate-limit.js"; import { ADMIN_SCOPE, APPROVALS_SCOPE, isAdminOnlyMethod, isApprovalMethod, isNodeRoleMethod, isPairingMethod, isReadMethod, isWriteMethod, PAIRING_SCOPE, READ_SCOPE, WRITE_SCOPE, } from "./method-scopes.js"; import { ErrorCodes, errorShape } from "./protocol/index.js"; import { agentHandlers } from "./server-methods/agent.js"; import { agentsHandlers } from "./server-methods/agents.js"; import { browserHandlers } from "./server-methods/browser.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 { pushHandlers } from "./server-methods/push.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 { ttsHandlers } from "./server-methods/tts.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 CONTROL_PLANE_WRITE_METHODS = new Set(["config.apply", "config.patch", "update.run"]); function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) { if (!client?.connect) { return null; } const role = client.connect.role ?? "operator"; const scopes = client.connect.scopes ?? []; if (isNodeRoleMethod(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 (isApprovalMethod(method) && !scopes.includes(APPROVALS_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.approvals"); } if (isPairingMethod(method) && !scopes.includes(PAIRING_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.pairing"); } if (isReadMethod(method) && !(scopes.includes(READ_SCOPE) || scopes.includes(WRITE_SCOPE))) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.read"); } if (isWriteMethod(method) && !scopes.includes(WRITE_SCOPE)) { return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.write"); } if (isApprovalMethod(method)) { return null; } if (isPairingMethod(method)) { return null; } if (isReadMethod(method)) { return null; } if (isWriteMethod(method)) { return null; } if (isAdminOnlyMethod(method)) { 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, ...ttsHandlers, ...skillsHandlers, ...sessionsHandlers, ...systemHandlers, ...updateHandlers, ...nodeHandlers, ...pushHandlers, ...sendHandlers, ...usageHandlers, ...agentHandlers, ...agentsHandlers, ...browserHandlers, }; 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; } if (CONTROL_PLANE_WRITE_METHODS.has(req.method)) { const budget = consumeControlPlaneWriteBudget({ client }); if (!budget.allowed) { const actor = resolveControlPlaneActor(client); context.logGateway.warn( `control-plane write rate-limited method=${req.method} ${formatControlPlaneActor(actor)} retryAfterMs=${budget.retryAfterMs} key=${budget.key}`, ); respond( false, undefined, errorShape( ErrorCodes.UNAVAILABLE, `rate limit exceeded for ${req.method}; retry after ${Math.ceil(budget.retryAfterMs / 1000)}s`, { retryable: true, retryAfterMs: budget.retryAfterMs, details: { method: req.method, limit: "3 per 60s", }, }, ), ); 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, }); }