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
Copy Markdown
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
Copy Markdown
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
Copy Markdown

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>
@reaganmcf
Copy link
Copy Markdown

@haydenbleasel this looks incredibly promising and useful for us. Any update on when this might get pushed over the finish line?

@cramforce
Copy link
Copy Markdown
Collaborator

I'm inclined not to ship this, but I can be convinced otherwise. I'm not sure what it really gains us, but it does add complexity.

@reaganmcf
Copy link
Copy Markdown

Socket mode is a huge blocker for us to use this, and I know many other people who share the same sentiment.

Without socket mode, your bot has to expose the handlers to the public internet over HTTP. This is a hard no for many of us who work behind corporate firewalls in sensitive industries with strict IP rules.

Socket mode allows us to run these bots behind our firewalls, greatly improving adoption.

@cramforce
Copy link
Copy Markdown
Collaborator

That makes sense. Could you give the PR a try?

@reaganmcf
Copy link
Copy Markdown

Gave the PR a try, most of the functionality worked out of the box perfectly:

  • chat messages worked great
  • rendering Card and other components work great

The main issues that I've faced (and fixed in my local branch - can push it if useful but it's Claude slop haven't verified it yet) revolves around slash commands and modals:

  • Slash commands arrive without "type" field in socket mode which prevents it routing correctly. Seems like it's trying to route the raw payload
  • Interactive payloads have this same problem (type: view_submission)
  • Modal response actions (push, update, and error) ack() with no payload before processing the events, which I don't believe is the correct implementation (should ack(payload))

More than happy to submit the code I have to patch the PR if it would make it easier - or if there is any more information I can provide to help in any capacity let me know.

Really appreciate the work on this so far 😄

@cramforce
Copy link
Copy Markdown
Collaborator

@reaganmcf If you could contribute your changes that would be amazing! I'll merge when it is ready

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

3 participants