@chat-adapter/slack 4.13.1 → 4.13.3
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 +9 -195
- package/dist/index.d.ts +39 -39
- package/dist/index.js +66 -39
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @chat-adapter/slack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@chat-adapter/slack)
|
|
4
|
+
[](https://www.npmjs.com/package/@chat-adapter/slack)
|
|
5
|
+
|
|
6
|
+
Slack adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports single-workspace and multi-workspace OAuth deployments.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -8,13 +11,13 @@ Slack adapter for the [chat](https://github.com/vercel-labs/chat) SDK.
|
|
|
8
11
|
npm install chat @chat-adapter/slack
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
## Usage
|
|
14
|
+
## Usage
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
14
17
|
import { Chat } from "chat";
|
|
15
18
|
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
16
19
|
|
|
17
|
-
const
|
|
20
|
+
const bot = new Chat({
|
|
18
21
|
userName: "mybot",
|
|
19
22
|
adapters: {
|
|
20
23
|
slack: createSlackAdapter({
|
|
@@ -24,203 +27,14 @@ const chat = new Chat({
|
|
|
24
27
|
},
|
|
25
28
|
});
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
chat.onNewMention(async (thread, message) => {
|
|
30
|
+
bot.onNewMention(async (thread, message) => {
|
|
29
31
|
await thread.post("Hello from Slack!");
|
|
30
32
|
});
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
##
|
|
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
|
-
|
|
118
|
-
## Configuration
|
|
119
|
-
|
|
120
|
-
| Option | Required | Description |
|
|
121
|
-
|--------|----------|-------------|
|
|
122
|
-
| `botToken` | No | Slack bot token (`xoxb-...`). Required for single-workspace mode. Omit for multi-workspace. |
|
|
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 |
|
|
127
|
-
|
|
128
|
-
## Environment Variables
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
SLACK_BOT_TOKEN=xoxb-... # single-workspace only
|
|
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
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Slack App Setup
|
|
139
|
-
|
|
140
|
-
### 1. Create a Slack App
|
|
141
|
-
|
|
142
|
-
1. Go to [api.slack.com/apps](https://api.slack.com/apps)
|
|
143
|
-
2. Click **Create New App** → **From scratch**
|
|
144
|
-
3. Enter app name and select workspace
|
|
145
|
-
4. Click **Create App**
|
|
146
|
-
|
|
147
|
-
### 2. Configure Bot Token Scopes
|
|
148
|
-
|
|
149
|
-
1. Go to **OAuth & Permissions** in the sidebar
|
|
150
|
-
2. Under **Scopes** → **Bot Token Scopes**, add:
|
|
151
|
-
- `app_mentions:read` - Receive @mention events
|
|
152
|
-
- `channels:history` - Read messages in public channels
|
|
153
|
-
- `channels:read` - View basic channel info
|
|
154
|
-
- `chat:write` - Send messages
|
|
155
|
-
- `groups:history` - Read messages in private channels
|
|
156
|
-
- `groups:read` - View basic private channel info
|
|
157
|
-
- `im:history` - Read direct messages
|
|
158
|
-
- `im:read` - View basic DM info
|
|
159
|
-
- `reactions:read` - View emoji reactions
|
|
160
|
-
- `reactions:write` - Add/remove emoji reactions
|
|
161
|
-
- `users:read` - View user info (for display names)
|
|
162
|
-
|
|
163
|
-
### 3. Install App to Workspace
|
|
164
|
-
|
|
165
|
-
**Single workspace:** Install directly from the Slack dashboard.
|
|
166
|
-
|
|
167
|
-
1. Go to **OAuth & Permissions**
|
|
168
|
-
2. Click **Install to Workspace**
|
|
169
|
-
3. Authorize the app
|
|
170
|
-
4. Copy the **Bot User OAuth Token** (starts with `xoxb-`) → `SLACK_BOT_TOKEN`
|
|
171
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
1. Go to **Basic Information**
|
|
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)
|
|
181
|
-
|
|
182
|
-
### 5. Configure Event Subscriptions
|
|
183
|
-
|
|
184
|
-
1. Go to **Event Subscriptions**
|
|
185
|
-
2. Toggle **Enable Events** to On
|
|
186
|
-
3. Set **Request URL** to: `https://your-domain.com/api/webhooks/slack`
|
|
187
|
-
- Slack will verify the URL immediately
|
|
188
|
-
4. Under **Subscribe to bot events**, add:
|
|
189
|
-
- `app_mention` - When someone @mentions your bot
|
|
190
|
-
- `message.channels` - Messages in public channels
|
|
191
|
-
- `message.groups` - Messages in private channels
|
|
192
|
-
- `message.im` - Direct messages
|
|
193
|
-
5. Click **Save Changes**
|
|
194
|
-
|
|
195
|
-
### 6. (Optional) Enable Interactivity
|
|
196
|
-
|
|
197
|
-
If you want to use buttons, modals, or other interactive components:
|
|
198
|
-
|
|
199
|
-
1. Go to **Interactivity & Shortcuts**
|
|
200
|
-
2. Toggle **Interactivity** to On
|
|
201
|
-
3. Set **Request URL** to: `https://your-domain.com/api/webhooks/slack`
|
|
202
|
-
|
|
203
|
-
## Features
|
|
204
|
-
|
|
205
|
-
- Multi-workspace support with OAuth V2 and encrypted token storage
|
|
206
|
-
- Message posting and editing
|
|
207
|
-
- Thread subscriptions
|
|
208
|
-
- Reaction handling (add/remove/events)
|
|
209
|
-
- File attachments
|
|
210
|
-
- Rich cards (Block Kit)
|
|
211
|
-
- Action callbacks (interactive components)
|
|
212
|
-
- Direct messages
|
|
213
|
-
|
|
214
|
-
## Troubleshooting
|
|
215
|
-
|
|
216
|
-
### "Invalid signature" error
|
|
217
|
-
- Verify `SLACK_SIGNING_SECRET` is correct
|
|
218
|
-
- Check that the request timestamp is within 5 minutes (clock sync issue)
|
|
35
|
+
## Documentation
|
|
219
36
|
|
|
220
|
-
|
|
221
|
-
- Verify Event Subscriptions are configured
|
|
222
|
-
- Check that the bot has been added to the channel
|
|
223
|
-
- Ensure the webhook URL is correct and accessible
|
|
37
|
+
Full setup instructions, configuration reference, and features at [chat-sdk.dev/docs/adapters/slack](https://chat-sdk.dev/docs/adapters/slack).
|
|
224
38
|
|
|
225
39
|
## License
|
|
226
40
|
|
package/dist/index.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger,
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
interface SlackBlock {
|
|
11
|
-
type: string;
|
|
12
11
|
block_id?: string;
|
|
12
|
+
type: string;
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -23,8 +23,8 @@ declare function cardToBlockKit(card: CardElement): SlackBlock[];
|
|
|
23
23
|
declare function cardToFallbackText(card: CardElement): string;
|
|
24
24
|
|
|
25
25
|
interface EncryptedTokenData {
|
|
26
|
-
iv: string;
|
|
27
26
|
data: string;
|
|
27
|
+
iv: string;
|
|
28
28
|
tag: string;
|
|
29
29
|
}
|
|
30
30
|
declare function decodeKey(rawKey: string): Buffer;
|
|
@@ -65,23 +65,23 @@ declare class SlackFormatConverter extends BaseFormatConverter {
|
|
|
65
65
|
interface SlackAdapterConfig {
|
|
66
66
|
/** Bot token (xoxb-...). Required for single-workspace mode. Omit for multi-workspace. */
|
|
67
67
|
botToken?: string;
|
|
68
|
-
/** Signing secret for webhook verification */
|
|
69
|
-
signingSecret: string;
|
|
70
|
-
/** Logger instance for error reporting */
|
|
71
|
-
logger: Logger;
|
|
72
|
-
/** Override bot username (optional) */
|
|
73
|
-
userName?: string;
|
|
74
68
|
/** Bot user ID (will be fetched if not provided) */
|
|
75
69
|
botUserId?: string;
|
|
70
|
+
/** Slack app client ID (required for OAuth / multi-workspace) */
|
|
71
|
+
clientId?: string;
|
|
72
|
+
/** Slack app client secret (required for OAuth / multi-workspace) */
|
|
73
|
+
clientSecret?: string;
|
|
76
74
|
/**
|
|
77
75
|
* Base64-encoded 32-byte AES-256-GCM encryption key.
|
|
78
76
|
* If provided, bot tokens stored via setInstallation() will be encrypted at rest.
|
|
79
77
|
*/
|
|
80
78
|
encryptionKey?: string;
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
|
|
79
|
+
/** Logger instance for error reporting */
|
|
80
|
+
logger: Logger;
|
|
81
|
+
/** Signing secret for webhook verification */
|
|
82
|
+
signingSecret: string;
|
|
83
|
+
/** Override bot username (optional) */
|
|
84
|
+
userName?: string;
|
|
85
85
|
}
|
|
86
86
|
/** Data stored per Slack workspace installation */
|
|
87
87
|
interface SlackInstallation {
|
|
@@ -96,20 +96,13 @@ interface SlackThreadId {
|
|
|
96
96
|
}
|
|
97
97
|
/** Slack event payload (raw message format) */
|
|
98
98
|
interface SlackEvent {
|
|
99
|
-
type: string;
|
|
100
|
-
user?: string;
|
|
101
99
|
bot_id?: string;
|
|
102
100
|
channel?: string;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
thread_ts?: string;
|
|
106
|
-
subtype?: string;
|
|
107
|
-
username?: string;
|
|
101
|
+
/** Channel type: "channel", "group", "mpim", or "im" (DM) */
|
|
102
|
+
channel_type?: string;
|
|
108
103
|
edited?: {
|
|
109
104
|
ts: string;
|
|
110
105
|
};
|
|
111
|
-
/** Channel type: "channel", "group", "mpim", or "im" (DM) */
|
|
112
|
-
channel_type?: string;
|
|
113
106
|
files?: Array<{
|
|
114
107
|
id?: string;
|
|
115
108
|
mimetype?: string;
|
|
@@ -119,42 +112,49 @@ interface SlackEvent {
|
|
|
119
112
|
original_w?: number;
|
|
120
113
|
original_h?: number;
|
|
121
114
|
}>;
|
|
122
|
-
team?: string;
|
|
123
|
-
team_id?: string;
|
|
124
|
-
/** Number of replies in the thread (present on thread parent messages) */
|
|
125
|
-
reply_count?: number;
|
|
126
115
|
/** Timestamp of the latest reply (present on thread parent messages) */
|
|
127
116
|
latest_reply?: string;
|
|
117
|
+
/** Number of replies in the thread (present on thread parent messages) */
|
|
118
|
+
reply_count?: number;
|
|
119
|
+
subtype?: string;
|
|
120
|
+
team?: string;
|
|
121
|
+
team_id?: string;
|
|
122
|
+
text?: string;
|
|
123
|
+
thread_ts?: string;
|
|
124
|
+
ts?: string;
|
|
125
|
+
type: string;
|
|
126
|
+
user?: string;
|
|
127
|
+
username?: string;
|
|
128
128
|
}
|
|
129
129
|
/** Slack reaction event payload */
|
|
130
130
|
interface SlackReactionEvent {
|
|
131
|
-
|
|
132
|
-
user: string;
|
|
133
|
-
reaction: string;
|
|
134
|
-
item_user?: string;
|
|
131
|
+
event_ts: string;
|
|
135
132
|
item: {
|
|
136
133
|
type: string;
|
|
137
134
|
channel: string;
|
|
138
135
|
ts: string;
|
|
139
136
|
};
|
|
140
|
-
|
|
137
|
+
item_user?: string;
|
|
138
|
+
reaction: string;
|
|
139
|
+
type: "reaction_added" | "reaction_removed";
|
|
140
|
+
user: string;
|
|
141
141
|
}
|
|
142
142
|
declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
143
143
|
readonly name = "slack";
|
|
144
144
|
readonly userName: string;
|
|
145
|
-
private client;
|
|
146
|
-
private signingSecret;
|
|
147
|
-
private defaultBotToken;
|
|
145
|
+
private readonly client;
|
|
146
|
+
private readonly signingSecret;
|
|
147
|
+
private readonly defaultBotToken;
|
|
148
148
|
private chat;
|
|
149
|
-
private logger;
|
|
149
|
+
private readonly logger;
|
|
150
150
|
private _botUserId;
|
|
151
151
|
private _botId;
|
|
152
|
-
private formatConverter;
|
|
152
|
+
private readonly formatConverter;
|
|
153
153
|
private static USER_CACHE_TTL_MS;
|
|
154
|
-
private clientId;
|
|
155
|
-
private clientSecret;
|
|
156
|
-
private encryptionKey;
|
|
157
|
-
private requestContext;
|
|
154
|
+
private readonly clientId;
|
|
155
|
+
private readonly clientSecret;
|
|
156
|
+
private readonly encryptionKey;
|
|
157
|
+
private readonly requestContext;
|
|
158
158
|
/** Bot user ID (e.g., U_BOT_123) used for mention detection */
|
|
159
159
|
get botUserId(): string | undefined;
|
|
160
160
|
constructor(config: SlackAdapterConfig);
|
package/dist/index.js
CHANGED
|
@@ -263,6 +263,7 @@ import crypto from "crypto";
|
|
|
263
263
|
var ALGORITHM = "aes-256-gcm";
|
|
264
264
|
var IV_LENGTH = 12;
|
|
265
265
|
var AUTH_TAG_LENGTH = 16;
|
|
266
|
+
var HEX_KEY_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
266
267
|
function encryptToken(plaintext, key) {
|
|
267
268
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
268
269
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
@@ -293,13 +294,15 @@ function decryptToken(encrypted, key) {
|
|
|
293
294
|
]).toString("utf8");
|
|
294
295
|
}
|
|
295
296
|
function isEncryptedTokenData(value) {
|
|
296
|
-
if (!value || typeof value !== "object")
|
|
297
|
+
if (!value || typeof value !== "object") {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
297
300
|
const obj = value;
|
|
298
301
|
return typeof obj.iv === "string" && typeof obj.data === "string" && typeof obj.tag === "string";
|
|
299
302
|
}
|
|
300
303
|
function decodeKey(rawKey) {
|
|
301
304
|
const trimmed = rawKey.trim();
|
|
302
|
-
const isHex =
|
|
305
|
+
const isHex = HEX_KEY_PATTERN.test(trimmed);
|
|
303
306
|
const key = Buffer.from(trimmed, isHex ? "hex" : "base64");
|
|
304
307
|
if (key.length !== 32) {
|
|
305
308
|
throw new Error(
|
|
@@ -437,11 +440,15 @@ ${node.value}
|
|
|
437
440
|
|
|
438
441
|
// src/modals.ts
|
|
439
442
|
function encodeModalMetadata(meta) {
|
|
440
|
-
if (!meta.contextId
|
|
443
|
+
if (!(meta.contextId || meta.privateMetadata)) {
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
441
446
|
return JSON.stringify({ c: meta.contextId, m: meta.privateMetadata });
|
|
442
447
|
}
|
|
443
448
|
function decodeModalMetadata(raw) {
|
|
444
|
-
if (!raw)
|
|
449
|
+
if (!raw) {
|
|
450
|
+
return {};
|
|
451
|
+
}
|
|
445
452
|
try {
|
|
446
453
|
const parsed = JSON.parse(raw);
|
|
447
454
|
if (typeof parsed === "object" && parsed !== null && ("c" in parsed || "m" in parsed)) {
|
|
@@ -478,6 +485,10 @@ function modalChildToBlock(child) {
|
|
|
478
485
|
return convertTextToBlock(child);
|
|
479
486
|
case "fields":
|
|
480
487
|
return convertFieldsToBlock(child);
|
|
488
|
+
default:
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Unknown modal child type: ${child.type}`
|
|
491
|
+
);
|
|
481
492
|
}
|
|
482
493
|
}
|
|
483
494
|
function textInputToBlock(input) {
|
|
@@ -595,7 +606,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
595
606
|
/** Bot user ID (e.g., U_BOT_123) used for mention detection */
|
|
596
607
|
get botUserId() {
|
|
597
608
|
const ctx = this.requestContext.getStore();
|
|
598
|
-
if (ctx?.botUserId)
|
|
609
|
+
if (ctx?.botUserId) {
|
|
610
|
+
return ctx.botUserId;
|
|
611
|
+
}
|
|
599
612
|
return this._botUserId || void 0;
|
|
600
613
|
}
|
|
601
614
|
constructor(config) {
|
|
@@ -617,8 +630,12 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
617
630
|
*/
|
|
618
631
|
getToken() {
|
|
619
632
|
const ctx = this.requestContext.getStore();
|
|
620
|
-
if (ctx?.token)
|
|
621
|
-
|
|
633
|
+
if (ctx?.token) {
|
|
634
|
+
return ctx.token;
|
|
635
|
+
}
|
|
636
|
+
if (this.defaultBotToken) {
|
|
637
|
+
return this.defaultBotToken;
|
|
638
|
+
}
|
|
622
639
|
throw new ChatError(
|
|
623
640
|
"No bot token available. In multi-workspace mode, ensure the webhook is being processed.",
|
|
624
641
|
"MISSING_BOT_TOKEN"
|
|
@@ -696,7 +713,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
696
713
|
const state = this.chat.getState();
|
|
697
714
|
const key = this.installationKey(teamId);
|
|
698
715
|
const stored = await state.get(key);
|
|
699
|
-
if (!stored)
|
|
716
|
+
if (!stored) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
700
719
|
if (this.encryptionKey && isEncryptedTokenData(stored.botToken)) {
|
|
701
720
|
return {
|
|
702
721
|
...stored,
|
|
@@ -714,7 +733,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
714
733
|
* exchanges it for tokens, and saves the installation.
|
|
715
734
|
*/
|
|
716
735
|
async handleOAuthCallback(request) {
|
|
717
|
-
if (!this.clientId
|
|
736
|
+
if (!(this.clientId && this.clientSecret)) {
|
|
718
737
|
throw new ChatError(
|
|
719
738
|
"clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter().",
|
|
720
739
|
"MISSING_OAUTH_CONFIG"
|
|
@@ -735,7 +754,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
735
754
|
code,
|
|
736
755
|
redirect_uri: redirectUri
|
|
737
756
|
});
|
|
738
|
-
if (!result.ok
|
|
757
|
+
if (!(result.ok && result.access_token && result.team?.id)) {
|
|
739
758
|
throw new ChatError(
|
|
740
759
|
`Slack OAuth failed: ${result.error || "missing access_token or team.id"}`,
|
|
741
760
|
"OAUTH_FAILED"
|
|
@@ -803,7 +822,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
803
822
|
try {
|
|
804
823
|
const params = new URLSearchParams(body);
|
|
805
824
|
const payloadStr = params.get("payload");
|
|
806
|
-
if (!payloadStr)
|
|
825
|
+
if (!payloadStr) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
807
828
|
const payload = JSON.parse(payloadStr);
|
|
808
829
|
return payload.team?.id || payload.team_id || null;
|
|
809
830
|
} catch {
|
|
@@ -894,9 +915,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
894
915
|
return new Response("Invalid JSON", { status: 400 });
|
|
895
916
|
}
|
|
896
917
|
if (payload.type === "url_verification" && payload.challenge) {
|
|
897
|
-
return
|
|
898
|
-
headers: { "Content-Type": "application/json" }
|
|
899
|
-
});
|
|
918
|
+
return Response.json({ challenge: payload.challenge });
|
|
900
919
|
}
|
|
901
920
|
if (!this.defaultBotToken && payload.type === "event_callback") {
|
|
902
921
|
const teamId = payload.team_id;
|
|
@@ -921,7 +940,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
921
940
|
const event = payload.event;
|
|
922
941
|
if (event.type === "message" || event.type === "app_mention") {
|
|
923
942
|
const slackEvent = event;
|
|
924
|
-
if (!slackEvent.team
|
|
943
|
+
if (!(slackEvent.team || slackEvent.team_id) && payload.team_id) {
|
|
925
944
|
slackEvent.team_id = payload.team_id;
|
|
926
945
|
}
|
|
927
946
|
this.handleMessageEvent(slackEvent, options);
|
|
@@ -1023,7 +1042,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1023
1042
|
const messageTs = payload.message?.ts || payload.container?.message_ts;
|
|
1024
1043
|
const threadTs = payload.message?.thread_ts || payload.container?.thread_ts || messageTs;
|
|
1025
1044
|
const isViewAction = payload.container?.type === "view";
|
|
1026
|
-
if (!isViewAction
|
|
1045
|
+
if (!(isViewAction || channel)) {
|
|
1027
1046
|
this.logger.warn("Missing channel in block_actions", { channel });
|
|
1028
1047
|
return;
|
|
1029
1048
|
}
|
|
@@ -1176,11 +1195,11 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1176
1195
|
return modal;
|
|
1177
1196
|
}
|
|
1178
1197
|
verifySignature(body, timestamp, signature) {
|
|
1179
|
-
if (!timestamp
|
|
1198
|
+
if (!(timestamp && signature)) {
|
|
1180
1199
|
return false;
|
|
1181
1200
|
}
|
|
1182
1201
|
const now = Math.floor(Date.now() / 1e3);
|
|
1183
|
-
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
|
|
1202
|
+
if (Math.abs(now - Number.parseInt(timestamp, 10)) > 300) {
|
|
1184
1203
|
return false;
|
|
1185
1204
|
}
|
|
1186
1205
|
const sigBasestring = `v0:${timestamp}:${body}`;
|
|
@@ -1209,7 +1228,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1209
1228
|
});
|
|
1210
1229
|
return;
|
|
1211
1230
|
}
|
|
1212
|
-
if (!event.channel
|
|
1231
|
+
if (!(event.channel && event.ts)) {
|
|
1213
1232
|
this.logger.debug("Ignoring event without channel or ts", {
|
|
1214
1233
|
channel: event.channel,
|
|
1215
1234
|
ts: event.ts
|
|
@@ -1443,11 +1462,15 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1443
1462
|
userIds.add(match[1]);
|
|
1444
1463
|
match = mentionPattern.exec(text);
|
|
1445
1464
|
}
|
|
1446
|
-
if (userIds.size === 0)
|
|
1465
|
+
if (userIds.size === 0) {
|
|
1466
|
+
return text;
|
|
1467
|
+
}
|
|
1447
1468
|
if (skipSelfMention && this._botUserId) {
|
|
1448
1469
|
userIds.delete(this._botUserId);
|
|
1449
1470
|
}
|
|
1450
|
-
if (userIds.size === 0)
|
|
1471
|
+
if (userIds.size === 0) {
|
|
1472
|
+
return text;
|
|
1473
|
+
}
|
|
1451
1474
|
const lookups = await Promise.all(
|
|
1452
1475
|
[...userIds].map(async (uid) => {
|
|
1453
1476
|
const info = await this.lookupUser(uid);
|
|
@@ -1486,9 +1509,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1486
1509
|
isMe
|
|
1487
1510
|
},
|
|
1488
1511
|
metadata: {
|
|
1489
|
-
dateSent: new Date(parseFloat(event.ts || "0") * 1e3),
|
|
1512
|
+
dateSent: new Date(Number.parseFloat(event.ts || "0") * 1e3),
|
|
1490
1513
|
edited: !!event.edited,
|
|
1491
|
-
editedAt: event.edited ? new Date(parseFloat(event.edited.ts) * 1e3) : void 0
|
|
1514
|
+
editedAt: event.edited ? new Date(Number.parseFloat(event.edited.ts) * 1e3) : void 0
|
|
1492
1515
|
},
|
|
1493
1516
|
attachments: (event.files || []).map(
|
|
1494
1517
|
(file) => this.createAttachment(file)
|
|
@@ -1543,7 +1566,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1543
1566
|
await this.uploadFiles(files, channel, threadTs || void 0);
|
|
1544
1567
|
const hasText = typeof message === "string" || typeof message === "object" && message !== null && ("raw" in message || "markdown" in message || "ast" in message);
|
|
1545
1568
|
const card2 = extractCard(message);
|
|
1546
|
-
if (!hasText
|
|
1569
|
+
if (!(hasText || card2)) {
|
|
1547
1570
|
return {
|
|
1548
1571
|
id: `file-${Date.now()}`,
|
|
1549
1572
|
threadId,
|
|
@@ -1921,7 +1944,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1921
1944
|
* Requires `recipientUserId` and `recipientTeamId` in options.
|
|
1922
1945
|
*/
|
|
1923
1946
|
async stream(threadId, textStream, options) {
|
|
1924
|
-
if (!options?.recipientUserId
|
|
1947
|
+
if (!(options?.recipientUserId && options?.recipientTeamId)) {
|
|
1925
1948
|
throw new ChatError(
|
|
1926
1949
|
"Slack streaming requires recipientUserId and recipientTeamId in options",
|
|
1927
1950
|
"MISSING_STREAM_OPTIONS"
|
|
@@ -1993,7 +2016,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1993
2016
|
const limit = options.limit || 100;
|
|
1994
2017
|
try {
|
|
1995
2018
|
if (direction === "forward") {
|
|
1996
|
-
return this.fetchMessagesForward(
|
|
2019
|
+
return await this.fetchMessagesForward(
|
|
1997
2020
|
channel,
|
|
1998
2021
|
threadTs,
|
|
1999
2022
|
threadId,
|
|
@@ -2001,7 +2024,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2001
2024
|
options.cursor
|
|
2002
2025
|
);
|
|
2003
2026
|
}
|
|
2004
|
-
return this.fetchMessagesBackward(
|
|
2027
|
+
return await this.fetchMessagesBackward(
|
|
2005
2028
|
channel,
|
|
2006
2029
|
threadTs,
|
|
2007
2030
|
threadId,
|
|
@@ -2141,7 +2164,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2141
2164
|
);
|
|
2142
2165
|
const messages = result.messages || [];
|
|
2143
2166
|
const target = messages.find((msg) => msg.ts === messageId);
|
|
2144
|
-
if (!target)
|
|
2167
|
+
if (!target) {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2145
2170
|
return this.parseSlackMessage(target, threadId);
|
|
2146
2171
|
} catch (error) {
|
|
2147
2172
|
this.handleSlackError(error);
|
|
@@ -2203,9 +2228,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2203
2228
|
isMe
|
|
2204
2229
|
},
|
|
2205
2230
|
metadata: {
|
|
2206
|
-
dateSent: new Date(parseFloat(event.ts || "0") * 1e3),
|
|
2231
|
+
dateSent: new Date(Number.parseFloat(event.ts || "0") * 1e3),
|
|
2207
2232
|
edited: !!event.edited,
|
|
2208
|
-
editedAt: event.edited ? new Date(parseFloat(event.edited.ts) * 1e3) : void 0
|
|
2233
|
+
editedAt: event.edited ? new Date(Number.parseFloat(event.edited.ts) * 1e3) : void 0
|
|
2209
2234
|
},
|
|
2210
2235
|
attachments: (event.files || []).map(
|
|
2211
2236
|
(file) => this.createAttachment(file)
|
|
@@ -2279,7 +2304,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2279
2304
|
);
|
|
2280
2305
|
let nextCursor;
|
|
2281
2306
|
if (result.has_more && slackMessages.length > 0) {
|
|
2282
|
-
const newest = slackMessages
|
|
2307
|
+
const newest = slackMessages.at(-1);
|
|
2283
2308
|
if (newest?.ts) {
|
|
2284
2309
|
nextCursor = newest.ts;
|
|
2285
2310
|
}
|
|
@@ -2402,7 +2427,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2402
2427
|
return {
|
|
2403
2428
|
id: channelId,
|
|
2404
2429
|
name: info?.name ? `#${info.name}` : void 0,
|
|
2405
|
-
isDM: info?.is_im || info?.is_mpim
|
|
2430
|
+
isDM: Boolean(info?.is_im || info?.is_mpim),
|
|
2406
2431
|
memberCount: info?.num_members,
|
|
2407
2432
|
metadata: {
|
|
2408
2433
|
purpose: info?.purpose?.value,
|
|
@@ -2425,7 +2450,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2425
2450
|
);
|
|
2426
2451
|
}
|
|
2427
2452
|
const syntheticThreadId = `slack:${channel}:`;
|
|
2428
|
-
return this.postMessage(syntheticThreadId, message);
|
|
2453
|
+
return await this.postMessage(syntheticThreadId, message);
|
|
2429
2454
|
}
|
|
2430
2455
|
renderFormatted(content) {
|
|
2431
2456
|
return this.formatConverter.fromAst(content);
|
|
@@ -2456,10 +2481,8 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2456
2481
|
}
|
|
2457
2482
|
handleSlackError(error) {
|
|
2458
2483
|
const slackError = error;
|
|
2459
|
-
if (slackError.code === "slack_webapi_platform_error") {
|
|
2460
|
-
|
|
2461
|
-
throw new AdapterRateLimitError("slack");
|
|
2462
|
-
}
|
|
2484
|
+
if (slackError.code === "slack_webapi_platform_error" && slackError.data?.error === "ratelimited") {
|
|
2485
|
+
throw new AdapterRateLimitError("slack");
|
|
2463
2486
|
}
|
|
2464
2487
|
throw error;
|
|
2465
2488
|
}
|
|
@@ -2476,9 +2499,13 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2476
2499
|
* Returns null if the messageId is not an ephemeral encoding.
|
|
2477
2500
|
*/
|
|
2478
2501
|
decodeEphemeralMessageId(messageId) {
|
|
2479
|
-
if (!messageId.startsWith("ephemeral:"))
|
|
2502
|
+
if (!messageId.startsWith("ephemeral:")) {
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2480
2505
|
const parts = messageId.split(":");
|
|
2481
|
-
if (parts.length < 3)
|
|
2506
|
+
if (parts.length < 3) {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2482
2509
|
const messageTs = parts[1];
|
|
2483
2510
|
const encodedData = parts.slice(2).join(":");
|
|
2484
2511
|
try {
|