From 528a5d6130c923b1184a824092a26f7fd17fa8ca Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Tue, 29 Sep 2026 22:12:12 +0200 Subject: [PATCH] slack: treat unknown scopes as missing, read integration data defensively Co-Authored-By: Claude Opus 5.5 --- apps/server/slack-manifest.json | 1 + apps/server/src/routes/slack/oauth.test.ts | 3 ++- .../src/routes/slack/workspace-resolver.ts | 18 +++++++++++++++--- packages/api/src/router/integration.ts | 6 ++++-- .../integration/__tests__/slack-scopes.test.ts | 1 + .../services/src/integration/slack-scopes.ts | 1 + 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/server/slack-manifest.json b/apps/server/slack-manifest.json index 895a25059..26a4e4a71 100644 --- a/apps/server/slack-manifest.json +++ b/apps/server/slack-manifest.json @@ -57,6 +57,7 @@ "channels:history", "channels:join", "channels:manage", + "channels:read", "channels:write.invites", "chat:write", "commands", diff --git a/apps/server/src/routes/slack/oauth.test.ts b/apps/server/src/routes/slack/oauth.test.ts index b62703df5..8f718f5d8 100644 --- a/apps/server/src/routes/slack/oauth.test.ts +++ b/apps/server/src/routes/slack/oauth.test.ts @@ -49,7 +49,8 @@ describe("slack manifest", () => { ); }); - test("subscribes to the events the handler acts on", () => { + // Subscribed ahead of their handlers so workspaces reconnect only once. + test("subscribes to every event the incident stack needs", () => { for (const event of [ "app_uninstalled", "tokens_revoked", diff --git a/apps/server/src/routes/slack/workspace-resolver.ts b/apps/server/src/routes/slack/workspace-resolver.ts index a9006e028..09ed08965 100644 --- a/apps/server/src/routes/slack/workspace-resolver.ts +++ b/apps/server/src/routes/slack/workspace-resolver.ts @@ -1,4 +1,4 @@ -import { and, db, desc, eq } from "@openstatus/db"; +import { and, db, desc, eq, sql } from "@openstatus/db"; import { integration, selectWorkspaceSchema, @@ -17,6 +17,17 @@ export interface SlackWorkspace { scopes?: string; } +function parseScopes(raw: string | null): string { + try { + return ( + integrationDataSchema.safeParse(JSON.parse(raw ?? "{}")).data?.scopes ?? + "" + ); + } catch { + return ""; + } +} + interface IntegrationCredential { botToken: string; botUserId: string; @@ -29,7 +40,8 @@ export async function resolveWorkspace( .select({ workspaceId: integration.workspaceId, credential: integration.credential, - data: integration.data, + // Raw text: the JSON-mode column throws on a malformed legacy row. + rawData: sql`${integration.data}`, }) .from(integration) .where( @@ -64,6 +76,6 @@ export async function resolveWorkspace( workspace: parsed.data, botToken: credential.botToken, botUserId: credential.botUserId ?? "", - scopes: integrationDataSchema.safeParse(row.data).data?.scopes ?? "", + scopes: parseScopes(row.rawData), }; } diff --git a/packages/api/src/router/integration.ts b/packages/api/src/router/integration.ts index 11475bd1c..dfbb226f1 100644 --- a/packages/api/src/router/integration.ts +++ b/packages/api/src/router/integration.ts @@ -36,8 +36,10 @@ export const integrationRouter = createTRPCRouter({ return integrations.map((i) => ({ ...i, missingScopes: - i.name === "slack-agent" && typeof i.data.scopes === "string" - ? missingSlackScopes(i.data.scopes) + i.name === "slack-agent" + ? missingSlackScopes( + typeof i.data.scopes === "string" ? i.data.scopes : undefined, + ) : [], })); } catch (err) { diff --git a/packages/services/src/integration/__tests__/slack-scopes.test.ts b/packages/services/src/integration/__tests__/slack-scopes.test.ts index 1ac6279d3..df658546d 100644 --- a/packages/services/src/integration/__tests__/slack-scopes.test.ts +++ b/packages/services/src/integration/__tests__/slack-scopes.test.ts @@ -13,6 +13,7 @@ describe("missingSlackScopes", () => { "app_mentions:read,assistant:write,channels:history,channels:join,chat:write,commands,groups:history,groups:read,groups:write,im:history,users:read,users:read.email"; expect(missingSlackScopes(old)).toEqual([ "channels:manage", + "channels:read", "channels:write.invites", "pins:write", "reactions:read", diff --git a/packages/services/src/integration/slack-scopes.ts b/packages/services/src/integration/slack-scopes.ts index c5ae16572..aa647cf91 100644 --- a/packages/services/src/integration/slack-scopes.ts +++ b/packages/services/src/integration/slack-scopes.ts @@ -5,6 +5,7 @@ export const SLACK_BOT_SCOPES = [ "channels:history", "channels:join", "channels:manage", + "channels:read", "channels:write.invites", "chat:write", "commands", -- 2.51.2