@chat-adapter/slack 4.8.0 → 4.9.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
@@ -8,7 +8,7 @@ Slack adapter for the [chat](https://github.com/vercel-labs/chat) SDK.
8
8
  npm install chat @chat-adapter/slack
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Usage (Single Workspace)
12
12
 
13
13
  ```typescript
14
14
  import { Chat } from "chat";
@@ -30,18 +30,109 @@ chat.onNewMention(async (thread, message) => {
30
30
  });
31
31
  ```
32
32
 
33
+ ## Multi-Workspace Mode
34
+
35
+ For apps installed across multiple Slack workspaces, omit `botToken` and let the adapter resolve tokens dynamically from your state adapter (e.g. Redis) using the `team_id` from incoming webhooks.
36
+
37
+ ```typescript
38
+ import { Chat } from "chat";
39
+ import { createSlackAdapter } from "@chat-adapter/slack";
40
+ import { createRedisState } from "@chat-adapter/state-redis";
41
+
42
+ const slackAdapter = createSlackAdapter({
43
+ signingSecret: process.env.SLACK_SIGNING_SECRET!,
44
+ clientId: process.env.SLACK_CLIENT_ID!,
45
+ clientSecret: process.env.SLACK_CLIENT_SECRET!,
46
+ logger: logger,
47
+ encryptionKey: process.env.SLACK_ENCRYPTION_KEY, // optional, encrypts tokens at rest
48
+ });
49
+
50
+ const chat = new Chat({
51
+ userName: "mybot",
52
+ adapters: { slack: slackAdapter },
53
+ state: createRedisState({ url: process.env.REDIS_URL! }),
54
+ // notice that there is no bot token
55
+ });
56
+ ```
57
+
58
+ ### OAuth callback
59
+
60
+ The adapter handles the full Slack OAuth V2 exchange. Pass `clientId` and `clientSecret` in the config, then point your OAuth redirect URL to a route that calls `handleOAuthCallback`:
61
+
62
+ ```typescript
63
+ import { slackAdapter } from "@/lib/chat"; // your adapter instance
64
+
65
+ export async function GET(request: Request) {
66
+ const { teamId } = await slackAdapter.handleOAuthCallback(request);
67
+ return new Response(`Installed for team ${teamId}!`);
68
+ }
69
+ ```
70
+
71
+ ### Webhook handling
72
+
73
+ No changes needed — the adapter extracts `team_id` from incoming webhooks and resolves the token automatically:
74
+
75
+ ```typescript
76
+ export async function POST(request: Request) {
77
+ return chat.webhooks.slack(request, { waitUntil });
78
+ }
79
+ ```
80
+
81
+ ### Using the adapter outside a webhook (cron jobs, workflows)
82
+
83
+ During webhook handling, the adapter resolves the token automatically from `team_id`. Outside that context (e.g. a cron job), use `getInstallation` to retrieve the token and `withBotToken` to scope it:
84
+
85
+ ```typescript
86
+ import { Chat } from "chat";
87
+
88
+ // In a cron job or background worker:
89
+ const install = await slackAdapter.getInstallation(teamId);
90
+ if (!install) throw new Error("Workspace not installed");
91
+
92
+ await slackAdapter.withBotToken(install.botToken, async () => {
93
+ // All adapter calls inside this callback use the provided token.
94
+ // You can use thread.post(), thread.subscribe(), etc. normally.
95
+ const thread = chat.thread("slack:C12345:1234567890.123456");
96
+ await thread.post("Hello from a cron job!");
97
+ });
98
+ ```
99
+
100
+ `withBotToken` uses `AsyncLocalStorage` under the hood, so concurrent calls with different tokens are isolated from each other.
101
+
102
+ ### Removing installations
103
+
104
+ ```typescript
105
+ await slackAdapter.deleteInstallation(teamId);
106
+ ```
107
+
108
+ ### Encryption
109
+
110
+ Pass a base64-encoded 32-byte key as `encryptionKey` to encrypt bot tokens at rest using AES-256-GCM. You can generate a key with:
111
+
112
+ ```bash
113
+ openssl rand -base64 32
114
+ ```
115
+
116
+ When `encryptionKey` is set, `setInstallation()` encrypts the token before storing it and `getInstallation()` decrypts it transparently.
117
+
33
118
  ## Configuration
34
119
 
35
120
  | Option | Required | Description |
36
121
  |--------|----------|-------------|
37
- | `botToken` | Yes | Slack bot token (starts with `xoxb-`) |
122
+ | `botToken` | No | Slack bot token (`xoxb-...`). Required for single-workspace mode. Omit for multi-workspace. |
38
123
  | `signingSecret` | Yes | Slack signing secret for webhook verification |
124
+ | `clientId` | No | Slack app client ID (required for OAuth / multi-workspace) |
125
+ | `clientSecret` | No | Slack app client secret (required for OAuth / multi-workspace) |
126
+ | `encryptionKey` | No | Base64-encoded 32-byte AES-256-GCM key for encrypting stored tokens |
39
127
 
40
128
  ## Environment Variables
41
129
 
42
130
  ```bash
43
- SLACK_BOT_TOKEN=xoxb-...
131
+ SLACK_BOT_TOKEN=xoxb-... # single-workspace only
44
132
  SLACK_SIGNING_SECRET=...
133
+ SLACK_CLIENT_ID=... # required for multi-workspace OAuth
134
+ SLACK_CLIENT_SECRET=... # required for multi-workspace OAuth
135
+ SLACK_ENCRYPTION_KEY=... # optional, for multi-workspace token encryption
45
136
  ```
46
137
 
47
138
  ## Slack App Setup
@@ -71,15 +162,22 @@ SLACK_SIGNING_SECRET=...
71
162
 
72
163
  ### 3. Install App to Workspace
73
164
 
165
+ **Single workspace:** Install directly from the Slack dashboard.
166
+
74
167
  1. Go to **OAuth & Permissions**
75
168
  2. Click **Install to Workspace**
76
169
  3. Authorize the app
77
170
  4. Copy the **Bot User OAuth Token** (starts with `xoxb-`) → `SLACK_BOT_TOKEN`
78
171
 
79
- ### 4. Get Signing Secret
172
+ **Multi-workspace:** Enable **Manage Distribution** under **Basic Information**, then set up an [OAuth redirect URL](https://api.slack.com/authentication/oauth-v2) pointing to your callback route. The adapter handles the token exchange via `handleOAuthCallback()` (see [Multi-Workspace Mode](#multi-workspace-mode) above).
173
+
174
+ ### 4. Get Signing Secret and OAuth Credentials
80
175
 
81
176
  1. Go to **Basic Information**
82
- 2. Under **App Credentials**, copy **Signing Secret** → `SLACK_SIGNING_SECRET`
177
+ 2. Under **App Credentials**, copy:
178
+ - **Signing Secret** → `SLACK_SIGNING_SECRET`
179
+ - **Client ID** → `SLACK_CLIENT_ID` (multi-workspace only)
180
+ - **Client Secret** → `SLACK_CLIENT_SECRET` (multi-workspace only)
83
181
 
84
182
  ### 5. Configure Event Subscriptions
85
183
 
@@ -104,6 +202,7 @@ If you want to use buttons, modals, or other interactive components:
104
202
 
105
203
  ## Features
106
204
 
205
+ - Multi-workspace support with OAuth V2 and encrypted token storage
107
206
  - Message posting and editing
108
207
  - Thread subscriptions
109
208
  - Reaction handling (add/remove/events)
package/dist/index.d.ts CHANGED
@@ -22,6 +22,13 @@ declare function cardToBlockKit(card: CardElement): SlackBlock[];
22
22
  */
23
23
  declare function cardToFallbackText(card: CardElement): string;
24
24
 
25
+ interface EncryptedTokenData {
26
+ iv: string;
27
+ data: string;
28
+ tag: string;
29
+ }
30
+ declare function decodeKey(rawKey: string): Buffer;
31
+
25
32
  /**
26
33
  * Slack-specific format conversion using AST-based parsing.
27
34
  *
@@ -56,8 +63,8 @@ declare class SlackFormatConverter extends BaseFormatConverter {
56
63
  }
57
64
 
58
65
  interface SlackAdapterConfig {
59
- /** Bot token (xoxb-...) */
60
- botToken: string;
66
+ /** Bot token (xoxb-...). Required for single-workspace mode. Omit for multi-workspace. */
67
+ botToken?: string;
61
68
  /** Signing secret for webhook verification */
62
69
  signingSecret: string;
63
70
  /** Logger instance for error reporting */
@@ -66,6 +73,21 @@ interface SlackAdapterConfig {
66
73
  userName?: string;
67
74
  /** Bot user ID (will be fetched if not provided) */
68
75
  botUserId?: string;
76
+ /**
77
+ * Base64-encoded 32-byte AES-256-GCM encryption key.
78
+ * If provided, bot tokens stored via setInstallation() will be encrypted at rest.
79
+ */
80
+ encryptionKey?: string;
81
+ /** Slack app client ID (required for OAuth / multi-workspace) */
82
+ clientId?: string;
83
+ /** Slack app client secret (required for OAuth / multi-workspace) */
84
+ clientSecret?: string;
85
+ }
86
+ /** Data stored per Slack workspace installation */
87
+ interface SlackInstallation {
88
+ botToken: string;
89
+ botUserId?: string;
90
+ teamName?: string;
69
91
  }
70
92
  /** Slack-specific thread ID data */
71
93
  interface SlackThreadId {
@@ -118,23 +140,75 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
118
140
  readonly userName: string;
119
141
  private client;
120
142
  private signingSecret;
121
- private botToken;
143
+ private defaultBotToken;
122
144
  private chat;
123
145
  private logger;
124
146
  private _botUserId;
125
147
  private _botId;
126
148
  private formatConverter;
127
149
  private static USER_CACHE_TTL_MS;
150
+ private clientId;
151
+ private clientSecret;
152
+ private encryptionKey;
153
+ private requestContext;
128
154
  /** Bot user ID (e.g., U_BOT_123) used for mention detection */
129
155
  get botUserId(): string | undefined;
130
156
  constructor(config: SlackAdapterConfig);
157
+ /**
158
+ * Get the current bot token for API calls.
159
+ * Checks request context (multi-workspace) → default token (single-workspace) → throws.
160
+ */
161
+ private getToken;
162
+ /**
163
+ * Add the current token to API call options.
164
+ * Workaround for Slack WebClient types not including `token` in per-method args.
165
+ */
166
+ private withToken;
131
167
  initialize(chat: ChatInstance): Promise<void>;
168
+ private installationKey;
169
+ /**
170
+ * Save a Slack workspace installation.
171
+ * Call this from your OAuth callback route after a successful installation.
172
+ */
173
+ setInstallation(teamId: string, installation: SlackInstallation): Promise<void>;
174
+ /**
175
+ * Retrieve a Slack workspace installation.
176
+ */
177
+ getInstallation(teamId: string): Promise<SlackInstallation | null>;
178
+ /**
179
+ * Handle the Slack OAuth V2 callback.
180
+ * Accepts the incoming request, extracts the authorization code,
181
+ * exchanges it for tokens, and saves the installation.
182
+ */
183
+ handleOAuthCallback(request: Request): Promise<{
184
+ teamId: string;
185
+ installation: SlackInstallation;
186
+ }>;
187
+ /**
188
+ * Remove a Slack workspace installation.
189
+ */
190
+ deleteInstallation(teamId: string): Promise<void>;
191
+ /**
192
+ * Run a function with a specific bot token in context.
193
+ * Use this for operations outside webhook handling (cron jobs, workflows).
194
+ */
195
+ withBotToken<T>(token: string, fn: () => T): T;
196
+ /**
197
+ * Resolve the bot token for a team from the state adapter.
198
+ */
199
+ private resolveTokenForTeam;
200
+ /**
201
+ * Extract team_id from an interactive payload (form-urlencoded).
202
+ */
203
+ private extractTeamIdFromInteractive;
132
204
  /**
133
205
  * Look up user info from Slack API with caching via state adapter.
134
206
  * Returns display name and real name, or falls back to user ID.
135
207
  */
136
208
  private lookupUser;
137
209
  handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
210
+ /** Extract and dispatch events from a validated payload */
211
+ private processEventPayload;
138
212
  /**
139
213
  * Handle Slack interactive payloads (button clicks, view submissions, etc.).
140
214
  * These are sent as form-urlencoded with a `payload` JSON field.
@@ -247,4 +321,4 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
247
321
  }
248
322
  declare function createSlackAdapter(config: SlackAdapterConfig): SlackAdapter;
249
323
 
250
- export { SlackAdapter, type SlackAdapterConfig, type SlackEvent, SlackFormatConverter, SlackFormatConverter as SlackMarkdownConverter, type SlackReactionEvent, type SlackThreadId, cardToBlockKit, cardToFallbackText, createSlackAdapter };
324
+ export { type EncryptedTokenData, SlackAdapter, type SlackAdapterConfig, type SlackEvent, SlackFormatConverter, type SlackInstallation, SlackFormatConverter as SlackMarkdownConverter, type SlackReactionEvent, type SlackThreadId, cardToBlockKit, cardToFallbackText, createSlackAdapter, decodeKey };