Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
38091a5
Add slack/socket mode dependency
haydenbleasel Mar 2, 2026
e2887af
Update config types and SlackAdapter class
haydenbleasel Mar 2, 2026
ee31b62
Add socket mode methods, extract interactive dispatch
haydenbleasel Mar 2, 2026
ebec0d6
Update createSlackAdapter factory function
haydenbleasel Mar 2, 2026
349eef8
Write tests for socket mode
haydenbleasel Mar 2, 2026
e64e36c
Create slack-socket-mode.md
haydenbleasel Mar 2, 2026
816f971
Run fix
haydenbleasel Mar 2, 2026
4795773
Fix polynomial regex issues
haydenbleasel Mar 2, 2026
a1d34bb
Fix: Floating promises in `routeSocketEvent` for slash commands and i…
vercel[bot] Mar 3, 2026
e4af0ae
Add socket mode forwarding support to Slack adapter
haydenbleasel Mar 3, 2026
d84af36
Add tests for socket mode forwarding
haydenbleasel Mar 3, 2026
62339d5
Add socket mode cron route and vercel config
haydenbleasel Mar 3, 2026
d98462b
Fix signingSecret defaulting to empty string in socket mode
haydenbleasel Mar 3, 2026
62f83b5
Wrap event_callback in try-catch in routeSocketEvent
haydenbleasel Mar 3, 2026
c82ea9d
Use dedicated socketForwardingSecret for forwarding auth
haydenbleasel Mar 3, 2026
d001452
Replace double cast with type guard for socket event body
haydenbleasel Mar 3, 2026
2a91337
Internalize SlackForwardedSocketEvent type
haydenbleasel Mar 3, 2026
9d8581c
Fix formatting in socketForwardingSecret check
haydenbleasel Mar 3, 2026
e8a3a97
Add socket mode documentation to Slack adapter README
haydenbleasel Mar 16, 2026
a99ccec
fix(slack): use SDK envelope type for socket mode event routing
dancer Apr 17, 2026
90beaa9
chore: resolve merge conflicts with main
dancer Apr 17, 2026
2bcd64b
fix(slack): pass interactive response through ack in socket mode
dancer Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slack-socket-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/slack": minor
---

Add Socket Mode support for environments behind firewalls that can't expose public HTTP endpoints
92 changes: 92 additions & 0 deletions examples/nextjs-chat/src/app/api/slack/socket-mode/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { after } from "next/server";
import { bot } from "@/lib/bot";
import { createPersistentListener } from "@/lib/persistent-listener";

export const maxDuration = 800;

// Default listener duration: 10 minutes
const DEFAULT_DURATION_MS = 600 * 1000;

/**
* Persistent listener for Slack Socket Mode.
* Handles cross-instance coordination via Redis pub/sub.
*/
const slackSocketMode = createPersistentListener({
name: "slack-socket-mode",
redisUrl: process.env.REDIS_URL,
defaultDurationMs: DEFAULT_DURATION_MS,
maxDurationMs: DEFAULT_DURATION_MS,
});

/**
* Start the Slack Socket Mode WebSocket listener.
*
* This endpoint is invoked by a Vercel cron job every 9 minutes to maintain
* continuous Socket Mode connectivity. Events are acked immediately and
* forwarded via HTTP POST to the existing webhook endpoint.
*
* Security: Requires CRON_SECRET validation.
*
* Usage: GET /api/slack/socket-mode
* Optional query param: ?duration=600000 (milliseconds, max 600000)
*/
export async function GET(request: Request): Promise<Response> {
const cronSecret = process.env.CRON_SECRET;
if (!cronSecret) {
console.error("[slack-socket-mode] CRON_SECRET not configured");
return new Response("CRON_SECRET not configured", { status: 500 });
}
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${cronSecret}`) {
console.log("[slack-socket-mode] Unauthorized: invalid CRON_SECRET");
return new Response("Unauthorized", { status: 401 });
}

await bot.initialize();

const slack = bot.getAdapter("slack");
if (!slack) {
console.log("[slack-socket-mode] Slack adapter not configured");
return new Response("Slack adapter not configured", { status: 404 });
}

// Construct webhook URL for forwarding socket events
const baseUrl =
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
process.env.VERCEL_URL ||
process.env.NEXT_PUBLIC_BASE_URL;
let webhookUrl: string | undefined;
if (baseUrl) {
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
const queryParam = bypassSecret
? `?x-vercel-protection-bypass=${bypassSecret}`
: "";
webhookUrl = `https://${baseUrl}/api/webhooks/slack${queryParam}`;
}

return slackSocketMode.start(request, {
afterTask: (task) => after(() => task),
run: async ({ abortSignal, durationMs, listenerId }) => {
console.log(
`[slack-socket-mode] Starting Socket Mode listener: ${listenerId}`,
{
webhookUrl: webhookUrl ? "configured" : "not configured",
durationMs,
}
);

const response = await slack.startSocketModeListener(
{ waitUntil: (task: Promise<unknown>) => after(() => task) },
durationMs,
abortSignal,
webhookUrl
);

console.log(
`[slack-socket-mode] Socket Mode listener ${listenerId} completed with status: ${response.status}`
);

return response;
},
});
}
4 changes: 4 additions & 0 deletions examples/nextjs-chat/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
{
"path": "/api/discord/gateway",
"schedule": "*/9 * * * *"
},
{
"path": "/api/slack/socket-mode",
"schedule": "*/9 * * * *"
}
]
}
90 changes: 88 additions & 2 deletions packages/adapter-slack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,86 @@ openssl rand -base64 32

When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently.

## Socket mode

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.

```typescript
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";

const bot = new Chat({
userName: "mybot",
adapters: {
slack: createSlackAdapter({
mode: "socket",
appToken: process.env.SLACK_APP_TOKEN!,
botToken: process.env.SLACK_BOT_TOKEN!,
}),
},
});
```

### Slack app setup for socket mode

1. Go to your app's settings at [api.slack.com/apps](https://api.slack.com/apps)
2. Navigate to **Socket Mode** and enable it
3. Generate an **App-Level Token** with the `connections:write` scope — this is your `SLACK_APP_TOKEN` (`xapp-...`)
4. Event subscriptions and interactivity still need to be configured, but no public request URL is required

> Socket mode is not compatible with multi-workspace OAuth (`clientId`/`clientSecret`). It's designed for single-workspace deployments.

### Socket mode on serverless (Vercel)

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:

1. A cron job periodically starts a transient socket listener
2. The listener connects via WebSocket, acks events immediately, and forwards them as HTTP requests to your webhook endpoint
3. Your existing webhook route processes the forwarded events normally

```typescript
// api/slack/socket-mode/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export const maxDuration = 800;

export async function GET(request: Request) {
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}

await bot.initialize();

const slack = bot.getAdapter("slack");
const webhookUrl = `https://${process.env.VERCEL_URL}/api/webhooks/slack`;

return slack.startSocketModeListener(
{ waitUntil: (task: Promise<unknown>) => after(() => task) },
600_000, // 10 minutes
undefined,
webhookUrl
);
}
```

Schedule the cron job to run every 9 minutes (overlapping with the 10-minute listener duration) to maintain continuous coverage:

```json
// vercel.json
{
"crons": [
{
"path": "/api/slack/socket-mode",
"schedule": "*/9 * * * *"
}
]
}
```

Forwarded events are authenticated using the `socketForwardingSecret` config option (defaults to `SLACK_SOCKET_FORWARDING_SECRET` env var, falling back to `appToken`).

## Slack app setup

### 1. Create a Slack app from manifest
Expand Down Expand Up @@ -188,19 +268,25 @@ All options are auto-detected from environment variables when not provided. You
|--------|----------|-------------|
| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
| `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
| `mode` | No | Connection mode: `"webhook"` (default) or `"socket"` |
| `appToken` | No** | App-level token (`xapp-...`) for socket mode. Auto-detected from `SLACK_APP_TOKEN` |
| `socketForwardingSecret` | No | Shared secret for authenticating forwarded socket events. Auto-detected from `SLACK_SOCKET_FORWARDING_SECRET`, falls back to `appToken` |
| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |

*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
*`signingSecret` is required for webhook mode — either via config or `SLACK_SIGNING_SECRET` env var.
**`appToken` is required for socket mode — either via config or `SLACK_APP_TOKEN` env var.

## Environment variables

```bash
SLACK_BOT_TOKEN=xoxb-... # Single-workspace only
SLACK_SIGNING_SECRET=...
SLACK_SIGNING_SECRET=... # Required for webhook mode
SLACK_APP_TOKEN=xapp-... # Required for socket mode
SLACK_SOCKET_FORWARDING_SECRET=... # Optional, for socket event forwarding auth
SLACK_CLIENT_ID=... # Multi-workspace only
SLACK_CLIENT_SECRET=... # Multi-workspace only
SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-slack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"dependencies": {
"@chat-adapter/shared": "workspace:*",
"@slack/socket-mode": "^2.0.5",
"@slack/web-api": "^7.14.0",
"chat": "workspace:*"
},
Expand Down
Loading
Loading