@cuylabs/channel-slack 0.1.0
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/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/activity-ByrD9Ftr.d.ts +66 -0
- package/dist/assistant.d.ts +58 -0
- package/dist/assistant.js +188 -0
- package/dist/bolt.d.ts +344 -0
- package/dist/bolt.js +705 -0
- package/dist/chunk-BODPT4I6.js +322 -0
- package/dist/chunk-FPCE5V5Y.js +292 -0
- package/dist/chunk-FX2JOVX5.js +405 -0
- package/dist/chunk-JZG4IETE.js +141 -0
- package/dist/chunk-NE57BLLU.js +0 -0
- package/dist/chunk-TWJGVDA2.js +108 -0
- package/dist/core.d.ts +425 -0
- package/dist/core.js +42 -0
- package/dist/diagnostics.d.ts +105 -0
- package/dist/diagnostics.js +8 -0
- package/dist/feedback.d.ts +137 -0
- package/dist/feedback.js +128 -0
- package/dist/history.d.ts +266 -0
- package/dist/history.js +747 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +57 -0
- package/dist/logging-Bl3HfcC8.d.ts +8 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +16 -0
- package/dist/setup.d.ts +165 -0
- package/dist/setup.js +453 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +43 -0
- package/dist/targets.d.ts +113 -0
- package/dist/targets.js +484 -0
- package/dist/users.d.ts +109 -0
- package/dist/users.js +240 -0
- package/docs/concepts/activity.md +33 -0
- package/docs/concepts/bolt-runtime.md +30 -0
- package/docs/concepts/message-policy.md +49 -0
- package/docs/concepts/setup-requirements.md +44 -0
- package/docs/concepts/supplemental-history.md +55 -0
- package/docs/recipes/app-mention-handler.md +34 -0
- package/docs/recipes/assistant-thread-handler.md +28 -0
- package/docs/recipes/generate-slack-manifest.md +28 -0
- package/docs/recipes/history-visibility.md +36 -0
- package/docs/recipes/socket-mode-app.md +29 -0
- package/docs/reference/channel-slack-boundary.md +50 -0
- package/docs/reference/exports.md +32 -0
- package/package.json +130 -0
package/dist/bolt.js
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
// src/bolt/auth.ts
|
|
2
|
+
function resolveDirectAuth(options) {
|
|
3
|
+
const provided = options.auth;
|
|
4
|
+
if (!provided || provided.mode === void 0 || provided.mode === "single-workspace") {
|
|
5
|
+
const singleWorkspace = provided;
|
|
6
|
+
const botToken = firstNonEmptyString(
|
|
7
|
+
singleWorkspace?.botToken,
|
|
8
|
+
options.botToken,
|
|
9
|
+
process.env.SLACK_BOT_TOKEN
|
|
10
|
+
);
|
|
11
|
+
if (!botToken) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Slack bot token is required for single-workspace mode. Pass `botToken`, use `auth: { mode: "oauth", ... }`, or use `auth: { mode: "authorize", ... }`.'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
mode: "single-workspace",
|
|
18
|
+
botToken,
|
|
19
|
+
botId: singleWorkspace?.botId,
|
|
20
|
+
botUserId: singleWorkspace?.botUserId
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (provided.mode === "authorize") {
|
|
24
|
+
return provided;
|
|
25
|
+
}
|
|
26
|
+
const oauth = provided;
|
|
27
|
+
const clientId = firstNonEmptyString(
|
|
28
|
+
oauth.clientId,
|
|
29
|
+
process.env.SLACK_CLIENT_ID
|
|
30
|
+
);
|
|
31
|
+
const clientSecret = firstNonEmptyString(
|
|
32
|
+
oauth.clientSecret,
|
|
33
|
+
process.env.SLACK_CLIENT_SECRET
|
|
34
|
+
);
|
|
35
|
+
if (!clientId || !clientSecret) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Slack OAuth mode requires `clientId` and `clientSecret` or SLACK_CLIENT_ID / SLACK_CLIENT_SECRET."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (oauth.stateVerification !== false && !trimToUndefined(oauth.stateSecret) && !oauth.stateStore && !trimToUndefined(process.env.SLACK_STATE_SECRET)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Slack OAuth mode requires `stateSecret` or a custom `stateStore` when state verification is enabled."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...oauth,
|
|
47
|
+
mode: "oauth",
|
|
48
|
+
clientId,
|
|
49
|
+
clientSecret,
|
|
50
|
+
stateSecret: firstNonEmptyString(
|
|
51
|
+
oauth.stateSecret,
|
|
52
|
+
process.env.SLACK_STATE_SECRET
|
|
53
|
+
)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function normalizeSlackEventsPath(path2) {
|
|
57
|
+
const trimmed = path2.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
throw new Error("Slack events path must not be empty.");
|
|
60
|
+
}
|
|
61
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
62
|
+
}
|
|
63
|
+
function trimToUndefined(value) {
|
|
64
|
+
const trimmed = value?.trim();
|
|
65
|
+
return trimmed || void 0;
|
|
66
|
+
}
|
|
67
|
+
function firstNonEmptyString(...values) {
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
const trimmed = trimToUndefined(value);
|
|
70
|
+
if (trimmed) {
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/bolt/app.ts
|
|
78
|
+
async function createSlackBoltApp(options = {}) {
|
|
79
|
+
const boltModule = await import("@slack/bolt");
|
|
80
|
+
const routePath = normalizeSlackEventsPath(options.path ?? "/slack/events");
|
|
81
|
+
const signingSecret = trimToUndefined(
|
|
82
|
+
options.signingSecret ?? process.env.SLACK_SIGNING_SECRET
|
|
83
|
+
);
|
|
84
|
+
if (!signingSecret) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Slack signing secret is required. Pass `signingSecret` or set SLACK_SIGNING_SECRET."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const auth = resolveDirectAuth(options);
|
|
90
|
+
const receiver = new boltModule.ExpressReceiver({
|
|
91
|
+
...options.receiverOptions,
|
|
92
|
+
signingSecret,
|
|
93
|
+
endpoints: routePath,
|
|
94
|
+
processBeforeResponse: options.processBeforeResponse,
|
|
95
|
+
signatureVerification: options.signatureVerification,
|
|
96
|
+
// Bolt is typed against Express 4 while this repo uses Express 5 types.
|
|
97
|
+
// The runtime contract is compatible; keep the cast localized here.
|
|
98
|
+
app: options.app,
|
|
99
|
+
...auth.mode === "oauth" ? {
|
|
100
|
+
clientId: auth.clientId,
|
|
101
|
+
clientSecret: auth.clientSecret,
|
|
102
|
+
stateSecret: auth.stateSecret,
|
|
103
|
+
installationStore: auth.installationStore,
|
|
104
|
+
redirectUri: auth.redirectUri,
|
|
105
|
+
scopes: auth.scopes,
|
|
106
|
+
installerOptions: {
|
|
107
|
+
stateStore: auth.stateStore,
|
|
108
|
+
stateVerification: auth.stateVerification,
|
|
109
|
+
legacyStateVerification: auth.legacyStateVerification,
|
|
110
|
+
stateCookieName: auth.stateCookieName,
|
|
111
|
+
stateCookieExpirationSeconds: auth.stateCookieExpirationSeconds,
|
|
112
|
+
metadata: auth.metadata,
|
|
113
|
+
userScopes: auth.userScopes,
|
|
114
|
+
installPath: auth.installPath,
|
|
115
|
+
redirectUriPath: auth.callbackPath,
|
|
116
|
+
renderHtmlForInstallPath: auth.renderHtmlForInstallPath,
|
|
117
|
+
installPathOptions: auth.installPathOptions,
|
|
118
|
+
callbackOptions: auth.callbackOptions,
|
|
119
|
+
directInstall: auth.directInstall,
|
|
120
|
+
authVersion: auth.authVersion,
|
|
121
|
+
authorizationUrl: auth.authorizationUrl
|
|
122
|
+
}
|
|
123
|
+
} : {}
|
|
124
|
+
});
|
|
125
|
+
const boltApp = new boltModule.App({
|
|
126
|
+
...options.boltAppOptions,
|
|
127
|
+
receiver,
|
|
128
|
+
...auth.mode === "single-workspace" ? {
|
|
129
|
+
token: auth.botToken,
|
|
130
|
+
botId: auth.botId,
|
|
131
|
+
botUserId: auth.botUserId
|
|
132
|
+
} : {},
|
|
133
|
+
...auth.mode === "authorize" ? { authorize: auth.authorize } : {}
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
boltApp,
|
|
137
|
+
receiver,
|
|
138
|
+
app: options.app ?? receiver.app,
|
|
139
|
+
authMode: auth.mode,
|
|
140
|
+
routePath
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/bolt/socket-app.ts
|
|
145
|
+
async function createSlackSocketBoltApp(options = {}) {
|
|
146
|
+
const appToken = trimToUndefined(
|
|
147
|
+
options.appToken ?? process.env.SLACK_APP_TOKEN
|
|
148
|
+
);
|
|
149
|
+
if (!appToken) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Slack app token is required for Socket Mode. Pass `appToken` or set SLACK_APP_TOKEN."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const auth = resolveDirectAuth({
|
|
155
|
+
auth: options.auth,
|
|
156
|
+
botToken: options.botToken
|
|
157
|
+
});
|
|
158
|
+
const boltModule = await import("@slack/bolt");
|
|
159
|
+
const receiverInstallerOptions = {
|
|
160
|
+
clientOptions: options.boltAppOptions?.clientOptions,
|
|
161
|
+
...auth.mode === "oauth" ? {
|
|
162
|
+
port: options.boltAppOptions?.port,
|
|
163
|
+
stateStore: auth.stateStore,
|
|
164
|
+
stateVerification: auth.stateVerification,
|
|
165
|
+
legacyStateVerification: auth.legacyStateVerification,
|
|
166
|
+
stateCookieName: auth.stateCookieName,
|
|
167
|
+
stateCookieExpirationSeconds: auth.stateCookieExpirationSeconds,
|
|
168
|
+
metadata: auth.metadata,
|
|
169
|
+
userScopes: auth.userScopes,
|
|
170
|
+
installPath: auth.installPath,
|
|
171
|
+
redirectUriPath: auth.callbackPath,
|
|
172
|
+
renderHtmlForInstallPath: auth.renderHtmlForInstallPath,
|
|
173
|
+
installPathOptions: auth.installPathOptions,
|
|
174
|
+
callbackOptions: auth.callbackOptions,
|
|
175
|
+
directInstall: auth.directInstall,
|
|
176
|
+
authVersion: auth.authVersion,
|
|
177
|
+
authorizationUrl: auth.authorizationUrl
|
|
178
|
+
} : {}
|
|
179
|
+
};
|
|
180
|
+
const receiver = new boltModule.SocketModeReceiver({
|
|
181
|
+
...options.socketModeReceiverOptions,
|
|
182
|
+
appToken,
|
|
183
|
+
customRoutes: options.socketModeReceiverOptions?.customRoutes ?? options.boltAppOptions?.customRoutes,
|
|
184
|
+
installerOptions: receiverInstallerOptions,
|
|
185
|
+
logger: options.boltAppOptions?.logger,
|
|
186
|
+
logLevel: options.boltAppOptions?.logLevel,
|
|
187
|
+
...auth.mode === "oauth" ? {
|
|
188
|
+
clientId: auth.clientId,
|
|
189
|
+
clientSecret: auth.clientSecret,
|
|
190
|
+
stateSecret: auth.stateSecret,
|
|
191
|
+
installationStore: auth.installationStore,
|
|
192
|
+
redirectUri: auth.redirectUri,
|
|
193
|
+
scopes: auth.scopes
|
|
194
|
+
} : {}
|
|
195
|
+
});
|
|
196
|
+
const boltApp = new boltModule.App({
|
|
197
|
+
...options.boltAppOptions,
|
|
198
|
+
receiver,
|
|
199
|
+
...auth.mode === "single-workspace" ? {
|
|
200
|
+
token: auth.botToken,
|
|
201
|
+
botId: auth.botId,
|
|
202
|
+
botUserId: auth.botUserId
|
|
203
|
+
} : {},
|
|
204
|
+
...auth.mode === "authorize" ? { authorize: auth.authorize } : {}
|
|
205
|
+
});
|
|
206
|
+
return { boltApp, authMode: auth.mode };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/bolt/socket-lock.ts
|
|
210
|
+
import crypto from "crypto";
|
|
211
|
+
import fs from "fs";
|
|
212
|
+
import os from "os";
|
|
213
|
+
import path from "path";
|
|
214
|
+
function acquireSlackSocketModeProcessLock({
|
|
215
|
+
appSlug,
|
|
216
|
+
appToken,
|
|
217
|
+
enabled = true,
|
|
218
|
+
lockDir,
|
|
219
|
+
logger
|
|
220
|
+
}) {
|
|
221
|
+
if (!enabled) {
|
|
222
|
+
logger?.debug?.("Slack Socket Mode process lock skipped", {
|
|
223
|
+
reason: "disabled"
|
|
224
|
+
});
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
if (!appToken?.trim()) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"Slack app token is required when the Slack Socket Mode process lock is enabled."
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const resolvedLockDir = lockDir ?? path.join(os.tmpdir(), "channel-slack");
|
|
233
|
+
const tokenHash = crypto.createHash("sha256").update(appToken).digest("hex").slice(0, 16);
|
|
234
|
+
const lockPath = path.join(
|
|
235
|
+
resolvedLockDir,
|
|
236
|
+
`${appSlug}-${tokenHash}.socket-mode.lock`
|
|
237
|
+
);
|
|
238
|
+
fs.mkdirSync(resolvedLockDir, { recursive: true, mode: 448 });
|
|
239
|
+
return createLockFile({ appSlug, lockPath, logger, tokenHash });
|
|
240
|
+
}
|
|
241
|
+
function createLockFile({
|
|
242
|
+
appSlug,
|
|
243
|
+
lockPath,
|
|
244
|
+
logger,
|
|
245
|
+
tokenHash
|
|
246
|
+
}) {
|
|
247
|
+
let fd = tryOpenLockFile({ lockPath, logger });
|
|
248
|
+
if (fd === void 0) {
|
|
249
|
+
removeStaleLockOrThrow({ lockPath, logger });
|
|
250
|
+
fd = tryOpenLockFile({ lockPath, logger });
|
|
251
|
+
}
|
|
252
|
+
if (fd === void 0) {
|
|
253
|
+
throw new Error(`Unable to acquire Slack Socket Mode lock at ${lockPath}`);
|
|
254
|
+
}
|
|
255
|
+
const record = {
|
|
256
|
+
appSlug,
|
|
257
|
+
hostname: os.hostname(),
|
|
258
|
+
pid: process.pid,
|
|
259
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
+
tokenHash
|
|
261
|
+
};
|
|
262
|
+
fs.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
|
|
263
|
+
`);
|
|
264
|
+
logger?.info?.("Slack Socket Mode process lock acquired", {
|
|
265
|
+
lockPath,
|
|
266
|
+
pid: record.pid
|
|
267
|
+
});
|
|
268
|
+
let released = false;
|
|
269
|
+
return {
|
|
270
|
+
path: lockPath,
|
|
271
|
+
release() {
|
|
272
|
+
if (released) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
released = true;
|
|
276
|
+
try {
|
|
277
|
+
fs.closeSync(fd);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
releaseLockFile({ lockPath, logger });
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function tryOpenLockFile({
|
|
285
|
+
lockPath,
|
|
286
|
+
logger
|
|
287
|
+
}) {
|
|
288
|
+
try {
|
|
289
|
+
return fs.openSync(lockPath, "wx", 384);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const code = error.code;
|
|
292
|
+
if (code === "EEXIST") {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
logger?.error("Failed to create Slack Socket Mode process lock", {
|
|
296
|
+
lockPath,
|
|
297
|
+
error: formatLockError(error)
|
|
298
|
+
});
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function removeStaleLockOrThrow({
|
|
303
|
+
lockPath,
|
|
304
|
+
logger
|
|
305
|
+
}) {
|
|
306
|
+
const record = readLockRecord(lockPath);
|
|
307
|
+
if (record?.pid && isProcessAlive(record.pid)) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Another Slack Socket Mode process appears to be active (pid=${record.pid}, lock=${lockPath})`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
logger?.warn?.("Removing stale Slack Socket Mode process lock", {
|
|
313
|
+
lockPath,
|
|
314
|
+
previousPid: record?.pid
|
|
315
|
+
});
|
|
316
|
+
fs.rmSync(lockPath, { force: true });
|
|
317
|
+
}
|
|
318
|
+
function releaseLockFile({
|
|
319
|
+
lockPath,
|
|
320
|
+
logger
|
|
321
|
+
}) {
|
|
322
|
+
const record = readLockRecord(lockPath);
|
|
323
|
+
if (record?.pid !== process.pid) {
|
|
324
|
+
logger?.warn?.("Slack Socket Mode process lock not released", {
|
|
325
|
+
lockPath,
|
|
326
|
+
reason: "lock-owned-by-another-process",
|
|
327
|
+
ownerPid: record?.pid
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
fs.rmSync(lockPath, { force: true });
|
|
332
|
+
logger?.debug?.("Slack Socket Mode process lock released", {
|
|
333
|
+
lockPath
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function readLockRecord(lockPath) {
|
|
337
|
+
try {
|
|
338
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
339
|
+
const parsed = JSON.parse(raw);
|
|
340
|
+
return {
|
|
341
|
+
appSlug: String(parsed.appSlug ?? ""),
|
|
342
|
+
hostname: String(parsed.hostname ?? ""),
|
|
343
|
+
pid: Number(parsed.pid),
|
|
344
|
+
startedAt: String(parsed.startedAt ?? ""),
|
|
345
|
+
tokenHash: String(parsed.tokenHash ?? "")
|
|
346
|
+
};
|
|
347
|
+
} catch {
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function isProcessAlive(pid) {
|
|
352
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
process.kill(pid, 0);
|
|
357
|
+
return true;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return error.code !== "ESRCH";
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function formatLockError(error) {
|
|
363
|
+
return error instanceof Error ? error.message : String(error);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/bolt/socket-runtime.ts
|
|
367
|
+
var DEFAULT_CLIENT_PING_TIMEOUT_MS = 3e4;
|
|
368
|
+
var DEFAULT_RESTART_GUARD_MAX_WARNINGS = 3;
|
|
369
|
+
var DEFAULT_RESTART_GUARD_WINDOW_MS = 6e4;
|
|
370
|
+
var DEFAULT_SERVER_PING_TIMEOUT_MS = 3e4;
|
|
371
|
+
var SOCKET_MODE_RECOVERY_WARNING_PATTERNS = [
|
|
372
|
+
"A pong wasn't received",
|
|
373
|
+
"A ping wasn't received",
|
|
374
|
+
"Failed to send ping",
|
|
375
|
+
"WebSocket error",
|
|
376
|
+
"BrokenPipeError",
|
|
377
|
+
"broken pipe"
|
|
378
|
+
];
|
|
379
|
+
function createSlackSocketModeRuntime({
|
|
380
|
+
appSlug,
|
|
381
|
+
appToken,
|
|
382
|
+
autoReconnectEnabled = true,
|
|
383
|
+
clientPingTimeoutMs = DEFAULT_CLIENT_PING_TIMEOUT_MS,
|
|
384
|
+
exitProcess = process.exit,
|
|
385
|
+
lockDir,
|
|
386
|
+
lockEnabled = true,
|
|
387
|
+
logger,
|
|
388
|
+
logLevel = "info",
|
|
389
|
+
pingPongLoggingEnabled = false,
|
|
390
|
+
restartGuardEnabled = false,
|
|
391
|
+
restartGuardMaxWarnings = DEFAULT_RESTART_GUARD_MAX_WARNINGS,
|
|
392
|
+
restartGuardWindowMs = DEFAULT_RESTART_GUARD_WINDOW_MS,
|
|
393
|
+
runtimePolicy = "single-instance",
|
|
394
|
+
serverPingTimeoutMs = DEFAULT_SERVER_PING_TIMEOUT_MS
|
|
395
|
+
}) {
|
|
396
|
+
const lock = acquireSlackSocketModeProcessLock({
|
|
397
|
+
appSlug,
|
|
398
|
+
appToken,
|
|
399
|
+
enabled: lockEnabled && runtimePolicy === "single-instance",
|
|
400
|
+
...lockDir ? { lockDir } : {},
|
|
401
|
+
...logger ? { logger } : {}
|
|
402
|
+
});
|
|
403
|
+
const restartGuard = createSlackSocketModeRestartGuard({
|
|
404
|
+
enabled: restartGuardEnabled,
|
|
405
|
+
exitProcess,
|
|
406
|
+
logger,
|
|
407
|
+
maxWarnings: restartGuardMaxWarnings,
|
|
408
|
+
windowMs: restartGuardWindowMs
|
|
409
|
+
});
|
|
410
|
+
logger?.info?.("Slack Socket Mode runtime guard configured", {
|
|
411
|
+
autoReconnectEnabled,
|
|
412
|
+
clientPingTimeoutMs,
|
|
413
|
+
lockEnabled: Boolean(lock),
|
|
414
|
+
pingPongLoggingEnabled,
|
|
415
|
+
restartGuardEnabled,
|
|
416
|
+
restartGuardMaxWarnings,
|
|
417
|
+
restartGuardWindowMs,
|
|
418
|
+
runtimePolicy,
|
|
419
|
+
serverPingTimeoutMs
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
boltAppOptions: {
|
|
423
|
+
logger: createSlackSdkLogger({
|
|
424
|
+
level: logLevel,
|
|
425
|
+
logger,
|
|
426
|
+
restartGuard
|
|
427
|
+
})
|
|
428
|
+
},
|
|
429
|
+
close() {
|
|
430
|
+
lock?.release();
|
|
431
|
+
},
|
|
432
|
+
socketModeReceiverOptions: {
|
|
433
|
+
autoReconnectEnabled,
|
|
434
|
+
clientPingTimeout: clientPingTimeoutMs,
|
|
435
|
+
pingPongLoggingEnabled,
|
|
436
|
+
serverPingTimeout: serverPingTimeoutMs
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function createSlackSocketModeRestartGuard({
|
|
441
|
+
enabled,
|
|
442
|
+
exitProcess,
|
|
443
|
+
logger,
|
|
444
|
+
maxWarnings,
|
|
445
|
+
windowMs
|
|
446
|
+
}) {
|
|
447
|
+
const warningTimes = [];
|
|
448
|
+
let tripped = false;
|
|
449
|
+
return {
|
|
450
|
+
record(message) {
|
|
451
|
+
if (!enabled || tripped || !SOCKET_MODE_RECOVERY_WARNING_PATTERNS.some(
|
|
452
|
+
(pattern) => message.includes(pattern)
|
|
453
|
+
)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const cutoff = now - windowMs;
|
|
458
|
+
while (warningTimes.length > 0 && warningTimes[0] < cutoff) {
|
|
459
|
+
warningTimes.shift();
|
|
460
|
+
}
|
|
461
|
+
warningTimes.push(now);
|
|
462
|
+
if (warningTimes.length < maxWarnings) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
tripped = true;
|
|
466
|
+
logger?.error("Slack Socket Mode restart guard tripped", {
|
|
467
|
+
maxWarnings,
|
|
468
|
+
windowMs,
|
|
469
|
+
reason: "repeated-websocket-health-warnings",
|
|
470
|
+
lastWarning: message
|
|
471
|
+
});
|
|
472
|
+
const timer = setTimeout(() => exitProcess(1), 50);
|
|
473
|
+
timer.unref?.();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function createSlackSdkLogger({
|
|
478
|
+
level,
|
|
479
|
+
logger,
|
|
480
|
+
restartGuard
|
|
481
|
+
}) {
|
|
482
|
+
let currentLevel = toSlackLogLevel(level);
|
|
483
|
+
let name = "slack-sdk";
|
|
484
|
+
function attributes(message) {
|
|
485
|
+
return {
|
|
486
|
+
component: name,
|
|
487
|
+
message
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
debug(...msg) {
|
|
492
|
+
logger?.debug?.("Slack SDK debug", attributes(formatSlackLogArgs(msg)));
|
|
493
|
+
},
|
|
494
|
+
error(...msg) {
|
|
495
|
+
const message = formatSlackLogArgs(msg);
|
|
496
|
+
restartGuard?.record(message);
|
|
497
|
+
logger?.error("Slack SDK error", attributes(message));
|
|
498
|
+
},
|
|
499
|
+
getLevel() {
|
|
500
|
+
return currentLevel;
|
|
501
|
+
},
|
|
502
|
+
info(...msg) {
|
|
503
|
+
logger?.debug?.("Slack SDK info", attributes(formatSlackLogArgs(msg)));
|
|
504
|
+
},
|
|
505
|
+
setLevel(nextLevel) {
|
|
506
|
+
currentLevel = nextLevel;
|
|
507
|
+
},
|
|
508
|
+
setName(nextName) {
|
|
509
|
+
name = nextName || name;
|
|
510
|
+
},
|
|
511
|
+
warn(...msg) {
|
|
512
|
+
const message = formatSlackLogArgs(msg);
|
|
513
|
+
restartGuard?.record(message);
|
|
514
|
+
logger?.warn?.("Slack SDK warning", attributes(message));
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function toSlackLogLevel(level) {
|
|
519
|
+
switch (level) {
|
|
520
|
+
case "debug":
|
|
521
|
+
return "debug";
|
|
522
|
+
case "warn":
|
|
523
|
+
return "warn";
|
|
524
|
+
case "error":
|
|
525
|
+
return "error";
|
|
526
|
+
case "info":
|
|
527
|
+
return "info";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function formatSlackLogArgs(args) {
|
|
531
|
+
return args.map(formatSlackLogArg).join(" ");
|
|
532
|
+
}
|
|
533
|
+
function formatSlackLogArg(value) {
|
|
534
|
+
if (value instanceof Error) {
|
|
535
|
+
return value.message;
|
|
536
|
+
}
|
|
537
|
+
if (typeof value === "string") {
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) {
|
|
541
|
+
return String(value);
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
return JSON.stringify(redactSlackSocketModeLogValue(value));
|
|
545
|
+
} catch {
|
|
546
|
+
return "[unserializable]";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function redactSlackSocketModeLogValue(value, depth = 0) {
|
|
550
|
+
if (depth > 4 || value === null || typeof value !== "object") {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(value)) {
|
|
554
|
+
return value.map((item) => redactSlackSocketModeLogValue(item, depth + 1));
|
|
555
|
+
}
|
|
556
|
+
const result = {};
|
|
557
|
+
for (const [key, item] of Object.entries(value)) {
|
|
558
|
+
result[key] = isSensitiveKey(key) ? "[redacted]" : redactSlackSocketModeLogValue(item, depth + 1);
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
function isSensitiveKey(key) {
|
|
563
|
+
const normalized = key.toLowerCase();
|
|
564
|
+
return normalized.includes("token") || normalized.includes("secret") || normalized === "authorization" || normalized === "cookie";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/bolt/installation-store.ts
|
|
568
|
+
import fs2 from "fs/promises";
|
|
569
|
+
import { dirname } from "path";
|
|
570
|
+
var InMemorySlackInstallationStore = class {
|
|
571
|
+
devDB = {};
|
|
572
|
+
async storeInstallation(installation) {
|
|
573
|
+
const key = getSlackInstallationKey(installation);
|
|
574
|
+
this.devDB[key] = installation;
|
|
575
|
+
}
|
|
576
|
+
async fetchInstallation(query) {
|
|
577
|
+
const key = getSlackInstallationKey(query);
|
|
578
|
+
const installation = this.devDB[key];
|
|
579
|
+
if (!installation) {
|
|
580
|
+
throw new Error(`No Slack installation found for key: ${key}`);
|
|
581
|
+
}
|
|
582
|
+
return installation;
|
|
583
|
+
}
|
|
584
|
+
async deleteInstallation(query) {
|
|
585
|
+
const key = getSlackInstallationKey(query);
|
|
586
|
+
delete this.devDB[key];
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function createInMemorySlackInstallationStore() {
|
|
590
|
+
return new InMemorySlackInstallationStore();
|
|
591
|
+
}
|
|
592
|
+
var JsonFileSlackInstallationStore = class {
|
|
593
|
+
constructor(filePath) {
|
|
594
|
+
this.filePath = filePath;
|
|
595
|
+
}
|
|
596
|
+
filePath;
|
|
597
|
+
pending = Promise.resolve();
|
|
598
|
+
async storeInstallation(installation) {
|
|
599
|
+
await this.withLock(async () => {
|
|
600
|
+
const installations = await this.readInstallations();
|
|
601
|
+
installations[getSlackInstallationKey(installation)] = installation;
|
|
602
|
+
await this.writeInstallations(installations);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async fetchInstallation(query) {
|
|
606
|
+
return await this.withLock(async () => {
|
|
607
|
+
const key = getSlackInstallationKey(query);
|
|
608
|
+
const installations = await this.readInstallations();
|
|
609
|
+
const installation = installations[key];
|
|
610
|
+
if (!installation) {
|
|
611
|
+
throw new Error(`No Slack installation found for key: ${key}`);
|
|
612
|
+
}
|
|
613
|
+
return installation;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async deleteInstallation(query) {
|
|
617
|
+
await this.withLock(async () => {
|
|
618
|
+
const key = getSlackInstallationKey(query);
|
|
619
|
+
const installations = await this.readInstallations();
|
|
620
|
+
delete installations[key];
|
|
621
|
+
await this.writeInstallations(installations);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
async withLock(operation) {
|
|
625
|
+
const run = this.pending.then(operation, operation);
|
|
626
|
+
this.pending = run.then(
|
|
627
|
+
() => void 0,
|
|
628
|
+
() => void 0
|
|
629
|
+
);
|
|
630
|
+
return await run;
|
|
631
|
+
}
|
|
632
|
+
async readInstallations() {
|
|
633
|
+
let contents;
|
|
634
|
+
try {
|
|
635
|
+
contents = await fs2.readFile(this.filePath, "utf8");
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (isNodeErrorCode(error, "ENOENT")) {
|
|
638
|
+
return {};
|
|
639
|
+
}
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
if (!contents.trim()) {
|
|
643
|
+
return {};
|
|
644
|
+
}
|
|
645
|
+
const parsed = JSON.parse(contents);
|
|
646
|
+
const record = asRecord(parsed);
|
|
647
|
+
const installations = asRecord(record?.installations) ?? record;
|
|
648
|
+
if (!installations) {
|
|
649
|
+
return {};
|
|
650
|
+
}
|
|
651
|
+
const output = {};
|
|
652
|
+
for (const [key, value] of Object.entries(installations)) {
|
|
653
|
+
if (asRecord(value)) {
|
|
654
|
+
output[key] = value;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return output;
|
|
658
|
+
}
|
|
659
|
+
async writeInstallations(installations) {
|
|
660
|
+
await fs2.mkdir(dirname(this.filePath), { recursive: true });
|
|
661
|
+
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
662
|
+
const payload = JSON.stringify({ version: 1, installations }, null, 2);
|
|
663
|
+
await fs2.writeFile(tempPath, `${payload}
|
|
664
|
+
`, "utf8");
|
|
665
|
+
await fs2.rename(tempPath, this.filePath);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
function createJsonFileSlackInstallationStore(filePath) {
|
|
669
|
+
return new JsonFileSlackInstallationStore(filePath);
|
|
670
|
+
}
|
|
671
|
+
function getSlackInstallationKey(input) {
|
|
672
|
+
if (input.isEnterpriseInstall && "enterprise" in input && input.enterprise?.id) {
|
|
673
|
+
return `enterprise:${input.enterprise.id}`;
|
|
674
|
+
}
|
|
675
|
+
if (input.isEnterpriseInstall && "enterpriseId" in input && input.enterpriseId) {
|
|
676
|
+
return `enterprise:${input.enterpriseId}`;
|
|
677
|
+
}
|
|
678
|
+
if ("team" in input && input.team?.id) {
|
|
679
|
+
return `team:${input.team.id}`;
|
|
680
|
+
}
|
|
681
|
+
if ("teamId" in input && input.teamId) {
|
|
682
|
+
return `team:${input.teamId}`;
|
|
683
|
+
}
|
|
684
|
+
throw new Error("Slack installation is missing a team or enterprise id.");
|
|
685
|
+
}
|
|
686
|
+
function asRecord(value) {
|
|
687
|
+
return value && typeof value === "object" ? value : void 0;
|
|
688
|
+
}
|
|
689
|
+
function isNodeErrorCode(error, code) {
|
|
690
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
691
|
+
}
|
|
692
|
+
export {
|
|
693
|
+
InMemorySlackInstallationStore,
|
|
694
|
+
JsonFileSlackInstallationStore,
|
|
695
|
+
acquireSlackSocketModeProcessLock,
|
|
696
|
+
createInMemorySlackInstallationStore,
|
|
697
|
+
createJsonFileSlackInstallationStore,
|
|
698
|
+
createSlackBoltApp,
|
|
699
|
+
createSlackSdkLogger,
|
|
700
|
+
createSlackSocketBoltApp,
|
|
701
|
+
createSlackSocketModeRestartGuard,
|
|
702
|
+
createSlackSocketModeRuntime,
|
|
703
|
+
getSlackInstallationKey,
|
|
704
|
+
redactSlackSocketModeLogValue
|
|
705
|
+
};
|