Skip to content

Commit 080d9b8

Browse files
fix(slack-search): include full scope sets in custom manifests
1 parent f953cea commit 080d9b8

3 files changed

Lines changed: 79 additions & 80 deletions

File tree

apps/docs/content/docs/search/slack.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ New Search member connections request these read-only **User Token Scopes**. DM
119119

120120
The custom app's **Bot Token Scopes** include `channels:read` and `groups:read` for the channel picker. Private channels appear only when the bot has access. For an existing app, add these scopes under **OAuth & Permissions**, reinstall the app in Slack to approve the changes, then reconnect the bot in Sim.
121121

122+
Custom and official app manifests declare the same full bot and user scope sets, including permissions reserved for additional capabilities. The table below lists the scopes requested by member indexing; declaring additional user scopes in the manifest does not automatically grant them to each member connection.
123+
122124
| Purpose | User scopes |
123125
|---|---|
124126
| Public channels | `channels:read`, `channels:history` |

apps/sim/lib/slack-search/manifest.test.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ describe('Search app manifest', () => {
1919
'groups:read',
2020
])
2121
)
22-
expect(manifest.oauth_config.scopes.bot).not.toContain('groups:history')
2322
expect(manifest.oauth_config.scopes.user).toEqual(
2423
expect.arrayContaining([
2524
'users:read',
@@ -49,28 +48,21 @@ describe('Search app manifest', () => {
4948
).toBe(true)
5049
})
5150
it('preserves existing member grants when updating a bot manifest', () => {
52-
expect(
53-
createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test', ['files:read'])
54-
.oauth_config.scopes.user
55-
).toContain('files:read')
51+
const manifest = createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test', [
52+
'files:read',
53+
'files:write',
54+
])
55+
expect(manifest.oauth_config.scopes.user).toContain('files:write')
56+
expect(manifest.oauth_config.scopes.user.filter((scope) => scope === 'files:read')).toEqual([
57+
'files:read',
58+
])
5659
})
57-
it('uses one origin for unified ingress and OAuth with only the required bot permissions', () => {
60+
it('uses one origin for unified ingress and OAuth', () => {
5861
const manifest = createSlackSearchManifest(
5962
'Sim Search',
6063
'Search with sources',
6164
'https://search-test.ngrok.app'
6265
)
63-
expect(manifest.oauth_config.scopes.bot).toEqual([
64-
'assistant:write',
65-
'chat:write',
66-
'channels:read',
67-
'groups:read',
68-
'im:history',
69-
'im:write',
70-
'app_mentions:read',
71-
'users:read',
72-
'users:read.email',
73-
])
7466
expect(manifest.settings.event_subscriptions.request_url).toBe(
7567
'https://search-test.ngrok.app/api/webhooks/slack'
7668
)
@@ -96,23 +88,26 @@ describe('Search app manifest', () => {
9688
})
9789
})
9890

99-
it('official app declares expanded permissions without subscribing to member message events', () => {
100-
const manifest = createSharedSlackSearchManifest('https://www.sim.ai')
101-
expect(manifest.oauth_config.scopes.user).toEqual([
91+
it.each([
92+
{
93+
name: 'custom',
94+
manifest: createSlackSearchManifest('Sim Search', 'Search', 'https://sim.test'),
95+
},
96+
{ name: 'shared', manifest: createSharedSlackSearchManifest('https://sim.test') },
97+
])('$name app declares the complete bot and user scope sets without duplicates', ({ manifest }) => {
98+
expect([...manifest.oauth_config.scopes.user].sort()).toEqual([
99+
'canvases:read',
100+
'canvases:write',
102101
'channels:history',
103102
'channels:read',
103+
'chat:write',
104+
'files:read',
104105
'groups:history',
105106
'groups:read',
106107
'im:history',
107108
'im:read',
108109
'mpim:history',
109110
'mpim:read',
110-
'users:read',
111-
'users:read.email',
112-
'canvases:read',
113-
'canvases:write',
114-
'chat:write',
115-
'files:read',
116111
'search:read.files',
117112
'search:read.im',
118113
'search:read.mpim',
@@ -121,32 +116,38 @@ it('official app declares expanded permissions without subscribing to member mes
121116
'search:read.users',
122117
'team:read',
123118
'usergroups:read',
124-
])
125-
expect(manifest.oauth_config.scopes.bot).toEqual([
126-
'assistant:write',
127-
'chat:write',
128-
'channels:read',
129-
'groups:read',
130-
'im:history',
131-
'im:write',
132-
'app_mentions:read',
133119
'users:read',
134120
'users:read.email',
135-
'commands',
121+
])
122+
expect([...manifest.oauth_config.scopes.bot].sort()).toEqual([
123+
'app_mentions:read',
124+
'assistant:write',
136125
'channels:history',
137126
'channels:manage',
127+
'channels:read',
138128
'channels:write.invites',
129+
'chat:write',
139130
'chat:write.public',
131+
'commands',
140132
'groups:history',
133+
'groups:read',
141134
'groups:write',
142135
'groups:write.invites',
136+
'im:history',
137+
'im:write',
143138
'links:read',
144139
'links:write',
145140
'mpim:history',
146141
'mpim:read',
147142
'mpim:write',
148143
'reactions:write',
144+
'users:read',
145+
'users:read.email',
149146
])
147+
})
148+
149+
it('official app declares commands and lifecycle events without member message events', () => {
150+
const manifest = createSharedSlackSearchManifest('https://www.sim.ai')
150151
expect(manifest.features.slash_commands.map((command) => command.command)).toEqual([
151152
'/query',
152153
'/connect',

apps/sim/lib/slack-search/manifest.ts

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import {
33
SLACK_MANAGED_USER_ENROLLMENT_CALLBACK_PATH,
44
SLACK_SEARCH_USER_SCOPES,
55
} from '@/lib/credential-groups/slack-managed-user-scopes'
6-
import { SLACK_SEARCH_SCOPES, SLACK_SHARED_SEARCH_BOT_SCOPES } from '@/lib/slack-search/constants'
6+
import { SLACK_SHARED_SEARCH_BOT_SCOPES } from '@/lib/slack-search/constants'
77

88
export const SLACK_SEARCH_CALLBACK_PATH = '/api/knowledge/slack/oauth/callback'
99
export const SLACK_SEARCH_WEBHOOK_PATH = '/api/webhooks/slack'
1010
export const SLACK_SEARCH_DEFAULT_NAME = 'Sim Search'
1111
export const SLACK_SEARCH_DEFAULT_DESCRIPTION =
1212
'Ask questions about your organization’s knowledge and get answers with sources.'
1313

14-
/** Bot conversations and member indexing share one manifest and app identity. */
14+
/**
15+
* Custom and shared apps declare the same permissions, including planned capabilities.
16+
* Runtime OAuth validation requires only scopes used by implemented features.
17+
*/
1518
export function createSlackSearchManifest(
1619
name: string,
1720
description: string,
@@ -38,8 +41,40 @@ export function createSlackSearchManifest(
3841
SLACK_MANAGED_USER_ENROLLMENT_CALLBACK_PATH,
3942
].map((path) => new URL(path, url).href),
4043
scopes: {
41-
bot: [...SLACK_SEARCH_SCOPES],
42-
user: [...new Set([...SLACK_SEARCH_USER_SCOPES, ...existingUserScopes])],
44+
bot: [
45+
...SLACK_SHARED_SEARCH_BOT_SCOPES,
46+
'channels:history',
47+
'channels:manage',
48+
'channels:write.invites',
49+
'chat:write.public',
50+
'groups:history',
51+
'groups:write',
52+
'groups:write.invites',
53+
'links:read',
54+
'links:write',
55+
'mpim:history',
56+
'mpim:read',
57+
'mpim:write',
58+
'reactions:write',
59+
],
60+
user: [
61+
...new Set([
62+
...SLACK_SEARCH_USER_SCOPES,
63+
'canvases:read',
64+
'canvases:write',
65+
'chat:write',
66+
'files:read',
67+
'search:read.files',
68+
'search:read.im',
69+
'search:read.mpim',
70+
'search:read.private',
71+
'search:read.public',
72+
'search:read.users',
73+
'team:read',
74+
'usergroups:read',
75+
...existingUserScopes,
76+
]),
77+
],
4378
},
4479
},
4580
settings: {
@@ -55,10 +90,7 @@ export function createSlackSearchManifest(
5590
}
5691
}
5792

58-
/**
59-
* Declares the company app's permissions, including planned capabilities.
60-
* Runtime OAuth validation continues to require only scopes used by implemented features.
61-
*/
93+
/** Adds the official app's commands and lifecycle events to the common manifest. */
6294
export function createSharedSlackSearchManifest(origin: string) {
6395
const manifest = createSlackSearchManifest(
6496
SLACK_SEARCH_DEFAULT_NAME,
@@ -87,42 +119,6 @@ export function createSharedSlackSearchManifest(origin: string) {
87119
},
88120
],
89121
},
90-
oauth_config: {
91-
...manifest.oauth_config,
92-
scopes: {
93-
bot: [
94-
...SLACK_SHARED_SEARCH_BOT_SCOPES,
95-
'channels:history',
96-
'channels:manage',
97-
'channels:write.invites',
98-
'chat:write.public',
99-
'groups:history',
100-
'groups:write',
101-
'groups:write.invites',
102-
'links:read',
103-
'links:write',
104-
'mpim:history',
105-
'mpim:read',
106-
'mpim:write',
107-
'reactions:write',
108-
],
109-
user: [
110-
...SLACK_SEARCH_USER_SCOPES,
111-
'canvases:read',
112-
'canvases:write',
113-
'chat:write',
114-
'files:read',
115-
'search:read.files',
116-
'search:read.im',
117-
'search:read.mpim',
118-
'search:read.private',
119-
'search:read.public',
120-
'search:read.users',
121-
'team:read',
122-
'usergroups:read',
123-
],
124-
},
125-
},
126122
settings: {
127123
...manifest.settings,
128124
event_subscriptions: {

0 commit comments

Comments
 (0)