@chat-adapter/slack 4.17.0 → 4.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,15 +3,17 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@chat-adapter/slack)](https://www.npmjs.com/package/@chat-adapter/slack)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/slack)](https://www.npmjs.com/package/@chat-adapter/slack)
5
5
 
6
- Slack adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports single-workspace and multi-workspace OAuth deployments.
6
+ Slack adapter for [Chat SDK](https://chat-sdk.dev). Configure single-workspace or multi-workspace OAuth deployments.
7
7
 
8
8
  ## Installation
9
9
 
10
10
  ```bash
11
- npm install chat @chat-adapter/slack
11
+ pnpm add @chat-adapter/slack
12
12
  ```
13
13
 
14
- ## Usage
14
+ ## Single-workspace mode
15
+
16
+ For bots deployed to a single Slack workspace. The adapter auto-detects `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from environment variables:
15
17
 
16
18
  ```typescript
17
19
  import { Chat } from "chat";
@@ -20,10 +22,7 @@ import { createSlackAdapter } from "@chat-adapter/slack";
20
22
  const bot = new Chat({
21
23
  userName: "mybot",
22
24
  adapters: {
23
- slack: createSlackAdapter({
24
- botToken: process.env.SLACK_BOT_TOKEN!,
25
- signingSecret: process.env.SLACK_SIGNING_SECRET!,
26
- }),
25
+ slack: createSlackAdapter(),
27
26
  },
28
27
  });
29
28
 
@@ -32,9 +31,324 @@ bot.onNewMention(async (thread, message) => {
32
31
  });
33
32
  ```
34
33
 
35
- ## Documentation
34
+ ## Multi-workspace mode
35
+
36
+ For apps installed across multiple Slack workspaces via OAuth, omit `botToken` and provide OAuth credentials instead. The adapter resolves tokens dynamically from your state adapter using the `team_id` from incoming webhooks.
37
+
38
+ When you pass any auth-related config (like `clientId`), the adapter won't fall back to env vars for other auth fields, preventing accidental mixing of auth modes.
39
+
40
+ ```typescript
41
+ import { createSlackAdapter } from "@chat-adapter/slack";
42
+ import { createRedisState } from "@chat-adapter/state-redis";
43
+
44
+ const slackAdapter = createSlackAdapter({
45
+ clientId: process.env.SLACK_CLIENT_ID!,
46
+ clientSecret: process.env.SLACK_CLIENT_SECRET!,
47
+ });
48
+
49
+ const bot = new Chat({
50
+ userName: "mybot",
51
+ adapters: { slack: slackAdapter },
52
+ state: createRedisState(),
53
+ });
54
+ ```
55
+
56
+ ### OAuth callback
57
+
58
+ The adapter handles the full Slack OAuth V2 exchange. Point your OAuth redirect URL to a route that calls `handleOAuthCallback`:
59
+
60
+ ```typescript
61
+ import { slackAdapter } from "@/lib/bot";
62
+
63
+ export async function GET(request: Request) {
64
+ const { teamId } = await slackAdapter.handleOAuthCallback(request);
65
+ return new Response(`Installed for team ${teamId}!`);
66
+ }
67
+ ```
68
+
69
+ ### Using the adapter outside webhooks
70
+
71
+ During webhook handling, the adapter resolves tokens automatically from `team_id`. Outside that context (e.g. cron jobs or background workers), use `getInstallation` and `withBotToken`:
72
+
73
+ ```typescript
74
+ const install = await slackAdapter.getInstallation(teamId);
75
+ if (!install) throw new Error("Workspace not installed");
76
+
77
+ await slackAdapter.withBotToken(install.botToken, async () => {
78
+ const thread = bot.thread("slack:C12345:1234567890.123456");
79
+ await thread.post("Hello from a cron job!");
80
+ });
81
+ ```
82
+
83
+ `withBotToken` uses `AsyncLocalStorage` under the hood, so concurrent calls with different tokens are isolated.
84
+
85
+ ### Removing installations
86
+
87
+ ```typescript
88
+ await slackAdapter.deleteInstallation(teamId);
89
+ ```
90
+
91
+ ### Token encryption
92
+
93
+ Pass a base64-encoded 32-byte key as `encryptionKey` to encrypt bot tokens at rest using AES-256-GCM:
94
+
95
+ ```bash
96
+ openssl rand -base64 32
97
+ ```
98
+
99
+ When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently.
100
+
101
+ ## Slack app setup
102
+
103
+ ### 1. Create a Slack app from manifest
104
+
105
+ 1. Go to [api.slack.com/apps](https://api.slack.com/apps)
106
+ 2. Click **Create New App** then **From an app manifest**
107
+ 3. Select your workspace and paste the following manifest:
108
+
109
+ ```yaml
110
+ display_information:
111
+ name: My Bot
112
+ description: A bot built with chat-sdk
113
+
114
+ features:
115
+ bot_user:
116
+ display_name: My Bot
117
+ always_online: true
118
+
119
+ oauth_config:
120
+ scopes:
121
+ bot:
122
+ - app_mentions:read
123
+ - channels:history
124
+ - channels:read
125
+ - chat:write
126
+ - groups:history
127
+ - groups:read
128
+ - im:history
129
+ - im:read
130
+ - mpim:history
131
+ - mpim:read
132
+ - reactions:read
133
+ - reactions:write
134
+ - users:read
135
+
136
+ settings:
137
+ event_subscriptions:
138
+ request_url: https://your-domain.com/api/webhooks/slack
139
+ bot_events:
140
+ - app_mention
141
+ - message.channels
142
+ - message.groups
143
+ - message.im
144
+ - message.mpim
145
+ - member_joined_channel
146
+ - assistant_thread_started
147
+ - assistant_thread_context_changed
148
+ interactivity:
149
+ is_enabled: true
150
+ request_url: https://your-domain.com/api/webhooks/slack
151
+ org_deploy_enabled: false
152
+ socket_mode_enabled: false
153
+ token_rotation_enabled: false
154
+ ```
155
+
156
+ 4. Replace `https://your-domain.com/api/webhooks/slack` with your deployed webhook URL
157
+ 5. Click **Create**
158
+
159
+ ### 2. Get credentials
160
+
161
+ After creating the app, go to **Basic Information** → **App Credentials** and copy:
162
+
163
+ - **Signing Secret** as `SLACK_SIGNING_SECRET`
164
+ - **Client ID** as `SLACK_CLIENT_ID` (multi-workspace only)
165
+ - **Client Secret** as `SLACK_CLIENT_SECRET` (multi-workspace only)
166
+
167
+ **Single workspace:** Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) as `SLACK_BOT_TOKEN`.
168
+
169
+ **Multi-workspace:** Enable **Manage Distribution** under **Basic Information** and set up an OAuth redirect URL pointing to your callback route.
170
+
171
+ ### 3. Configure slash commands (optional)
172
+
173
+ 1. Go to **Slash Commands** in your app settings
174
+ 2. Click **Create New Command**
175
+ 3. Set **Command** (e.g., `/feedback`)
176
+ 4. Set **Request URL** to `https://your-domain.com/api/webhooks/slack`
177
+ 5. Add a description and click **Save**
178
+
179
+ ## Configuration
180
+
181
+ All options are auto-detected from environment variables when not provided. You can call `createSlackAdapter()` with no arguments if the env vars are set.
182
+
183
+ | Option | Required | Description |
184
+ |--------|----------|-------------|
185
+ | `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
186
+ | `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
187
+ | `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
188
+ | `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
189
+ | `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
190
+ | `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
191
+ | `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
192
+
193
+ *`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
194
+
195
+ ## Environment variables
196
+
197
+ ```bash
198
+ SLACK_BOT_TOKEN=xoxb-... # Single-workspace only
199
+ SLACK_SIGNING_SECRET=...
200
+ SLACK_CLIENT_ID=... # Multi-workspace only
201
+ SLACK_CLIENT_SECRET=... # Multi-workspace only
202
+ SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
203
+ ```
204
+
205
+ ## Features
206
+
207
+ ### Messaging
208
+
209
+ | Feature | Supported |
210
+ |---------|-----------|
211
+ | Post message | Yes |
212
+ | Edit message | Yes |
213
+ | Delete message | Yes |
214
+ | File uploads | Yes |
215
+ | Streaming | Native API |
216
+
217
+ ### Rich content
218
+
219
+ | Feature | Supported |
220
+ |---------|-----------|
221
+ | Card format | Block Kit |
222
+ | Buttons | Yes |
223
+ | Link buttons | Yes |
224
+ | Select menus | Yes |
225
+ | Tables | Block Kit |
226
+ | Fields | Yes |
227
+ | Images in cards | Yes |
228
+ | Modals | Yes |
229
+
230
+ ### Conversations
231
+
232
+ | Feature | Supported |
233
+ |---------|-----------|
234
+ | Slash commands | Yes |
235
+ | Mentions | Yes |
236
+ | Add reactions | Yes |
237
+ | Remove reactions | Yes |
238
+ | Typing indicator | Yes |
239
+ | DMs | Yes |
240
+ | Ephemeral messages | Yes (native) |
241
+
242
+ ### Message history
243
+
244
+ | Feature | Supported |
245
+ |---------|-----------|
246
+ | Fetch messages | Yes |
247
+ | Fetch single message | Yes |
248
+ | Fetch thread info | Yes |
249
+ | Fetch channel messages | Yes |
250
+ | List threads | Yes |
251
+ | Fetch channel info | Yes |
252
+ | Post channel message | Yes |
253
+
254
+ ### Platform-specific
255
+
256
+ | Feature | Supported |
257
+ |---------|-----------|
258
+ | Assistants API | Yes |
259
+ | Member joined channel | Yes |
260
+ | App Home tab | Yes |
261
+
262
+ ## Slack Assistants API
263
+
264
+ The adapter supports Slack's [Assistants API](https://api.slack.com/docs/apps/ai) for building AI-powered assistant experiences. This enables suggested prompts, status indicators, and thread titles in assistant DM threads.
265
+
266
+ ### Event handlers
267
+
268
+ Register handlers on the `Chat` instance:
269
+
270
+ ```typescript
271
+ bot.onAssistantThreadStarted(async (event) => {
272
+ const slack = bot.getAdapter("slack") as SlackAdapter;
273
+ await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
274
+ { title: "Summarize", message: "Summarize this channel" },
275
+ { title: "Draft", message: "Help me draft a message" },
276
+ ]);
277
+ });
278
+
279
+ bot.onAssistantContextChanged(async (event) => {
280
+ // User navigated to a different channel with the assistant panel open
281
+ });
282
+ ```
283
+
284
+ ### Adapter methods
285
+
286
+ The `SlackAdapter` exposes these methods for the Assistants API:
287
+
288
+ | Method | Description |
289
+ |--------|-------------|
290
+ | `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
291
+ | `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
292
+ | `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
293
+ | `publishHomeView(userId, view)` | Publish a Home tab view for a user |
294
+ | `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
295
+
296
+ ### Required scopes and events
297
+
298
+ Add these to your Slack app manifest for Assistants API support:
299
+
300
+ ```yaml
301
+ oauth_config:
302
+ scopes:
303
+ bot:
304
+ - assistant:write
305
+
306
+ settings:
307
+ event_subscriptions:
308
+ bot_events:
309
+ - assistant_thread_started
310
+ - assistant_thread_context_changed
311
+ ```
312
+
313
+ ### Stream with stop blocks
314
+
315
+ When streaming in an assistant thread, you can attach Block Kit elements to the final message:
316
+
317
+ ```typescript
318
+ await thread.stream(textStream, {
319
+ stopBlocks: [
320
+ { type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
321
+ ],
322
+ });
323
+ ```
324
+
325
+ ## Troubleshooting
326
+
327
+ ### `handleOAuthCallback` throws "Adapter not initialized"
328
+
329
+ - Call `await bot.initialize()` before `handleOAuthCallback()` in your callback route.
330
+ - In a Next.js app, this ensures:
331
+ - state adapter is connected
332
+ - the Slack adapter is attached to Chat
333
+ - installation writes succeed
334
+
335
+ ```typescript
336
+ const slackAdapter = bot.getAdapter("slack");
337
+
338
+ await bot.initialize();
339
+ await slackAdapter.handleOAuthCallback(request);
340
+ ```
341
+
342
+ ### "Invalid signature" error
343
+
344
+ - Verify `SLACK_SIGNING_SECRET` is correct
345
+ - Check that the request timestamp is within 5 minutes (clock sync issue)
346
+
347
+ ### Bot not responding to messages
36
348
 
37
- Full setup instructions, configuration reference, and features at [chat-sdk.dev/docs/adapters/slack](https://chat-sdk.dev/docs/adapters/slack).
349
+ - Verify event subscriptions are configured
350
+ - Check that the bot has been added to the channel
351
+ - Ensure the webhook URL is correct and accessible
38
352
 
39
353
  ## License
40
354
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
1
+ import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ScheduledMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
2
2
 
3
3
  /**
4
4
  * Slack Block Kit converter for cross-platform cards.
@@ -59,6 +59,12 @@ declare class SlackFormatConverter extends BaseFormatConverter {
59
59
  * Parse Slack mrkdwn into an AST.
60
60
  */
61
61
  toAst(mrkdwn: string): Root;
62
+ /**
63
+ * Convert AST to Slack blocks, using a native table block for the first table.
64
+ * Returns null if the AST contains no tables (caller should use regular text).
65
+ * Slack allows at most one table block per message; additional tables use ASCII.
66
+ */
67
+ toBlocksWithTable(ast: Root): SlackBlock[] | null;
62
68
  private nodeToMrkdwn;
63
69
  }
64
70
 
@@ -307,8 +313,16 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
307
313
  * Includes a fetchData method that uses the bot token for auth.
308
314
  */
309
315
  private createAttachment;
316
+ /**
317
+ * Try to render a message using native Slack table blocks.
318
+ * Returns blocks + fallback text if the message contains tables, null otherwise.
319
+ */
320
+ private renderWithTableBlocks;
310
321
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
311
322
  postEphemeral(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
323
+ scheduleMessage(threadId: string, message: AdapterPostableMessage, options: {
324
+ postAt: Date;
325
+ }): Promise<ScheduledMessage>;
312
326
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
313
327
  viewId: string;
314
328
  }>;