Skip to content

Add Slack Socket Mode support#162

Open
haydenbleasel wants to merge 19 commits intomainfrom
123-slack-socket-mode-support
Open

Add Slack Socket Mode support#162
haydenbleasel wants to merge 19 commits intomainfrom
123-slack-socket-mode-support

Conversation

@haydenbleasel
Copy link
Contributor

@haydenbleasel haydenbleasel commented Mar 2, 2026

Summary

  • Adds serverless-compatible Socket Mode forwarding to the Slack adapter, mirroring the Discord gateway pattern
  • A cron-invoked listener maintains the WebSocket, acks events, and forwards them via HTTP POST to the existing webhook endpoint using an x-slack-socket-token header
  • The adapter stays in mode: "webhook" (default) — socket mode listener is a separate external concern
  • routeSocketEvent() now accepts WebhookOptions and uses waitUntil for async handlers
  • New startSocketModeListener() / runSocketModeListener() / forwardSocketEvent() methods
  • Example cron route at /api/slack/socket-mode with createPersistentListener for Redis-based cross-instance coordination
  • Cron runs every 9 min, listener duration 10 min (same as Discord)

Test plan

  • Forwarded event accepted with valid appToken → 200
  • Forwarded event rejected with invalid token → 401
  • Forwarded event rejected when no appToken configured → 401
  • Bypasses signature verification for forwarded events
  • Options passthrough to handlers for forwarded events
  • startSocketModeListener returns 200 with valid config
  • startSocketModeListener returns 500 without waitUntil or appToken
  • routeSocketEvent passes options through to handlers
  • All 258 tests pass
  • Full pnpm validate passes (knip, check, typecheck, test, build)

Closes #123

@haydenbleasel haydenbleasel linked an issue Mar 2, 2026 that may be closed by this pull request
1 task
@vercel
Copy link
Contributor

vercel bot commented Mar 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview, Comment, Open in v0 Mar 16, 2026 8:29pm
chat-sdk-nextjs-chat Ready Ready Preview, Comment, Open in v0 Mar 16, 2026 8:29pm

@socket-security
Copy link

socket-security bot commented Mar 2, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​streamdown/​code@​1.0.3 ⏵ 1.1.076 +110061 +193 +3100
Updatedremend@​1.2.1 ⏵ 1.2.2100 +1100100 +191 -1100
Added@​slack/​socket-mode@​2.0.59910010092100
Updatedstreamdown@​2.3.0 ⏵ 2.4.099 +110010097 -1100
Updated@​slack/​web-api@​7.14.1 ⏵ 7.15.099 +110010098100

View full report

haydenbleasel and others added 14 commits March 16, 2026 13:20
…nteractive 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>
- 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>
- 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>
- 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>
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>
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>
haydenbleasel and others added 4 commits March 16, 2026 13:23
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>
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>
Remove export — only used internally by the forwarding mechanism.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@haydenbleasel haydenbleasel marked this pull request as ready for review March 16, 2026 20:28
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack Socket Mode Support

1 participant