You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Add slack/socket mode dependency
* Update config types and SlackAdapter class
* Add socket mode methods, extract interactive dispatch
* Update createSlackAdapter factory function
* Write tests for socket mode
* Create slack-socket-mode.md
* Run fix
* Fix polynomial regex issues
* Fix: Floating promises in `routeSocketEvent` for slash commands and interactive payloads can cause unhandled promise rejections that crash the Node.js process.
This commit fixes the issue reported at packages/adapter-slack/src/index.ts:1152
**Bug Analysis:**
In `routeSocketEvent` (line 1150), which is a synchronous `void` method, two async operations produce floating promises:
1. `this.handleSlashCommand(params)` (line 1165) - `handleSlashCommand` is `async` and always returns a `Promise<Response>`. It calls `await this.lookupUser(userId)` which internally calls `await this.chat.getState().get()` (before the try/catch around the API call), and `this.chat.processSlashCommand()`. Any of these could throw.
2. `this.dispatchInteractivePayload(payload)` (line 1172) - Returns `Response | Promise<Response>`. When the payload type is `view_submission`, it delegates to `async handleViewSubmission()`, which calls `await this.chat.processModalSubmit()` and accesses `payload.view.state.values` (which could throw on malformed payloads).
Since `routeSocketEvent` is synchronous (`void` return type) and called from a sync context within the socket mode event handler (after `await ack()` has already completed), these returned promises are fire-and-forget. If any reject, it triggers an unhandled promise rejection, which in Node.js 15+ terminates the process by default.
In contrast, in the webhook code path (`handleWebhook`), these same methods are always `return`-ed from async functions, so their promises are properly chained to the caller.
**Fix:**
Added `.catch()` handlers to both floating promises:
1. For `handleSlashCommand`: Added `.catch()` that logs the error via `this.logger.error`.
2. For `dispatchInteractivePayload`: Since it returns `Response | Promise<Response>` (only a Promise for `view_submission`), used `instanceof Promise` to conditionally attach a `.catch()` handler only when the result is a Promise.
This approach was chosen over making `routeSocketEvent` async because: (a) it doesn't change the method signature, (b) the caller doesn't need to await it (the ack has already been sent), and (c) errors are logged rather than silently swallowed.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: haydenbleasel <hello@haydenbleasel.com>
* Add socket mode forwarding support to Slack adapter
- Export SlackForwardedSocketEvent type
- Add x-slack-socket-token check at top of handleWebhook() for forwarded events
- Update routeSocketEvent() to accept WebhookOptions and use waitUntil
- Add startSocketModeListener(), runSocketModeListener(), forwardSocketEvent()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for socket mode forwarding
- Forwarded event accepted/rejected based on appToken
- Bypasses signature verification for forwarded events
- Options passthrough to handlers
- startSocketModeListener returns 200/500 appropriately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add socket mode cron route and vercel config
- New /api/slack/socket-mode route using createPersistentListener
- Mirrors Discord gateway pattern (CRON_SECRET auth, Redis coordination)
- Cron runs every 9 min, listener duration 10 min
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix signingSecret defaulting to empty string in socket mode
Make signingSecret optional (string | undefined) instead of falling
back to "". verifySignature now returns false when no secret is
configured, preventing HMAC with an empty key from silently passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wrap event_callback in try-catch in routeSocketEvent
Sync errors from processEventPayload were silently dropped in
socket mode. Wrap with try-catch for parity with slash_commands
and interactive cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use dedicated socketForwardingSecret for forwarding auth
Stop using the Slack app-level token (xapp-...) as the bearer token
for HTTP forwarding. Adds socketForwardingSecret config option
(auto-detected from SLACK_SOCKET_FORWARDING_SECRET) with fallback
to appToken for backwards compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Replace double cast with type guard for socket event body
Validate body.event exists and construct a properly typed
SlackWebhookPayload instead of using `as unknown as`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Internalize SlackForwardedSocketEvent type
Remove export — only used internally by the forwarding mechanism.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix formatting in socketForwardingSecret check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add socket mode documentation to Slack adapter README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(slack): use SDK envelope type for socket mode event routing
* fix(slack): pass interactive response through ack in socket mode
* feat(chat): add clear modal response action to close entire view stack
* chore: update changeset for clear modal action
---------
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
Add Socket Mode support for environments behind firewalls that can't expose public HTTP endpoints, and add `{ action: "clear" }` modal response to close the entire modal view stack
Copy file name to clipboardExpand all lines: packages/adapter-slack/README.md
+88-2Lines changed: 88 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -102,6 +102,86 @@ openssl rand -base64 32
102
102
103
103
When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently.
104
104
105
+
## Socket mode
106
+
107
+
For environments behind firewalls that can't expose public HTTP endpoints, the adapter supports [Slack Socket Mode](https://api.slack.com/apis/socket-mode). Instead of receiving webhooks, the adapter connects to Slack over a WebSocket.
1. Go to your app's settings at [api.slack.com/apps](https://api.slack.com/apps)
128
+
2. Navigate to **Socket Mode** and enable it
129
+
3. Generate an **App-Level Token** with the `connections:write` scope — this is your `SLACK_APP_TOKEN` (`xapp-...`)
130
+
4. Event subscriptions and interactivity still need to be configured, but no public request URL is required
131
+
132
+
> Socket mode is not compatible with multi-workspace OAuth (`clientId`/`clientSecret`). It's designed for single-workspace deployments.
133
+
134
+
### Socket mode on serverless (Vercel)
135
+
136
+
Socket mode requires a persistent WebSocket connection, which doesn't fit the request/response model of serverless functions. The adapter provides a forwarding mechanism to bridge this gap:
137
+
138
+
1. A cron job periodically starts a transient socket listener
139
+
2. The listener connects via WebSocket, acks events immediately, and forwards them as HTTP requests to your webhook endpoint
140
+
3. Your existing webhook route processes the forwarded events normally
Schedule the cron job to run every 9 minutes (overlapping with the 10-minute listener duration) to maintain continuous coverage:
170
+
171
+
```json
172
+
// vercel.json
173
+
{
174
+
"crons": [
175
+
{
176
+
"path": "/api/slack/socket-mode",
177
+
"schedule": "*/9 * * * *"
178
+
}
179
+
]
180
+
}
181
+
```
182
+
183
+
Forwarded events are authenticated using the `socketForwardingSecret` config option (defaults to `SLACK_SOCKET_FORWARDING_SECRET` env var, falling back to `appToken`).
184
+
105
185
## Slack app setup
106
186
107
187
### 1. Create a Slack app from manifest
@@ -188,19 +268,25 @@ All options are auto-detected from environment variables when not provided. You
188
268
|--------|----------|-------------|
189
269
| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
190
270
| `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
271
+
| `mode` | No | Connection mode: `"webhook"`(default) or `"socket"` |
272
+
| `appToken` | No** | App-level token (`xapp-...`) for socket mode. Auto-detected from `SLACK_APP_TOKEN` |
273
+
| `socketForwardingSecret` | No | Shared secret for authenticating forwarded socket events. Auto-detected from `SLACK_SOCKET_FORWARDING_SECRET`, falls back to `appToken` |
191
274
| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
192
275
| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
193
276
| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
194
277
| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
195
278
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
196
279
197
-
*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
280
+
*`signingSecret` is required for webhook mode — either via config or `SLACK_SIGNING_SECRET` env var.
281
+
**`appToken` is required for socket mode — either via config or `SLACK_APP_TOKEN` env var.
198
282
199
283
## Environment variables
200
284
201
285
```bash
202
286
SLACK_BOT_TOKEN=xoxb-... # Single-workspace only
203
-
SLACK_SIGNING_SECRET=...
287
+
SLACK_SIGNING_SECRET=... # Required for webhook mode
288
+
SLACK_APP_TOKEN=xapp-... # Required for socket mode
289
+
SLACK_SOCKET_FORWARDING_SECRET=... # Optional, for socket event forwarding auth
204
290
SLACK_CLIENT_ID=... # Multi-workspace only
205
291
SLACK_CLIENT_SECRET=... # Multi-workspace only
206
292
SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
0 commit comments