From 85647949a484957ba6bac00e47653b0acd4a92d7 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Mon, 30 Mar 2026 16:17:17 +0200 Subject: [PATCH] tighten phone-control scope helper extraction --- extensions/phone-control/index.test.ts | 3 ++- extensions/phone-control/index.ts | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/extensions/phone-control/index.test.ts b/extensions/phone-control/index.test.ts index aeab80ee9df..1d52961228f 100644 --- a/extensions/phone-control/index.test.ts +++ b/extensions/phone-control/index.test.ts @@ -167,7 +167,7 @@ describe("phone-control plugin", () => { }); }); - it("blocks non-webchat gateway callers with operator.write from mutating phone control", async () => { + it("regression: blocks non-webchat gateway callers with operator.write from arm/disarm", async () => { await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => { const armRes = await command.handler({ ...createCommandContext("arm writes 30s"), @@ -183,6 +183,7 @@ describe("phone-control plugin", () => { gatewayClientScopes: ["operator.write"], }); expect(String(disarmRes?.text ?? "")).toContain("requires operator.admin"); + expect(writeConfigFile).not.toHaveBeenCalled(); }); }); diff --git a/extensions/phone-control/index.ts b/extensions/phone-control/index.ts index 62386aee367..fcf94c6ba97 100644 --- a/extensions/phone-control/index.ts +++ b/extensions/phone-control/index.ts @@ -29,6 +29,7 @@ type ArmStateFile = ArmStateFileV1 | ArmStateFileV2; const STATE_VERSION = 2; const STATE_REL_PATH = ["plugins", "phone-control", "armed.json"] as const; +const PHONE_ADMIN_SCOPE = "operator.admin"; const GROUP_COMMANDS: Record, string[]> = { camera: ["camera.snap", "camera.clip"], @@ -268,6 +269,16 @@ function parseGroup(raw: string | undefined): ArmGroup | null { return null; } +function requiresAdminToMutatePhoneControl( + channel: string, + gatewayClientScopes?: readonly string[], +): boolean { + if (Array.isArray(gatewayClientScopes)) { + return !gatewayClientScopes.includes(PHONE_ADMIN_SCOPE); + } + return channel === "webchat"; +} + function formatStatus(state: ArmStateFile | null): string { if (!state) { return "Phone control: disarmed."; @@ -358,10 +369,7 @@ export default definePluginEntry({ } if (action === "disarm") { - if ( - (ctx.channel === "webchat" || Array.isArray(ctx.gatewayClientScopes)) && - !ctx.gatewayClientScopes?.includes("operator.admin") - ) { + if (requiresAdminToMutatePhoneControl(ctx.channel, ctx.gatewayClientScopes)) { return { text: "⚠️ /phone disarm requires operator.admin.", }; @@ -383,10 +391,7 @@ export default definePluginEntry({ } if (action === "arm") { - if ( - (ctx.channel === "webchat" || Array.isArray(ctx.gatewayClientScopes)) && - !ctx.gatewayClientScopes?.includes("operator.admin") - ) { + if (requiresAdminToMutatePhoneControl(ctx.channel, ctx.gatewayClientScopes)) { return { text: "⚠️ /phone arm requires operator.admin.", };