@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.
Files changed (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. 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
- const tokenFromHeader = typeof authorization === "string"
40
- ? /^Bearer\s+(.+)$/iu.exec(authorization.trim())?.[1]?.trim()
41
- : undefined;
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
- const index = state.nextDmIndex++;
166
- return `D${String(index).padStart(9, "0")}`;
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
- const index = state.nextMpimIndex++;
170
- return `G${String(index).padStart(9, "0")}`;
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
- return SLACK_TS_RULE.pattern.test(boundary) && encodeSlackCursor(kind, boundary) === cursor
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
- function classifySlackRetryReason(error) {
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 headers = {
401
- "content-type": "application/json",
587
+ const headerEntries = [
588
+ ["content-type", "application/json"],
402
589
  ...(attempt > 0
403
- ? {
404
- "x-slack-retry-num": String(attempt),
405
- "x-slack-retry-reason": retryReason ?? "unknown_error",
406
- }
407
- : {}),
408
- "x-slack-request-timestamp": timestamp,
409
- "x-slack-signature": slackRequestSignature(state.signingSecret, timestamp, body),
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
- let requestUrl = state.eventsRequestUrl;
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 fetch(requestUrl, {
603
+ response = await postSlackEventRequest({
416
604
  body,
417
- headers,
418
- method: "POST",
419
- redirect: "manual",
605
+ headerEntries,
420
606
  signal,
607
+ state,
608
+ timeoutAt,
609
+ url: requestUrl,
421
610
  });
422
- const location = response.headers.get("location");
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).toString();
624
+ requestUrl = new URL(location, requestUrl);
433
625
  }
434
626
  if (response) {
435
- const noRetry = response.headers.get("x-slack-no-retry") === "1";
436
- await response.body?.cancel();
437
- if (response.ok || noRetry) {
627
+ if (response.status >= 200 && response.status < 300) {
438
628
  return;
439
629
  }
440
- retryReason = "http_error";
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 channel = requireSlackSendTargetId(params.body.channel);
493
- if (channel instanceof Response) {
494
- return channel;
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 mpim = [...params.state.userMpimChannels.values()].find((candidate) => candidate.id === channel);
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
- channel: {
572
- id: channel,
573
- is_channel: channel.startsWith("C"),
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 payload = mutation
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(),