Skip to content

Commit c06da43

Browse files
feat(slack): gate the approval-pending scopes behind an opt-in env flag
1 parent da24b62 commit c06da43

4 files changed

Lines changed: 39 additions & 6 deletions

File tree

apps/sim/lib/core/config/env-flags.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENA
144144
export const isAppConfigEnabled =
145145
isHosted && Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT)
146146

147+
/**
148+
* Whether the deployment's Slack app is approved for `app_mentions:read`,
149+
* `assistant:write`, and `im:history` — the scopes backing the native Sim app
150+
* trigger's mention, assistant-thread, and DM events.
151+
*
152+
* Off by default because Slack rejects the ENTIRE authorization when it requests
153+
* a scope the app is not approved for, breaking every Slack connect. Sim Cloud's
154+
* app is directory-listed and pinned to its review-approved set, so this stays
155+
* off there until review lands; a self-hosted deployment pointing at its own
156+
* (unlisted) Slack app can opt in.
157+
*
158+
* Server code reads `SLACK_EXTENDED_SCOPES`. Server-only vars never reach browser
159+
* bundles, so client evaluation reads the `NEXT_PUBLIC_SLACK_EXTENDED_SCOPES`
160+
* twin (see {@link isBillingEnabled}) — deployments must set both together. The
161+
* server value decides the grant; the client value only decides what the credential
162+
* pickers advertise and treat as missing.
163+
*/
164+
export const isSlackExtendedScopesEnabled =
165+
typeof window === 'undefined'
166+
? isTruthy(env.SLACK_EXTENDED_SCOPES)
167+
: isTruthy(getEnv('NEXT_PUBLIC_SLACK_EXTENDED_SCOPES'))
168+
147169
/**
148170
* Is Trigger.dev enabled for async job processing
149171
*/

apps/sim/lib/core/config/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ export const env = createEnv({
421421
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
422422
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
423423
SLACK_SIGNING_SECRET: z.string().optional(), // Official Sim Slack app signing secret (verifies inbound events for the native OAuth trigger)
424+
SLACK_EXTENDED_SCOPES: z.boolean().optional(), // Request app_mentions:read, assistant:write, im:history — only where the Slack app is approved for them
424425
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
425426
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
426427
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
@@ -571,6 +572,7 @@ export const env = createEnv({
571572
// Feature Flags
572573
NEXT_PUBLIC_SSO_ENABLED: z.boolean().optional(), // Enable SSO login UI components
573574
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted
575+
NEXT_PUBLIC_SLACK_EXTENDED_SCOPES: z.boolean().optional(), // Client twin of SLACK_EXTENDED_SCOPES — set both together
574576
NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: z.boolean().optional(), // Enable custom blocks (deploy-as-block) settings on self-hosted
575577
NEXT_PUBLIC_WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements)
576578
NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements)
@@ -612,6 +614,7 @@ export const env = createEnv({
612614
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR,
613615
NEXT_PUBLIC_SSO_ENABLED: process.env.NEXT_PUBLIC_SSO_ENABLED,
614616
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED,
617+
NEXT_PUBLIC_SLACK_EXTENDED_SCOPES: process.env.NEXT_PUBLIC_SLACK_EXTENDED_SCOPES,
615618
NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: process.env.NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED,
616619
NEXT_PUBLIC_WHITELABELING_ENABLED: process.env.NEXT_PUBLIC_WHITELABELING_ENABLED,
617620
NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED,

apps/sim/lib/oauth/oauth.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
ZoomIcon,
5858
} from '@/components/icons'
5959
import { env } from '@/lib/core/config/env'
60+
import { isSlackExtendedScopesEnabled } from '@/lib/core/config/env-flags'
6061
import {
6162
DEFAULT_MAX_ERROR_BODY_BYTES,
6263
readResponseTextWithLimit,
@@ -66,6 +67,16 @@ import type { OAuthProviderConfig } from './types'
6667

6768
const logger = createLogger('OAuth')
6869

70+
/**
71+
* Slack scopes requested only where the app is approved for them, gated by
72+
* {@link isSlackExtendedScopesEnabled}. Slack rejects the entire authorization
73+
* with "unapproved permissions requested" when any requested scope is not on the
74+
* app's approved list, so these stay out of the default grant.
75+
*/
76+
const SLACK_APPROVAL_GATED_SCOPES = isSlackExtendedScopesEnabled
77+
? (['assistant:write', 'app_mentions:read', 'im:history'] as const)
78+
: ([] as const)
79+
6980
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
7081
'claude-platform': {
7182
name: 'Claude Platform',
@@ -782,12 +793,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
782793
'groups:write',
783794
'chat:write',
784795
'chat:write.public',
785-
// TODO: Re-add once Slack app review approves these. Requesting a scope
786-
// the app is not yet approved for makes Slack reject the entire
787-
// authorization with "unapproved permissions requested", breaking connect.
788-
// 'assistant:write',
789-
// 'app_mentions:read',
790-
// 'im:history',
796+
...SLACK_APPROVAL_GATED_SCOPES,
791797
'im:write',
792798
'im:read',
793799
'users:read',

packages/testing/src/mocks/env-flags.mock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface EnvFlagsMockState {
2121
isEmailPasswordEnabled: boolean
2222
isSignupMxValidationEnabled: boolean
2323
isAppConfigEnabled: boolean
24+
isSlackExtendedScopesEnabled: boolean
2425
isTriggerDevEnabled: boolean
2526
isSsoEnabled: boolean
2627
isAccessControlEnabled: boolean
@@ -61,6 +62,7 @@ const defaultEnvFlagsState: EnvFlagsMockState = {
6162
isEmailPasswordEnabled: true,
6263
isSignupMxValidationEnabled: false,
6364
isAppConfigEnabled: false,
65+
isSlackExtendedScopesEnabled: false,
6466
isTriggerDevEnabled: false,
6567
isSsoEnabled: false,
6668
isAccessControlEnabled: false,

0 commit comments

Comments
 (0)