@openclaw/crabline 0.1.10 → 0.1.12

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 (197) hide show
  1. package/README.md +99 -12
  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 +264 -50
  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/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +532 -42
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1241 -22
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1193 -118
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. 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
  }
@@ -162,12 +179,24 @@ function randomDecimalDigits(length) {
162
179
  return Array.from({ length }, () => randomInt(10)).join("");
163
180
  }
164
181
  function nextDmChannelId(state) {
165
- const index = state.nextDmIndex++;
166
- return `D${String(index).padStart(9, "0")}`;
182
+ for (;;) {
183
+ const index = state.nextDmIndex++;
184
+ const channelId = `D${String(index).padStart(9, "0")}`;
185
+ if (!state.messagesByChannel.has(channelId) &&
186
+ ![...state.userDmChannels.values()].includes(channelId)) {
187
+ return channelId;
188
+ }
189
+ }
167
190
  }
168
191
  function nextMpimChannelId(state) {
169
- const index = state.nextMpimIndex++;
170
- return `G${String(index).padStart(9, "0")}`;
192
+ for (;;) {
193
+ const index = state.nextMpimIndex++;
194
+ const channelId = `G${String(index).padStart(9, "0")}`;
195
+ if (!state.messagesByChannel.has(channelId) &&
196
+ ![...state.userMpimChannels.values()].some((channel) => channel.id === channelId)) {
197
+ return channelId;
198
+ }
199
+ }
171
200
  }
172
201
  function dmChannelForUser(state, userId) {
173
202
  const existing = state.userDmChannels.get(userId);
@@ -178,6 +207,16 @@ function dmChannelForUser(state, userId) {
178
207
  state.userDmChannels.set(userId, channelId);
179
208
  return channelId;
180
209
  }
210
+ function rememberDmChannel(state, userId, channelId) {
211
+ if (!channelId.startsWith("D")) {
212
+ return;
213
+ }
214
+ const mappedUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
215
+ if (mappedUser && mappedUser !== userId) {
216
+ return;
217
+ }
218
+ state.userDmChannels.set(userId, channelId);
219
+ }
181
220
  function mpimChannelForUsers(state, users) {
182
221
  const key = [...users].sort().join(",");
183
222
  const existing = state.userMpimChannels.get(key);
@@ -230,7 +269,10 @@ function decodeSlackCursor(value, kind) {
230
269
  return slackError("invalid_cursor");
231
270
  }
232
271
  const boundary = decoded.slice(prefix.length);
233
- return SLACK_TS_RULE.pattern.test(boundary) && encodeSlackCursor(kind, boundary) === cursor
272
+ const validBoundary = kind === "list"
273
+ ? SLACK_CHANNEL_ID_RULE.pattern.test(boundary)
274
+ : SLACK_TS_RULE.pattern.test(boundary);
275
+ return validBoundary && encodeSlackCursor(kind, boundary) === cursor
234
276
  ? boundary
235
277
  : slackError("invalid_cursor");
236
278
  }
@@ -247,6 +289,72 @@ function appendMessage(state, message) {
247
289
  state.messagesByChannel.set(message.channel, messages);
248
290
  return message;
249
291
  }
292
+ function slackConversation(state, channelId) {
293
+ const dmUser = [...state.userDmChannels].find(([, value]) => value === channelId)?.[0];
294
+ const mpim = [...state.userMpimChannels.values()].find((candidate) => candidate.id === channelId);
295
+ const hasMessages = state.messagesByChannel.has(channelId);
296
+ if (!dmUser && !mpim && !hasMessages) {
297
+ return undefined;
298
+ }
299
+ if (dmUser) {
300
+ return {
301
+ id: channelId,
302
+ is_channel: false,
303
+ is_group: false,
304
+ is_im: true,
305
+ is_mpim: false,
306
+ user: dmUser,
307
+ };
308
+ }
309
+ if (mpim) {
310
+ return {
311
+ id: channelId,
312
+ is_channel: false,
313
+ is_group: false,
314
+ is_im: false,
315
+ is_mpim: true,
316
+ is_private: true,
317
+ members: [state.botUserId, ...mpim.users],
318
+ name: "crabline",
319
+ };
320
+ }
321
+ return {
322
+ id: channelId,
323
+ is_channel: channelId.startsWith("C"),
324
+ is_group: channelId.startsWith("G"),
325
+ is_im: channelId.startsWith("D"),
326
+ is_mpim: false,
327
+ name: "crabline",
328
+ };
329
+ }
330
+ function slackConversations(state) {
331
+ const ids = new Set([
332
+ ...state.messagesByChannel.keys(),
333
+ ...state.userDmChannels.values(),
334
+ ...[...state.userMpimChannels.values()].map((channel) => channel.id),
335
+ ]);
336
+ return [...ids]
337
+ .sort()
338
+ .map((channelId) => slackConversation(state, channelId))
339
+ .filter((channel) => channel !== undefined);
340
+ }
341
+ function requireSlackConversationTypes(value) {
342
+ const types = new Set((readTrimmedString(value) ?? "public_channel")
343
+ .split(",")
344
+ .map((entry) => entry.trim())
345
+ .filter(Boolean));
346
+ const supported = new Set(["im", "mpim", "private_channel", "public_channel"]);
347
+ return [...types].every((type) => supported.has(type)) ? types : slackError("invalid_types");
348
+ }
349
+ function slackConversationType(conversation) {
350
+ if (conversation.is_im === true) {
351
+ return "im";
352
+ }
353
+ if (conversation.is_mpim === true) {
354
+ return "mpim";
355
+ }
356
+ return conversation.is_group === true ? "private_channel" : "public_channel";
357
+ }
250
358
  async function appendEvent(state, event, committed = false) {
251
359
  const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
252
360
  await (committed ? recordCommittedServerEvent(params) : recordServerEvent(params));
@@ -281,10 +389,18 @@ function createSlackMessage(state, params) {
281
389
  user: params.user ?? (params.bot ? state.botUserId : "UCRABUSER"),
282
390
  };
283
391
  }
284
- function asSlackEventCallback(message) {
392
+ function asSlackEventCallback(state, message) {
285
393
  return {
286
394
  api_app_id: "ACRABLINE",
287
- authorizations: [],
395
+ authorizations: [
396
+ {
397
+ enterprise_id: null,
398
+ is_bot: true,
399
+ is_enterprise_install: false,
400
+ team_id: "TCRABLINE",
401
+ user_id: state.botUserId,
402
+ },
403
+ ],
288
404
  event: message,
289
405
  event_id: `Ev${message.ts.replace(".", "")}`,
290
406
  event_time: Number(message.ts.slice(0, 10)),
@@ -328,7 +444,14 @@ function errorChain(error) {
328
444
  }
329
445
  return chain;
330
446
  }
331
- function classifySlackRetryReason(error) {
447
+ /** @internal */
448
+ export function classifySlackRetryReason(error) {
449
+ if (error instanceof SlackEventHttpError) {
450
+ return "http_error";
451
+ }
452
+ if (error instanceof SlackEventTargetError) {
453
+ return error.retryReason;
454
+ }
332
455
  const chain = errorChain(error);
333
456
  const names = chain.map((entry) => String(entry.name ?? ""));
334
457
  const codes = chain.map((entry) => String(entry.code ?? "").toUpperCase());
@@ -361,6 +484,62 @@ function classifySlackRetryReason(error) {
361
484
  }
362
485
  return "unknown_error";
363
486
  }
487
+ function slackResponseHeader(response, name) {
488
+ const value = response.headers[name];
489
+ return Array.isArray(value) ? value[0] : value;
490
+ }
491
+ function slackTargetRetryReason(error) {
492
+ return error === "https-required" ? "ssl_error" : "connection_failed";
493
+ }
494
+ /** @internal */
495
+ export async function postSlackEventToAddresses(params) {
496
+ const addresses = params.addresses && params.addresses.length > 0 ? [...params.addresses] : [undefined];
497
+ const request = params.request ?? postWebhookRequestWithResponse;
498
+ let lastError;
499
+ for (const [index, address] of addresses.entries()) {
500
+ const remainingMs = params.timeoutAt - Date.now();
501
+ if (remainingMs <= 0) {
502
+ throw new DOMException("Slack Events API delivery timed out", "TimeoutError");
503
+ }
504
+ const attemptsRemaining = addresses.length - index;
505
+ try {
506
+ return await request({
507
+ address,
508
+ body: params.body,
509
+ headerEntries: params.headerEntries,
510
+ signal: params.signal,
511
+ timeoutMs: Math.max(1, Math.floor(remainingMs / attemptsRemaining)),
512
+ url: params.url,
513
+ });
514
+ }
515
+ catch (error) {
516
+ lastError = error;
517
+ if (params.signal.aborted) {
518
+ throw error;
519
+ }
520
+ }
521
+ }
522
+ throw lastError;
523
+ }
524
+ async function postSlackEventRequest(params) {
525
+ const target = await validateWebhookTarget({
526
+ allowLoopbackHttp: params.state.allowLoopbackHttpEvents,
527
+ restrictPrivateAddresses: params.state.restrictEventTargets,
528
+ signal: params.signal,
529
+ url: params.url,
530
+ });
531
+ if ("error" in target) {
532
+ throw new SlackEventTargetError(slackTargetRetryReason(target.error), target.error);
533
+ }
534
+ return await postSlackEventToAddresses({
535
+ addresses: target.addresses,
536
+ body: params.body,
537
+ headerEntries: params.headerEntries,
538
+ signal: params.signal,
539
+ timeoutAt: params.timeoutAt,
540
+ url: params.url,
541
+ });
542
+ }
364
543
  async function waitForSlackRetry(delayMs, signal) {
365
544
  if (signal.aborted) {
366
545
  return false;
@@ -397,47 +576,50 @@ async function deliverSlackEvent(state, event, lifecycleSignal) {
397
576
  }
398
577
  try {
399
578
  const timestamp = Math.floor(Date.now() / 1000).toString();
400
- const headers = {
401
- "content-type": "application/json",
579
+ const headerEntries = [
580
+ ["content-type", "application/json"],
402
581
  ...(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
- };
582
+ ? [
583
+ ["x-slack-retry-num", String(attempt)],
584
+ ["x-slack-retry-reason", retryReason ?? "unknown_error"],
585
+ ]
586
+ : []),
587
+ ["x-slack-request-timestamp", timestamp],
588
+ ["x-slack-signature", slackRequestSignature(state.signingSecret, timestamp, body)],
589
+ ];
411
590
  const signal = AbortSignal.any([lifecycleSignal, AbortSignal.timeout(3_000)]);
412
- let requestUrl = state.eventsRequestUrl;
591
+ const timeoutAt = Date.now() + 3_000;
592
+ let requestUrl = new URL(state.eventsRequestUrl);
413
593
  let response;
414
594
  for (let redirectCount = 0;; redirectCount += 1) {
415
- response = await fetch(requestUrl, {
595
+ response = await postSlackEventRequest({
416
596
  body,
417
- headers,
418
- method: "POST",
419
- redirect: "manual",
597
+ headerEntries,
420
598
  signal,
599
+ state,
600
+ timeoutAt,
601
+ url: requestUrl,
421
602
  });
422
- const location = response.headers.get("location");
603
+ const noRetry = slackResponseHeader(response, "x-slack-no-retry") === "1";
604
+ if (noRetry) {
605
+ return;
606
+ }
607
+ const location = slackResponseHeader(response, "location");
423
608
  if (![301, 302].includes(response.status) || !location) {
424
609
  break;
425
610
  }
426
- await response.body?.cancel();
427
611
  if (redirectCount >= SLACK_EVENT_MAX_REDIRECTS) {
428
612
  response = undefined;
429
613
  retryReason = "too_many_redirects";
430
614
  break;
431
615
  }
432
- requestUrl = new URL(location, requestUrl).toString();
616
+ requestUrl = new URL(location, requestUrl);
433
617
  }
434
618
  if (response) {
435
- const noRetry = response.headers.get("x-slack-no-retry") === "1";
436
- await response.body?.cancel();
437
- if (response.ok || noRetry) {
619
+ if (response.status >= 200 && response.status < 300) {
438
620
  return;
439
621
  }
440
- retryReason = "http_error";
622
+ throw new SlackEventHttpError(response.status);
441
623
  }
442
624
  }
443
625
  catch (error) {
@@ -489,10 +671,13 @@ async function handleSlackApi(params) {
489
671
  if (rateLimit) {
490
672
  rateLimit.remaining -= 1;
491
673
  }
492
- const channel = requireSlackSendTargetId(params.body.channel);
493
- if (channel instanceof Response) {
494
- return channel;
674
+ const requestedChannel = requireSlackSendTargetId(params.body.channel);
675
+ if (requestedChannel instanceof Response) {
676
+ return requestedChannel;
495
677
  }
678
+ const channel = SLACK_USER_ID_RULE.pattern.test(requestedChannel)
679
+ ? dmChannelForUser(params.state, requestedChannel)
680
+ : requestedChannel;
496
681
  const text = readSlackText(params.body.text) ?? "";
497
682
  const attachments = readStructuredArray(params.body.attachments, "invalid_attachments");
498
683
  if (attachments instanceof Response) {
@@ -566,21 +751,34 @@ async function handleSlackApi(params) {
566
751
  if (channel instanceof Response) {
567
752
  return channel;
568
753
  }
569
- const mpim = [...params.state.userMpimChannels.values()].find((candidate) => candidate.id === channel);
754
+ const conversation = slackConversation(params.state, channel);
755
+ return conversation ? slackOk({ channel: conversation }) : slackError("channel_not_found");
756
+ }
757
+ case "conversations.list": {
758
+ const types = requireSlackConversationTypes(params.body.types);
759
+ if (types instanceof Response) {
760
+ return types;
761
+ }
762
+ const limit = requireSlackLimit(params.body.limit);
763
+ if (limit instanceof Response) {
764
+ return limit;
765
+ }
766
+ const boundary = decodeSlackCursor(params.body.cursor, "list");
767
+ if (boundary instanceof Response) {
768
+ return boundary;
769
+ }
770
+ const remaining = slackConversations(params.state).filter((conversation) => {
771
+ const channelId = readTrimmedString(conversation.id);
772
+ return channelId && (boundary === undefined || channelId > boundary);
773
+ });
774
+ const virtualPage = remaining.slice(0, limit);
775
+ const channels = virtualPage.filter((conversation) => types.has(slackConversationType(conversation)));
776
+ const hasMore = virtualPage.length < remaining.length;
777
+ const nextBoundary = readTrimmedString(virtualPage.at(-1)?.id);
570
778
  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",
779
+ channels,
780
+ response_metadata: {
781
+ next_cursor: hasMore && nextBoundary ? encodeSlackCursor("list", nextBoundary) : "",
584
782
  },
585
783
  });
586
784
  }
@@ -589,6 +787,9 @@ async function handleSlackApi(params) {
589
787
  if (channel instanceof Response) {
590
788
  return channel;
591
789
  }
790
+ if (!slackConversation(params.state, channel)) {
791
+ return slackError("channel_not_found");
792
+ }
592
793
  const oldest = requireSlackThreadTs(params.body.oldest);
593
794
  if (oldest instanceof Response) {
594
795
  return oldest;
@@ -636,6 +837,9 @@ async function handleSlackApi(params) {
636
837
  if (channel instanceof Response) {
637
838
  return channel;
638
839
  }
840
+ if (!slackConversation(params.state, channel)) {
841
+ return slackError("channel_not_found");
842
+ }
639
843
  const ts = requireSlackThreadTs(params.body.ts);
640
844
  if (ts instanceof Response) {
641
845
  return ts;
@@ -693,6 +897,7 @@ async function handleAdminInbound(params) {
693
897
  if (user instanceof Response) {
694
898
  return user;
695
899
  }
900
+ rememberDmChannel(params.state, user, channel);
696
901
  const threadTs = requireSlackThreadTs(params.body.threadTs ?? params.body.thread_ts);
697
902
  if (threadTs instanceof Response) {
698
903
  return threadTs;
@@ -713,7 +918,7 @@ async function handleAdminInbound(params) {
713
918
  ts,
714
919
  user,
715
920
  }));
716
- const event = asSlackEventCallback(message);
921
+ const event = asSlackEventCallback(params.state, message);
717
922
  scheduleSlackEventDelivery(params.state, event);
718
923
  return slackOk({ event, message });
719
924
  }
@@ -819,17 +1024,18 @@ async function handleRequest(params) {
819
1024
  state: params.state,
820
1025
  });
821
1026
  const mutation = ["chat.postMessage", "conversations.open"].includes(methodMatch[1]);
822
- const payload = mutation
1027
+ const recordsAcceptance = mutation || methodMatch[1] === "auth.test";
1028
+ const payload = recordsAcceptance
823
1029
  ? (await response
824
1030
  .clone()
825
1031
  .json()
826
1032
  .catch(() => undefined))
827
1033
  : undefined;
828
1034
  const committed = isJsonObject(payload) && payload.ok === true;
829
- if (methodMatch[1] === "chat.postMessage") {
1035
+ if (methodMatch[1] === "auth.test" || methodMatch[1] === "chat.postMessage") {
830
1036
  event.accepted = committed;
831
1037
  }
832
- await appendEvent(params.state, event, committed);
1038
+ await appendEvent(params.state, event, mutation && committed);
833
1039
  return response;
834
1040
  }
835
1041
  export async function startSlackServer(params = {}) {
@@ -838,6 +1044,7 @@ export async function startSlackServer(params = {}) {
838
1044
  const state = {
839
1045
  activeEventDeliveries: new Set(),
840
1046
  adminToken: params.adminToken ?? randomBytes(24).toString("base64url"),
1047
+ allowLoopbackHttpEvents: isLoopbackHost(host),
841
1048
  botId: params.botId ?? "BCRABLINE",
842
1049
  botToken: params.botToken ?? "xoxb-crabline-slack-token",
843
1050
  botUserId: params.botUserId ?? "UCRABBOT",
@@ -850,6 +1057,7 @@ export async function startSlackServer(params = {}) {
850
1057
  nextTsIndex: 100,
851
1058
  onEvent: params.onEvent,
852
1059
  recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "slack.jsonl"),
1060
+ restrictEventTargets: true,
853
1061
  signingSecret: params.signingSecret ?? "crabline-slack-signing-secret",
854
1062
  userDmChannels: new Map(),
855
1063
  userMpimChannels: new Map(),