From 6da1cd7c0b67f8578504cc1e8d8c6d170f4689fa Mon Sep 17 00:00:00 2001 From: Matt Rubens <2600+mrubens@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:50:02 -0400 Subject: [PATCH] Reuse Slack manifest scopes for OAuth installs --- .../src/lib/slack-install-url.client.test.ts | 26 +++++++++++++++++ apps/web/src/lib/slack-install-url.ts | 13 +++++++++ apps/web/src/trpc/commands/slack/index.ts | 28 ++++--------------- 3 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 apps/web/src/lib/slack-install-url.client.test.ts create mode 100644 apps/web/src/lib/slack-install-url.ts diff --git a/apps/web/src/lib/slack-install-url.client.test.ts b/apps/web/src/lib/slack-install-url.client.test.ts new file mode 100644 index 000000000..7ea1ebd03 --- /dev/null +++ b/apps/web/src/lib/slack-install-url.client.test.ts @@ -0,0 +1,26 @@ +import { SLACK_MANIFEST_BOT_SCOPES } from './slack-app-manifest'; +import { buildSlackInstallUrl } from './slack-install-url'; + +describe('Slack install URL builder', () => { + it('requests the same bot scopes as the generated app manifest', () => { + const url = new URL( + buildSlackInstallUrl({ + clientId: 'client-id', + state: 'signed-state', + redirectUri: 'https://roomote.example.com/api/slack/callback', + }), + ); + + expect(url.searchParams.get('scope')?.split(',')).toEqual([ + ...SLACK_MANIFEST_BOT_SCOPES, + ]); + expect(url.searchParams.get('scope')?.split(',')).toContain( + 'assistant:write', + ); + expect(url.searchParams.get('client_id')).toBe('client-id'); + expect(url.searchParams.get('state')).toBe('signed-state'); + expect(url.searchParams.get('redirect_uri')).toBe( + 'https://roomote.example.com/api/slack/callback', + ); + }); +}); diff --git a/apps/web/src/lib/slack-install-url.ts b/apps/web/src/lib/slack-install-url.ts new file mode 100644 index 000000000..c5bc3ac5c --- /dev/null +++ b/apps/web/src/lib/slack-install-url.ts @@ -0,0 +1,13 @@ +import { SLACK_MANIFEST_BOT_SCOPES } from './slack-app-manifest'; + +export function buildSlackInstallUrl({ + clientId, + state, + redirectUri, +}: { + clientId: string; + state: string; + redirectUri: string; +}) { + return `https://slack.com/oauth/v2/authorize?client_id=${clientId}&scope=${SLACK_MANIFEST_BOT_SCOPES.join(',')}&state=${state}&redirect_uri=${encodeURIComponent(redirectUri)}`; +} diff --git a/apps/web/src/trpc/commands/slack/index.ts b/apps/web/src/trpc/commands/slack/index.ts index e4c9a62cb..19f2dc3dc 100644 --- a/apps/web/src/trpc/commands/slack/index.ts +++ b/apps/web/src/trpc/commands/slack/index.ts @@ -28,6 +28,7 @@ import type { UserAuthSuccess } from '@/types'; import { bootstrapWebRuntimeEnv } from '@/lib/server/bootstrap-runtime-env'; import { getSlackRedirectUri } from '@/lib/server/slack-redirect-uri'; import { syncUser } from '@/lib/server/sync-internal'; +import { buildSlackInstallUrl } from '@/lib/slack-install-url'; import { createSignedSlackInstallState, createSignedSlackLinkAccountState, @@ -546,30 +547,13 @@ export async function connectSlackAppCommand( } const slackOAuthConfig = await resolveSlackOAuthConfig(); - const permissions = [ - 'app_mentions:read', - 'channels:read', - 'channels:history', - 'chat:write', - 'files:read', - 'groups:read', - 'groups:history', - 'im:read', - 'im:history', - 'im:write', - 'links:read', - 'links:write', - 'mpim:read', - 'mpim:history', - 'reactions:read', - 'reactions:write', - 'team:read', - 'users:read', - ]; - const redirectPath = input.redirectPath ?? '/settings'; const state = await createSignedSlackInstallState({ redirectPath }); - const url = `https://slack.com/oauth/v2/authorize?client_id=${slackOAuthConfig.clientId}&scope=${permissions.join(',')}&state=${state}&redirect_uri=${encodeURIComponent(getSlackRedirectUri())}`; + const url = buildSlackInstallUrl({ + clientId: slackOAuthConfig.clientId, + state, + redirectUri: getSlackRedirectUri(), + }); return { success: true, url }; } catch (error) {