diff --git a/apps/server/slack-manifest.json b/apps/server/slack-manifest.json index 895a2505..26a4e4a7 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 b62703df..8f718f5d 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 a9006e02..09ed0896 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 11475bd1..dfbb226f 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 1ac6279d..df658546 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 c5ae1657..aa647cf9 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",