feat: unify device auth + pairing

This commit is contained in:
Peter Steinberger
2026-01-19 02:31:18 +00:00
parent 47d1f23d55
commit 73e9e787b4
30 changed files with 2041 additions and 20 deletions

View File

@@ -6,6 +6,7 @@ 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";
@@ -23,6 +24,43 @@ 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 APPROVALS_SCOPE = "operator.approvals";
const PAIRING_SCOPE = "operator.pairing";
const APPROVAL_METHODS = new Set(["exec.approval.request", "exec.approval.resolve"]);
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",
]);
const ADMIN_METHOD_PREFIXES = ["exec.approvals."];
function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) {
if (!client?.connect) return null;
const role = client.connect.role ?? "operator";
const scopes = client.connect.scopes ?? [];
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 (ADMIN_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix))) {
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
}
return null;
}
export const coreGatewayHandlers: GatewayRequestHandlers = {
...connectHandlers,
...logsHandlers,
@@ -31,6 +69,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
...channelsHandlers,
...chatHandlers,
...cronHandlers,
...deviceHandlers,
...execApprovalsHandlers,
...webHandlers,
...modelsHandlers,
@@ -52,6 +91,11 @@ export async function handleGatewayRequest(
opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
): Promise<void> {
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(