@openclaw/crabline 0.1.11 → 0.1.13
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 +137 -23
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +281 -52
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +270 -54
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1218 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -1
- package/dist/src/servers/whatsapp.js +149 -25
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -3,10 +3,27 @@ import path from "node:path";
|
|
|
3
3
|
import { adminAuthError, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, parseUnknownRequestBody, queryRecord, readBody, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
|
|
4
4
|
import { SLACK_CHANNEL_ID_RULE, SLACK_SEND_TARGET_ID_RULE, SLACK_TS_RULE, SLACK_USER_ID_RULE, } from "../providers/slack-ids.js";
|
|
5
5
|
import { recordCommittedServerEvent, recordServerEvent, } from "./recorder.js";
|
|
6
|
+
import { postWebhookRequestWithResponse, validateWebhookTarget, } from "./webhook-target.js";
|
|
6
7
|
const SLACK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
7
8
|
const SLACK_EVENT_MAX_RETRIES = 3;
|
|
8
9
|
const SLACK_EVENT_MAX_REDIRECTS = 2;
|
|
9
10
|
const SLACK_EVENT_RETRY_DELAYS_MS = [0, 60_000, 5 * 60_000];
|
|
11
|
+
class SlackEventHttpError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(status) {
|
|
14
|
+
super(`Slack Events API delivery failed with HTTP ${status}.`);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = "SlackEventHttpError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
class SlackEventTargetError extends Error {
|
|
20
|
+
retryReason;
|
|
21
|
+
constructor(retryReason, targetError) {
|
|
22
|
+
super(`Slack Events API target rejected: ${targetError}`);
|
|
23
|
+
this.retryReason = retryReason;
|
|
24
|
+
this.name = "SlackEventTargetError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
function slackOk(result = {}) {
|
|
11
28
|
return jsonResponse({ ok: true, ...result });
|
|
12
29
|
}
|
|
@@ -36,9 +53,17 @@ function resolveChatPostMessageRateLimit(value) {
|
|
|
36
53
|
}
|
|
37
54
|
function requireSlackToken(request, body, state) {
|
|
38
55
|
const authorization = request.headers.authorization;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
let tokenFromHeader;
|
|
57
|
+
if (typeof authorization === "string") {
|
|
58
|
+
const trimmed = authorization.trim();
|
|
59
|
+
if (trimmed.slice(0, 6).toLowerCase() === "bearer" && /\s/u.test(trimmed[6] ?? "")) {
|
|
60
|
+
let tokenStart = 7;
|
|
61
|
+
while (tokenStart < trimmed.length && /\s/u.test(trimmed[tokenStart])) {
|
|
62
|
+
tokenStart += 1;
|
|
63
|
+
}
|
|
64
|
+
tokenFromHeader = trimmed.slice(tokenStart).trim() || undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
42
67
|
const token = tokenFromHeader ?? readTrimmedString(body.token);
|
|
43
68
|
if (!token) {
|
|
44
69
|
return slackError("not_authed");
|
|
@@ -162,12 +187,24 @@ function randomDecimalDigits(length) {
|
|
|
162
187
|
return Array.from({ length }, () => randomInt(10)).join("");
|
|
163
188
|
}
|
|
164
189
|
function nextDmChannelId(state) {
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
for (;;) {
|
|
191
|
+
const index = state.nextDmIndex++;
|
|
192
|
+
const channelId = `D${String(index).padStart(9, "0")}`;
|
|
193
|
+
if (!state.messagesByChannel.has(channelId) &&
|
|
194
|
+
![...state.userDmChannels.values()].includes(channelId)) {
|
|
195
|
+
return channelId;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
167
198
|
}
|
|
168
199
|
function nextMpimChannelId(state) {
|
|
169
|
-
|
|
170
|
-
|
|
200
|
+
for (;;) {
|
|
201
|
+
const index = state.nextMpimIndex++;
|
|
202
|
+
const channelId = `G${String(index).padStart(9, "0")}`;
|
|
203
|
+
if (!state.messagesByChannel.has(channelId) &&
|
|
204
|
+
![...state.userMpimChannels.values()].some((channel) => channel.id === channelId)) {
|
|
205
|
+
return channelId;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
171
208
|
}
|
|
172
209
|
function dmChannelForUser(state, userId) {
|
|
173
210
|
const existing = state.userDmChannels.get(userId);
|
|
@@ -178,6 +215,16 @@ function dmChannelForUser(state, userId) {
|
|
|
178
215
|
state.userDmChannels.set(userId, channelId);
|
|
179
216
|
return channelId;
|
|
180
217
|
}
|
|
218
|
+
function rememberDmChannel(state, userId, channelId) {
|
|
219
|
+
if (!channelId.startsWith("D")) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const mappedUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
|
|
223
|
+
if (mappedUser && mappedUser !== userId) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
state.userDmChannels.set(userId, channelId);
|
|
227
|
+
}
|
|
181
228
|
function mpimChannelForUsers(state, users) {
|
|
182
229
|
const key = [...users].sort().join(",");
|
|
183
230
|
const existing = state.userMpimChannels.get(key);
|
|
@@ -230,7 +277,10 @@ function decodeSlackCursor(value, kind) {
|
|
|
230
277
|
return slackError("invalid_cursor");
|
|
231
278
|
}
|
|
232
279
|
const boundary = decoded.slice(prefix.length);
|
|
233
|
-
|
|
280
|
+
const validBoundary = kind === "list"
|
|
281
|
+
? SLACK_CHANNEL_ID_RULE.pattern.test(boundary)
|
|
282
|
+
: SLACK_TS_RULE.pattern.test(boundary);
|
|
283
|
+
return validBoundary && encodeSlackCursor(kind, boundary) === cursor
|
|
234
284
|
? boundary
|
|
235
285
|
: slackError("invalid_cursor");
|
|
236
286
|
}
|
|
@@ -247,6 +297,72 @@ function appendMessage(state, message) {
|
|
|
247
297
|
state.messagesByChannel.set(message.channel, messages);
|
|
248
298
|
return message;
|
|
249
299
|
}
|
|
300
|
+
function slackConversation(state, channelId) {
|
|
301
|
+
const dmUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
|
|
302
|
+
const mpim = [...state.userMpimChannels.values()].find((candidate) => candidate.id === channelId);
|
|
303
|
+
const hasMessages = state.messagesByChannel.has(channelId);
|
|
304
|
+
if (!dmUser && !mpim && !hasMessages) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
if (dmUser) {
|
|
308
|
+
return {
|
|
309
|
+
id: channelId,
|
|
310
|
+
is_channel: false,
|
|
311
|
+
is_group: false,
|
|
312
|
+
is_im: true,
|
|
313
|
+
is_mpim: false,
|
|
314
|
+
user: dmUser,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (mpim) {
|
|
318
|
+
return {
|
|
319
|
+
id: channelId,
|
|
320
|
+
is_channel: false,
|
|
321
|
+
is_group: false,
|
|
322
|
+
is_im: false,
|
|
323
|
+
is_mpim: true,
|
|
324
|
+
is_private: true,
|
|
325
|
+
members: [state.botUserId, ...mpim.users],
|
|
326
|
+
name: "crabline",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
id: channelId,
|
|
331
|
+
is_channel: channelId.startsWith("C"),
|
|
332
|
+
is_group: channelId.startsWith("G"),
|
|
333
|
+
is_im: channelId.startsWith("D"),
|
|
334
|
+
is_mpim: false,
|
|
335
|
+
name: "crabline",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function slackConversations(state) {
|
|
339
|
+
const ids = new Set([
|
|
340
|
+
...state.messagesByChannel.keys(),
|
|
341
|
+
...state.userDmChannels.values(),
|
|
342
|
+
...[...state.userMpimChannels.values()].map((channel) => channel.id),
|
|
343
|
+
]);
|
|
344
|
+
return [...ids]
|
|
345
|
+
.sort()
|
|
346
|
+
.map((channelId) => slackConversation(state, channelId))
|
|
347
|
+
.filter((channel) => channel !== undefined);
|
|
348
|
+
}
|
|
349
|
+
function requireSlackConversationTypes(value) {
|
|
350
|
+
const types = new Set((readTrimmedString(value) ?? "public_channel")
|
|
351
|
+
.split(",")
|
|
352
|
+
.map((entry) => entry.trim())
|
|
353
|
+
.filter(Boolean));
|
|
354
|
+
const supported = new Set(["im", "mpim", "private_channel", "public_channel"]);
|
|
355
|
+
return [...types].every((type) => supported.has(type)) ? types : slackError("invalid_types");
|
|
356
|
+
}
|
|
357
|
+
function slackConversationType(conversation) {
|
|
358
|
+
if (conversation.is_im === true) {
|
|
359
|
+
return "im";
|
|
360
|
+
}
|
|
361
|
+
if (conversation.is_mpim === true) {
|
|
362
|
+
return "mpim";
|
|
363
|
+
}
|
|
364
|
+
return conversation.is_group === true ? "private_channel" : "public_channel";
|
|
365
|
+
}
|
|
250
366
|
async function appendEvent(state, event, committed = false) {
|
|
251
367
|
const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
|
|
252
368
|
await (committed ? recordCommittedServerEvent(params) : recordServerEvent(params));
|
|
@@ -281,10 +397,18 @@ function createSlackMessage(state, params) {
|
|
|
281
397
|
user: params.user ?? (params.bot ? state.botUserId : "UCRABUSER"),
|
|
282
398
|
};
|
|
283
399
|
}
|
|
284
|
-
function asSlackEventCallback(message) {
|
|
400
|
+
function asSlackEventCallback(state, message) {
|
|
285
401
|
return {
|
|
286
402
|
api_app_id: "ACRABLINE",
|
|
287
|
-
authorizations: [
|
|
403
|
+
authorizations: [
|
|
404
|
+
{
|
|
405
|
+
enterprise_id: null,
|
|
406
|
+
is_bot: true,
|
|
407
|
+
is_enterprise_install: false,
|
|
408
|
+
team_id: "TCRABLINE",
|
|
409
|
+
user_id: state.botUserId,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
288
412
|
event: message,
|
|
289
413
|
event_id: `Ev${message.ts.replace(".", "")}`,
|
|
290
414
|
event_time: Number(message.ts.slice(0, 10)),
|
|
@@ -328,7 +452,14 @@ function errorChain(error) {
|
|
|
328
452
|
}
|
|
329
453
|
return chain;
|
|
330
454
|
}
|
|
331
|
-
|
|
455
|
+
/** @internal */
|
|
456
|
+
export function classifySlackRetryReason(error) {
|
|
457
|
+
if (error instanceof SlackEventHttpError) {
|
|
458
|
+
return "http_error";
|
|
459
|
+
}
|
|
460
|
+
if (error instanceof SlackEventTargetError) {
|
|
461
|
+
return error.retryReason;
|
|
462
|
+
}
|
|
332
463
|
const chain = errorChain(error);
|
|
333
464
|
const names = chain.map((entry) => String(entry.name ?? ""));
|
|
334
465
|
const codes = chain.map((entry) => String(entry.code ?? "").toUpperCase());
|
|
@@ -361,6 +492,62 @@ function classifySlackRetryReason(error) {
|
|
|
361
492
|
}
|
|
362
493
|
return "unknown_error";
|
|
363
494
|
}
|
|
495
|
+
function slackResponseHeader(response, name) {
|
|
496
|
+
const value = response.headers[name];
|
|
497
|
+
return Array.isArray(value) ? value[0] : value;
|
|
498
|
+
}
|
|
499
|
+
function slackTargetRetryReason(error) {
|
|
500
|
+
return error === "https-required" ? "ssl_error" : "connection_failed";
|
|
501
|
+
}
|
|
502
|
+
/** @internal */
|
|
503
|
+
export async function postSlackEventToAddresses(params) {
|
|
504
|
+
const addresses = params.addresses && params.addresses.length > 0 ? [...params.addresses] : [undefined];
|
|
505
|
+
const request = params.request ?? postWebhookRequestWithResponse;
|
|
506
|
+
let lastError;
|
|
507
|
+
for (const [index, address] of addresses.entries()) {
|
|
508
|
+
const remainingMs = params.timeoutAt - Date.now();
|
|
509
|
+
if (remainingMs <= 0) {
|
|
510
|
+
throw new DOMException("Slack Events API delivery timed out", "TimeoutError");
|
|
511
|
+
}
|
|
512
|
+
const attemptsRemaining = addresses.length - index;
|
|
513
|
+
try {
|
|
514
|
+
return await request({
|
|
515
|
+
address,
|
|
516
|
+
body: params.body,
|
|
517
|
+
headerEntries: params.headerEntries,
|
|
518
|
+
signal: params.signal,
|
|
519
|
+
timeoutMs: Math.max(1, Math.floor(remainingMs / attemptsRemaining)),
|
|
520
|
+
url: params.url,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
lastError = error;
|
|
525
|
+
if (params.signal.aborted) {
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
throw lastError;
|
|
531
|
+
}
|
|
532
|
+
async function postSlackEventRequest(params) {
|
|
533
|
+
const target = await validateWebhookTarget({
|
|
534
|
+
allowLoopbackHttp: params.state.allowLoopbackHttpEvents,
|
|
535
|
+
restrictPrivateAddresses: params.state.restrictEventTargets,
|
|
536
|
+
signal: params.signal,
|
|
537
|
+
url: params.url,
|
|
538
|
+
});
|
|
539
|
+
if ("error" in target) {
|
|
540
|
+
throw new SlackEventTargetError(slackTargetRetryReason(target.error), target.error);
|
|
541
|
+
}
|
|
542
|
+
return await postSlackEventToAddresses({
|
|
543
|
+
addresses: target.addresses,
|
|
544
|
+
body: params.body,
|
|
545
|
+
headerEntries: params.headerEntries,
|
|
546
|
+
signal: params.signal,
|
|
547
|
+
timeoutAt: params.timeoutAt,
|
|
548
|
+
url: params.url,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
364
551
|
async function waitForSlackRetry(delayMs, signal) {
|
|
365
552
|
if (signal.aborted) {
|
|
366
553
|
return false;
|
|
@@ -397,47 +584,50 @@ async function deliverSlackEvent(state, event, lifecycleSignal) {
|
|
|
397
584
|
}
|
|
398
585
|
try {
|
|
399
586
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
400
|
-
const
|
|
401
|
-
"content-type"
|
|
587
|
+
const headerEntries = [
|
|
588
|
+
["content-type", "application/json"],
|
|
402
589
|
...(attempt > 0
|
|
403
|
-
?
|
|
404
|
-
"x-slack-retry-num"
|
|
405
|
-
"x-slack-retry-reason"
|
|
406
|
-
|
|
407
|
-
:
|
|
408
|
-
"x-slack-request-timestamp"
|
|
409
|
-
"x-slack-signature"
|
|
410
|
-
|
|
590
|
+
? [
|
|
591
|
+
["x-slack-retry-num", String(attempt)],
|
|
592
|
+
["x-slack-retry-reason", retryReason ?? "unknown_error"],
|
|
593
|
+
]
|
|
594
|
+
: []),
|
|
595
|
+
["x-slack-request-timestamp", timestamp],
|
|
596
|
+
["x-slack-signature", slackRequestSignature(state.signingSecret, timestamp, body)],
|
|
597
|
+
];
|
|
411
598
|
const signal = AbortSignal.any([lifecycleSignal, AbortSignal.timeout(3_000)]);
|
|
412
|
-
|
|
599
|
+
const timeoutAt = Date.now() + 3_000;
|
|
600
|
+
let requestUrl = new URL(state.eventsRequestUrl);
|
|
413
601
|
let response;
|
|
414
602
|
for (let redirectCount = 0;; redirectCount += 1) {
|
|
415
|
-
response = await
|
|
603
|
+
response = await postSlackEventRequest({
|
|
416
604
|
body,
|
|
417
|
-
|
|
418
|
-
method: "POST",
|
|
419
|
-
redirect: "manual",
|
|
605
|
+
headerEntries,
|
|
420
606
|
signal,
|
|
607
|
+
state,
|
|
608
|
+
timeoutAt,
|
|
609
|
+
url: requestUrl,
|
|
421
610
|
});
|
|
422
|
-
const
|
|
611
|
+
const noRetry = slackResponseHeader(response, "x-slack-no-retry") === "1";
|
|
612
|
+
if (noRetry) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const location = slackResponseHeader(response, "location");
|
|
423
616
|
if (![301, 302].includes(response.status) || !location) {
|
|
424
617
|
break;
|
|
425
618
|
}
|
|
426
|
-
await response.body?.cancel();
|
|
427
619
|
if (redirectCount >= SLACK_EVENT_MAX_REDIRECTS) {
|
|
428
620
|
response = undefined;
|
|
429
621
|
retryReason = "too_many_redirects";
|
|
430
622
|
break;
|
|
431
623
|
}
|
|
432
|
-
requestUrl = new URL(location, requestUrl)
|
|
624
|
+
requestUrl = new URL(location, requestUrl);
|
|
433
625
|
}
|
|
434
626
|
if (response) {
|
|
435
|
-
|
|
436
|
-
await response.body?.cancel();
|
|
437
|
-
if (response.ok || noRetry) {
|
|
627
|
+
if (response.status >= 200 && response.status < 300) {
|
|
438
628
|
return;
|
|
439
629
|
}
|
|
440
|
-
|
|
630
|
+
throw new SlackEventHttpError(response.status);
|
|
441
631
|
}
|
|
442
632
|
}
|
|
443
633
|
catch (error) {
|
|
@@ -489,10 +679,13 @@ async function handleSlackApi(params) {
|
|
|
489
679
|
if (rateLimit) {
|
|
490
680
|
rateLimit.remaining -= 1;
|
|
491
681
|
}
|
|
492
|
-
const
|
|
493
|
-
if (
|
|
494
|
-
return
|
|
682
|
+
const requestedChannel = requireSlackSendTargetId(params.body.channel);
|
|
683
|
+
if (requestedChannel instanceof Response) {
|
|
684
|
+
return requestedChannel;
|
|
495
685
|
}
|
|
686
|
+
const channel = SLACK_USER_ID_RULE.pattern.test(requestedChannel)
|
|
687
|
+
? dmChannelForUser(params.state, requestedChannel)
|
|
688
|
+
: requestedChannel;
|
|
496
689
|
const text = readSlackText(params.body.text) ?? "";
|
|
497
690
|
const attachments = readStructuredArray(params.body.attachments, "invalid_attachments");
|
|
498
691
|
if (attachments instanceof Response) {
|
|
@@ -566,21 +759,34 @@ async function handleSlackApi(params) {
|
|
|
566
759
|
if (channel instanceof Response) {
|
|
567
760
|
return channel;
|
|
568
761
|
}
|
|
569
|
-
const
|
|
762
|
+
const conversation = slackConversation(params.state, channel);
|
|
763
|
+
return conversation ? slackOk({ channel: conversation }) : slackError("channel_not_found");
|
|
764
|
+
}
|
|
765
|
+
case "conversations.list": {
|
|
766
|
+
const types = requireSlackConversationTypes(params.body.types);
|
|
767
|
+
if (types instanceof Response) {
|
|
768
|
+
return types;
|
|
769
|
+
}
|
|
770
|
+
const limit = requireSlackLimit(params.body.limit);
|
|
771
|
+
if (limit instanceof Response) {
|
|
772
|
+
return limit;
|
|
773
|
+
}
|
|
774
|
+
const boundary = decodeSlackCursor(params.body.cursor, "list");
|
|
775
|
+
if (boundary instanceof Response) {
|
|
776
|
+
return boundary;
|
|
777
|
+
}
|
|
778
|
+
const remaining = slackConversations(params.state).filter((conversation) => {
|
|
779
|
+
const channelId = readTrimmedString(conversation.id);
|
|
780
|
+
return channelId && (boundary === undefined || channelId > boundary);
|
|
781
|
+
});
|
|
782
|
+
const virtualPage = remaining.slice(0, limit);
|
|
783
|
+
const channels = virtualPage.filter((conversation) => types.has(slackConversationType(conversation)));
|
|
784
|
+
const hasMore = virtualPage.length < remaining.length;
|
|
785
|
+
const nextBoundary = readTrimmedString(virtualPage.at(-1)?.id);
|
|
570
786
|
return slackOk({
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
is_group: mpim ? false : channel.startsWith("G"),
|
|
575
|
-
is_im: channel.startsWith("D"),
|
|
576
|
-
...(mpim
|
|
577
|
-
? {
|
|
578
|
-
is_mpim: true,
|
|
579
|
-
is_private: true,
|
|
580
|
-
members: [params.state.botUserId, ...mpim.users],
|
|
581
|
-
}
|
|
582
|
-
: {}),
|
|
583
|
-
name: "crabline",
|
|
787
|
+
channels,
|
|
788
|
+
response_metadata: {
|
|
789
|
+
next_cursor: hasMore && nextBoundary ? encodeSlackCursor("list", nextBoundary) : "",
|
|
584
790
|
},
|
|
585
791
|
});
|
|
586
792
|
}
|
|
@@ -589,6 +795,9 @@ async function handleSlackApi(params) {
|
|
|
589
795
|
if (channel instanceof Response) {
|
|
590
796
|
return channel;
|
|
591
797
|
}
|
|
798
|
+
if (!slackConversation(params.state, channel)) {
|
|
799
|
+
return slackError("channel_not_found");
|
|
800
|
+
}
|
|
592
801
|
const oldest = requireSlackThreadTs(params.body.oldest);
|
|
593
802
|
if (oldest instanceof Response) {
|
|
594
803
|
return oldest;
|
|
@@ -636,6 +845,9 @@ async function handleSlackApi(params) {
|
|
|
636
845
|
if (channel instanceof Response) {
|
|
637
846
|
return channel;
|
|
638
847
|
}
|
|
848
|
+
if (!slackConversation(params.state, channel)) {
|
|
849
|
+
return slackError("channel_not_found");
|
|
850
|
+
}
|
|
639
851
|
const ts = requireSlackThreadTs(params.body.ts);
|
|
640
852
|
if (ts instanceof Response) {
|
|
641
853
|
return ts;
|
|
@@ -693,6 +905,7 @@ async function handleAdminInbound(params) {
|
|
|
693
905
|
if (user instanceof Response) {
|
|
694
906
|
return user;
|
|
695
907
|
}
|
|
908
|
+
rememberDmChannel(params.state, user, channel);
|
|
696
909
|
const threadTs = requireSlackThreadTs(params.body.threadTs ?? params.body.thread_ts);
|
|
697
910
|
if (threadTs instanceof Response) {
|
|
698
911
|
return threadTs;
|
|
@@ -713,7 +926,7 @@ async function handleAdminInbound(params) {
|
|
|
713
926
|
ts,
|
|
714
927
|
user,
|
|
715
928
|
}));
|
|
716
|
-
const event = asSlackEventCallback(message);
|
|
929
|
+
const event = asSlackEventCallback(params.state, message);
|
|
717
930
|
scheduleSlackEventDelivery(params.state, event);
|
|
718
931
|
return slackOk({ event, message });
|
|
719
932
|
}
|
|
@@ -819,17 +1032,18 @@ async function handleRequest(params) {
|
|
|
819
1032
|
state: params.state,
|
|
820
1033
|
});
|
|
821
1034
|
const mutation = ["chat.postMessage", "conversations.open"].includes(methodMatch[1]);
|
|
822
|
-
const
|
|
1035
|
+
const recordsAcceptance = mutation || methodMatch[1] === "auth.test";
|
|
1036
|
+
const payload = recordsAcceptance
|
|
823
1037
|
? (await response
|
|
824
1038
|
.clone()
|
|
825
1039
|
.json()
|
|
826
1040
|
.catch(() => undefined))
|
|
827
1041
|
: undefined;
|
|
828
1042
|
const committed = isJsonObject(payload) && payload.ok === true;
|
|
829
|
-
if (methodMatch[1] === "chat.postMessage") {
|
|
1043
|
+
if (methodMatch[1] === "auth.test" || methodMatch[1] === "chat.postMessage") {
|
|
830
1044
|
event.accepted = committed;
|
|
831
1045
|
}
|
|
832
|
-
await appendEvent(params.state, event, committed);
|
|
1046
|
+
await appendEvent(params.state, event, mutation && committed);
|
|
833
1047
|
return response;
|
|
834
1048
|
}
|
|
835
1049
|
export async function startSlackServer(params = {}) {
|
|
@@ -838,6 +1052,7 @@ export async function startSlackServer(params = {}) {
|
|
|
838
1052
|
const state = {
|
|
839
1053
|
activeEventDeliveries: new Set(),
|
|
840
1054
|
adminToken: params.adminToken ?? randomBytes(24).toString("base64url"),
|
|
1055
|
+
allowLoopbackHttpEvents: isLoopbackHost(host),
|
|
841
1056
|
botId: params.botId ?? "BCRABLINE",
|
|
842
1057
|
botToken: params.botToken ?? "xoxb-crabline-slack-token",
|
|
843
1058
|
botUserId: params.botUserId ?? "UCRABBOT",
|
|
@@ -850,6 +1065,7 @@ export async function startSlackServer(params = {}) {
|
|
|
850
1065
|
nextTsIndex: 100,
|
|
851
1066
|
onEvent: params.onEvent,
|
|
852
1067
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "slack.jsonl"),
|
|
1068
|
+
restrictEventTargets: true,
|
|
853
1069
|
signingSecret: params.signingSecret ?? "crabline-slack-signing-secret",
|
|
854
1070
|
userDmChannels: new Map(),
|
|
855
1071
|
userMpimChannels: new Map(),
|