@openclaw/slack 2026.5.12-beta.7

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 (88) hide show
  1. package/dist/account-inspect-D7AZNs8C.js +77 -0
  2. package/dist/account-inspect-api.js +10 -0
  3. package/dist/accounts-ClAPP5ry.js +139 -0
  4. package/dist/accounts.runtime-DDVcLJUI.js +2 -0
  5. package/dist/action-runtime-e2UhRsNx.js +350 -0
  6. package/dist/action-runtime.runtime-BFcqMbOm.js +2 -0
  7. package/dist/actions-CYLFK-Zy.js +292 -0
  8. package/dist/actions.runtime-CO3OaTLb.js +2 -0
  9. package/dist/allow-list-BPnnlRPL.js +82 -0
  10. package/dist/api.js +21 -0
  11. package/dist/approval-handler.runtime-CmeRr9qA.js +256 -0
  12. package/dist/blocks-input-CwTFVImV.js +29 -0
  13. package/dist/blocks-render-BIDw-Pom.js +161 -0
  14. package/dist/channel-DRjHBTDB.js +1020 -0
  15. package/dist/channel-api-B_nZwosg.js +20 -0
  16. package/dist/channel-config-api.js +2 -0
  17. package/dist/channel-entry.js +22 -0
  18. package/dist/channel-plugin-api.js +2 -0
  19. package/dist/channel.setup-Cayn7afd.js +73 -0
  20. package/dist/client-CPe4GmDR.js +103 -0
  21. package/dist/config-api-B_jq4NJW.js +2 -0
  22. package/dist/config-schema-D9B5LB_L.js +167 -0
  23. package/dist/configured-state.js +11 -0
  24. package/dist/contract-api.js +5 -0
  25. package/dist/directory-config-B3JiHeB7.js +54 -0
  26. package/dist/directory-contract-api.js +2 -0
  27. package/dist/directory-live-Bf16GwDh.js +133 -0
  28. package/dist/doctor-contract-KUjHnkQm.js +147 -0
  29. package/dist/doctor-contract-api.js +2 -0
  30. package/dist/errors-BYFHR24f.js +109 -0
  31. package/dist/exec-approvals-7xUNgLi9.js +58 -0
  32. package/dist/group-policy-CyLUK6My.js +41 -0
  33. package/dist/http-routes-api.js +2 -0
  34. package/dist/inbound-contract-test-api.js +3 -0
  35. package/dist/index.js +33 -0
  36. package/dist/interactive-replies-api.js +2 -0
  37. package/dist/interactive-replies-qAIfuBor.js +173 -0
  38. package/dist/magic-string.es-BMaGRRZ1.js +1011 -0
  39. package/dist/media-D1XCd1uP.js +469 -0
  40. package/dist/message-tool-api-6lowf9zE.js +104 -0
  41. package/dist/message-tool-api.js +2 -0
  42. package/dist/monitor-a97o17G6.js +13 -0
  43. package/dist/mrkdwn-Cax-eSfK.js +6 -0
  44. package/dist/outbound-adapter-B_5sEhCg.js +174 -0
  45. package/dist/outbound-payload-test-api.js +2 -0
  46. package/dist/outbound-payload.test-harness-CVCamg1x.js +13558 -0
  47. package/dist/pipeline.runtime-DT0hLnq2.js +1379 -0
  48. package/dist/plugin-routes-DtTPmga1.js +20 -0
  49. package/dist/prepare-D3YqV8jB.js +1482 -0
  50. package/dist/prepare.test-helpers-DVcjRhfG.js +49 -0
  51. package/dist/probe-3eZf1FjI.js +42 -0
  52. package/dist/provider-D7uAN3Fq.js +3235 -0
  53. package/dist/registry-CeaoNfoP.js +39 -0
  54. package/dist/replies-Xe_jMR6o.js +139 -0
  55. package/dist/reply-blocks-Z5l6_R6H.js +14 -0
  56. package/dist/resolve-allowlist-common-Bk3clYPK.js +43 -0
  57. package/dist/resolve-channels-BRYqyNVJ.js +81 -0
  58. package/dist/resolve-users-Bd_SdP8j.js +113 -0
  59. package/dist/rolldown-runtime-CiIaOW0V.js +13 -0
  60. package/dist/room-context-0vovmZPU.js +787 -0
  61. package/dist/runtime-Bo-KHM-F.js +8 -0
  62. package/dist/runtime-api-Dd1xIV5v.js +9 -0
  63. package/dist/runtime-api.js +14 -0
  64. package/dist/runtime-setter-api.js +2 -0
  65. package/dist/scopes-CDevO8jg.js +74 -0
  66. package/dist/secret-contract-Bo6lbSkh.js +141 -0
  67. package/dist/secret-contract-api.js +2 -0
  68. package/dist/security-audit-BtHGnD3d.js +51 -0
  69. package/dist/security-contract-api.js +2 -0
  70. package/dist/send-D_A9kL-C.js +721 -0
  71. package/dist/send.runtime-BRE_ncCU.js +2 -0
  72. package/dist/send.runtime-_l76lUuL.js +2 -0
  73. package/dist/setup-core-B9NetDkM.js +320 -0
  74. package/dist/setup-entry.js +15 -0
  75. package/dist/setup-plugin-api.js +2 -0
  76. package/dist/setup-surface-D88QBVOW.js +128 -0
  77. package/dist/shared-D8U42xFL.js +208 -0
  78. package/dist/slash-commands.runtime-22kgyst2.js +19 -0
  79. package/dist/slash-dispatch.runtime-BJgT0jwV.js +32 -0
  80. package/dist/slash-plugin-commands.runtime-CF-n3MeP.js +2 -0
  81. package/dist/slash-skill-commands.runtime-BMs0VjTe.js +7 -0
  82. package/dist/streaming-compat-RkZgTmQ2.js +43 -0
  83. package/dist/target-parsing-CQmv-iSm.js +55 -0
  84. package/dist/targets-B1tYCAr6.js +2 -0
  85. package/dist/test-api.js +8 -0
  86. package/dist/thread-ts-C2x7c5PP.js +24 -0
  87. package/openclaw.plugin.json +2405 -0
  88. package/package.json +84 -0
@@ -0,0 +1,721 @@
1
+ import { a as resolveSlackAccount, d as resolveSlackBotToken } from "./accounts-ClAPP5ry.js";
2
+ import { r as parseSlackTarget } from "./target-parsing-CQmv-iSm.js";
3
+ import "./targets-B1tYCAr6.js";
4
+ import { i as truncateSlackText, r as SLACK_TEXT_LIMIT, t as normalizeSlackThreadTsCandidate } from "./thread-ts-C2x7c5PP.js";
5
+ import { a as getSlackWriteClient, n as createSlackTokenCacheKey } from "./client-CPe4GmDR.js";
6
+ import { r as validateSlackBlocksArray } from "./blocks-input-CwTFVImV.js";
7
+ import { t as getOptionalSlackRuntime } from "./runtime-Bo-KHM-F.js";
8
+ import { i as loadOutboundMediaFromUrl } from "./runtime-api-Dd1xIV5v.js";
9
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
10
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-message";
11
+ import { withTrustedEnvProxyGuardedFetchMode } from "openclaw/plugin-sdk/fetch-runtime";
12
+ import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
13
+ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
14
+ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
15
+ import { chunkMarkdownTextWithMode, isSilentReplyText, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
16
+ import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
17
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
18
+ import { markdownToIR, renderMarkdownIRChunksWithinLimit, renderMarkdownWithMarkers } from "openclaw/plugin-sdk/text-chunking";
19
+ import { resolveGlobalDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
20
+ //#region extensions/slack/src/blocks-fallback.ts
21
+ function cleanCandidate(value) {
22
+ if (typeof value !== "string") return;
23
+ const normalized = value.replace(/\s+/g, " ").trim();
24
+ return normalized.length > 0 ? normalized : void 0;
25
+ }
26
+ function readSectionText(block) {
27
+ return cleanCandidate(block.text?.text);
28
+ }
29
+ function readHeaderText(block) {
30
+ return cleanCandidate(block.text?.text);
31
+ }
32
+ function readImageText(block) {
33
+ return cleanCandidate(block.alt_text) ?? cleanCandidate(block.title?.text);
34
+ }
35
+ function readVideoText(block) {
36
+ return cleanCandidate(block.title?.text) ?? cleanCandidate(block.alt_text);
37
+ }
38
+ function readContextText(block) {
39
+ if (!Array.isArray(block.elements)) return;
40
+ const textParts = block.elements.map((element) => cleanCandidate(element.text)).filter((value) => Boolean(value));
41
+ return textParts.length > 0 ? textParts.join(" ") : void 0;
42
+ }
43
+ function buildSlackBlocksFallbackText(blocks) {
44
+ for (const raw of blocks) {
45
+ const block = raw;
46
+ switch (block.type) {
47
+ case "header": {
48
+ const text = readHeaderText(block);
49
+ if (text) return text;
50
+ break;
51
+ }
52
+ case "section": {
53
+ const text = readSectionText(block);
54
+ if (text) return text;
55
+ break;
56
+ }
57
+ case "image": {
58
+ const text = readImageText(block);
59
+ if (text) return text;
60
+ return "Shared an image";
61
+ }
62
+ case "video": {
63
+ const text = readVideoText(block);
64
+ if (text) return text;
65
+ return "Shared a video";
66
+ }
67
+ case "file": return "Shared a file";
68
+ case "context": {
69
+ const text = readContextText(block);
70
+ if (text) return text;
71
+ break;
72
+ }
73
+ default: break;
74
+ }
75
+ }
76
+ return "Shared a Block Kit message";
77
+ }
78
+ //#endregion
79
+ //#region extensions/slack/src/format.ts
80
+ function escapeSlackMrkdwnSegment(text) {
81
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
82
+ }
83
+ const SLACK_ANGLE_TOKEN_RE = /<[^>\n]+>/g;
84
+ function isAllowedSlackAngleToken(token) {
85
+ if (!token.startsWith("<") || !token.endsWith(">")) return false;
86
+ const inner = token.slice(1, -1);
87
+ return inner.startsWith("@") || inner.startsWith("#") || inner.startsWith("!") || inner.startsWith("mailto:") || inner.startsWith("tel:") || inner.startsWith("http://") || inner.startsWith("https://") || inner.startsWith("slack://");
88
+ }
89
+ function escapeSlackMrkdwnContent(text) {
90
+ if (!text) return "";
91
+ if (!text.includes("&") && !text.includes("<") && !text.includes(">")) return text;
92
+ SLACK_ANGLE_TOKEN_RE.lastIndex = 0;
93
+ const out = [];
94
+ let lastIndex = 0;
95
+ for (let match = SLACK_ANGLE_TOKEN_RE.exec(text); match; match = SLACK_ANGLE_TOKEN_RE.exec(text)) {
96
+ const matchIndex = match.index ?? 0;
97
+ out.push(escapeSlackMrkdwnSegment(text.slice(lastIndex, matchIndex)));
98
+ const token = match[0] ?? "";
99
+ out.push(isAllowedSlackAngleToken(token) ? token : escapeSlackMrkdwnSegment(token));
100
+ lastIndex = matchIndex + token.length;
101
+ }
102
+ out.push(escapeSlackMrkdwnSegment(text.slice(lastIndex)));
103
+ return out.join("");
104
+ }
105
+ function escapeSlackMrkdwnText(text) {
106
+ if (!text) return "";
107
+ if (!text.includes("&") && !text.includes("<") && !text.includes(">")) return text;
108
+ return text.split("\n").map((line) => {
109
+ if (line.startsWith("> ")) return `> ${escapeSlackMrkdwnContent(line.slice(2))}`;
110
+ return escapeSlackMrkdwnContent(line);
111
+ }).join("\n");
112
+ }
113
+ function buildSlackLink(link, text) {
114
+ const href = link.href.trim();
115
+ if (!href) return null;
116
+ const trimmedLabel = text.slice(link.start, link.end).trim();
117
+ const comparableHref = href.startsWith("mailto:") ? href.slice(7) : href;
118
+ if (!(trimmedLabel.length > 0 && trimmedLabel !== href && trimmedLabel !== comparableHref)) return null;
119
+ const safeHref = escapeSlackMrkdwnSegment(href);
120
+ return {
121
+ start: link.start,
122
+ end: link.end,
123
+ open: `<${safeHref}|`,
124
+ close: ">"
125
+ };
126
+ }
127
+ function buildSlackRenderOptions() {
128
+ return {
129
+ styleMarkers: {
130
+ bold: {
131
+ open: "*",
132
+ close: "*"
133
+ },
134
+ italic: {
135
+ open: "_",
136
+ close: "_"
137
+ },
138
+ strikethrough: {
139
+ open: "~",
140
+ close: "~"
141
+ },
142
+ code: {
143
+ open: "`",
144
+ close: "`"
145
+ },
146
+ code_block: {
147
+ open: "```\n",
148
+ close: "```"
149
+ }
150
+ },
151
+ escapeText: escapeSlackMrkdwnText,
152
+ buildLink: buildSlackLink
153
+ };
154
+ }
155
+ function markdownToSlackMrkdwn(markdown, options = {}) {
156
+ return renderMarkdownWithMarkers(markdownToIR(markdown ?? "", {
157
+ linkify: false,
158
+ autolink: false,
159
+ headingStyle: "bold",
160
+ blockquotePrefix: "> ",
161
+ tableMode: options.tableMode
162
+ }), buildSlackRenderOptions());
163
+ }
164
+ function normalizeSlackOutboundText(markdown) {
165
+ return markdownToSlackMrkdwn(markdown ?? "");
166
+ }
167
+ function markdownToSlackMrkdwnChunks(markdown, limit, options = {}) {
168
+ const ir = markdownToIR(markdown ?? "", {
169
+ linkify: false,
170
+ autolink: false,
171
+ headingStyle: "bold",
172
+ blockquotePrefix: "> ",
173
+ tableMode: options.tableMode
174
+ });
175
+ const renderOptions = buildSlackRenderOptions();
176
+ return renderMarkdownIRChunksWithinLimit({
177
+ ir,
178
+ limit,
179
+ renderChunk: (chunk) => renderMarkdownWithMarkers(chunk, renderOptions),
180
+ measureRendered: (rendered) => rendered.length
181
+ }).map(({ rendered }) => rendered);
182
+ }
183
+ //#endregion
184
+ //#region extensions/slack/src/sent-thread-cache.ts
185
+ /**
186
+ * In-memory cache of Slack threads the bot has participated in.
187
+ * Used to auto-respond in threads without requiring @mention after the first reply.
188
+ * Follows a similar TTL pattern to the MS Teams and Telegram sent-message caches.
189
+ */
190
+ const TTL_MS = 1440 * 60 * 1e3;
191
+ const MAX_ENTRIES = 5e3;
192
+ const PERSISTENT_MAX_ENTRIES = 1e3;
193
+ const PERSISTENT_NAMESPACE = "slack.thread-participation";
194
+ const threadParticipation = resolveGlobalDedupeCache(Symbol.for("openclaw.slackThreadParticipation"), {
195
+ ttlMs: TTL_MS,
196
+ maxSize: MAX_ENTRIES
197
+ });
198
+ let persistentStore;
199
+ let persistentStoreDisabled = false;
200
+ function makeKey(accountId, channelId, threadTs) {
201
+ return `${accountId}:${channelId}:${threadTs}`;
202
+ }
203
+ function reportPersistentThreadParticipationError(error) {
204
+ try {
205
+ getOptionalSlackRuntime()?.logging.getChildLogger({
206
+ plugin: "slack",
207
+ feature: "thread-participation-state"
208
+ }).warn("Slack persistent thread participation state failed", { error: String(error) });
209
+ } catch {}
210
+ }
211
+ function disablePersistentThreadParticipation(error) {
212
+ persistentStoreDisabled = true;
213
+ persistentStore = void 0;
214
+ reportPersistentThreadParticipationError(error);
215
+ }
216
+ function getPersistentThreadParticipationStore() {
217
+ if (persistentStoreDisabled) return;
218
+ if (persistentStore) return persistentStore;
219
+ const runtime = getOptionalSlackRuntime();
220
+ if (!runtime) return;
221
+ try {
222
+ persistentStore = runtime.state.openKeyedStore({
223
+ namespace: PERSISTENT_NAMESPACE,
224
+ maxEntries: PERSISTENT_MAX_ENTRIES,
225
+ defaultTtlMs: TTL_MS
226
+ });
227
+ return persistentStore;
228
+ } catch (error) {
229
+ disablePersistentThreadParticipation(error);
230
+ return;
231
+ }
232
+ }
233
+ function rememberPersistentThreadParticipation(params) {
234
+ const store = getPersistentThreadParticipationStore();
235
+ if (!store) return;
236
+ store.register(params.key, {
237
+ ...params.agentId ? { agentId: params.agentId } : {},
238
+ repliedAt: Date.now()
239
+ }).catch(disablePersistentThreadParticipation);
240
+ }
241
+ async function lookupPersistentThreadParticipation(key) {
242
+ const store = getPersistentThreadParticipationStore();
243
+ if (!store) return false;
244
+ try {
245
+ return Boolean(await store.lookup(key));
246
+ } catch (error) {
247
+ disablePersistentThreadParticipation(error);
248
+ return false;
249
+ }
250
+ }
251
+ function recordSlackThreadParticipation(accountId, channelId, threadTs, opts) {
252
+ if (!accountId || !channelId || !threadTs) return;
253
+ const key = makeKey(accountId, channelId, threadTs);
254
+ threadParticipation.check(key);
255
+ rememberPersistentThreadParticipation({
256
+ key,
257
+ agentId: opts?.agentId
258
+ });
259
+ }
260
+ function hasSlackThreadParticipation(accountId, channelId, threadTs) {
261
+ if (!accountId || !channelId || !threadTs) return false;
262
+ return threadParticipation.peek(makeKey(accountId, channelId, threadTs));
263
+ }
264
+ async function hasSlackThreadParticipationWithPersistence(params) {
265
+ if (!params.accountId || !params.channelId || !params.threadTs) return false;
266
+ const key = makeKey(params.accountId, params.channelId, params.threadTs);
267
+ if (threadParticipation.peek(key)) return true;
268
+ const found = await lookupPersistentThreadParticipation(key);
269
+ if (found) threadParticipation.check(key);
270
+ return found;
271
+ }
272
+ function clearSlackThreadParticipationCache() {
273
+ threadParticipation.clear();
274
+ persistentStore = void 0;
275
+ persistentStoreDisabled = false;
276
+ }
277
+ //#endregion
278
+ //#region extensions/slack/src/send.ts
279
+ const SLACK_UPLOAD_SSRF_POLICY = {
280
+ allowedHostnames: [
281
+ "*.slack.com",
282
+ "*.slack-edge.com",
283
+ "*.slack-files.com"
284
+ ],
285
+ allowRfc2544BenchmarkRange: true
286
+ };
287
+ const SLACK_DM_CHANNEL_CACHE_MAX = 1024;
288
+ const SLACK_DNS_RETRY_CODES = new Set([
289
+ "EAI_AGAIN",
290
+ "ENOTFOUND",
291
+ "UND_ERR_DNS_RESOLVE_FAILED"
292
+ ]);
293
+ const SLACK_DNS_RETRY_ATTEMPTS = 2;
294
+ const SLACK_DNS_RETRY_BASE_DELAY_MS = 250;
295
+ const slackDmChannelCache = /* @__PURE__ */ new Map();
296
+ const slackSendQueues = /* @__PURE__ */ new Map();
297
+ function hasCustomIdentity(identity) {
298
+ return Boolean(identity?.username || identity?.iconUrl || identity?.iconEmoji);
299
+ }
300
+ function buildSlackUnfurlPayload(options) {
301
+ return {
302
+ ...typeof options?.unfurlLinks === "boolean" ? { unfurl_links: options.unfurlLinks } : {},
303
+ ...typeof options?.unfurlMedia === "boolean" ? { unfurl_media: options.unfurlMedia } : {}
304
+ };
305
+ }
306
+ function buildSlackPostMessagePayload(params) {
307
+ const threadPayload = params.replyBroadcast && params.threadTs ? {
308
+ thread_ts: params.threadTs,
309
+ reply_broadcast: true
310
+ } : params.threadTs ? { thread_ts: params.threadTs } : {};
311
+ const unfurlPayload = buildSlackUnfurlPayload(params.unfurl);
312
+ if (params.blocks?.length) return {
313
+ channel: params.channelId,
314
+ text: params.text,
315
+ blocks: params.blocks,
316
+ ...threadPayload,
317
+ ...unfurlPayload
318
+ };
319
+ return {
320
+ channel: params.channelId,
321
+ text: params.text,
322
+ ...threadPayload,
323
+ ...unfurlPayload
324
+ };
325
+ }
326
+ function normalizeSlackApiString(value) {
327
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
328
+ }
329
+ function normalizeSlackScopeList(value) {
330
+ if (!Array.isArray(value)) return [];
331
+ return value.flatMap((scope) => {
332
+ const normalized = normalizeSlackApiString(scope);
333
+ return normalized ? [normalized] : [];
334
+ });
335
+ }
336
+ function getSlackWebApiErrorData(err) {
337
+ if (!(err instanceof Error)) return;
338
+ const data = err.data;
339
+ if (!data || typeof data !== "object") return;
340
+ return data;
341
+ }
342
+ function formatSlackWebApiErrorMessage(err) {
343
+ if (!(err instanceof Error)) return;
344
+ const data = getSlackWebApiErrorData(err);
345
+ const code = normalizeSlackApiString(data?.error);
346
+ if (!code) return;
347
+ const details = [];
348
+ const needed = normalizeSlackApiString(data?.needed);
349
+ if (needed) details.push(`needed: ${needed}`);
350
+ const scopes = normalizeSlackScopeList(data?.response_metadata?.scopes);
351
+ if (scopes.length) details.push(`granted: ${scopes.join(", ")}`);
352
+ const acceptedScopes = normalizeSlackScopeList(data?.response_metadata?.acceptedScopes);
353
+ if (acceptedScopes.length) details.push(`accepted: ${acceptedScopes.join(", ")}`);
354
+ return `${err.message || `An API error occurred: ${code}`}${details.length ? ` (${details.join("; ")})` : ""}`;
355
+ }
356
+ function enrichSlackWebApiError(err) {
357
+ const message = formatSlackWebApiErrorMessage(err);
358
+ if (!message || !(err instanceof Error) || message === err.message) return err;
359
+ return new Error(message);
360
+ }
361
+ function readSlackRequestErrorCode(value) {
362
+ if (!value || typeof value !== "object") return;
363
+ const code = value.code;
364
+ return typeof code === "string" ? code.toUpperCase() : void 0;
365
+ }
366
+ function readSlackRequestErrorMessage(value) {
367
+ if (value instanceof Error) return value.message;
368
+ return typeof value === "string" ? value : "";
369
+ }
370
+ function hasSlackDnsRequestSignal(err) {
371
+ let current = err;
372
+ const seen = /* @__PURE__ */ new Set();
373
+ for (let depth = 0; current && typeof current === "object" && depth < 6; depth += 1) {
374
+ if (seen.has(current)) return false;
375
+ seen.add(current);
376
+ const code = readSlackRequestErrorCode(current);
377
+ if (code && SLACK_DNS_RETRY_CODES.has(code)) return true;
378
+ const message = readSlackRequestErrorMessage(current);
379
+ if (/\b(EAI_AGAIN|ENOTFOUND|UND_ERR_DNS_RESOLVE_FAILED)\b/i.test(message)) return true;
380
+ current = current.original ?? current.cause;
381
+ }
382
+ return false;
383
+ }
384
+ function delaySlackDnsRetry(attempt) {
385
+ return new Promise((resolve) => setTimeout(resolve, SLACK_DNS_RETRY_BASE_DELAY_MS * Math.max(1, attempt)));
386
+ }
387
+ async function withSlackDnsRequestRetry(operation, fn) {
388
+ for (let attempt = 0;; attempt += 1) try {
389
+ return await fn();
390
+ } catch (err) {
391
+ if (attempt >= SLACK_DNS_RETRY_ATTEMPTS || !hasSlackDnsRequestSignal(err)) throw err;
392
+ logVerbose(`slack send: retrying ${operation} after transient DNS request error (${attempt + 1}/${SLACK_DNS_RETRY_ATTEMPTS})`);
393
+ await delaySlackDnsRetry(attempt + 1);
394
+ }
395
+ }
396
+ function isSlackCustomizeScopeError(err) {
397
+ const data = getSlackWebApiErrorData(err);
398
+ if (normalizeLowercaseStringOrEmpty(normalizeSlackApiString(data?.error)) !== "missing_scope") return false;
399
+ if (normalizeLowercaseStringOrEmpty(normalizeSlackApiString(data?.needed))?.includes("chat:write.customize")) return true;
400
+ return [...normalizeSlackScopeList(data?.response_metadata?.scopes), ...normalizeSlackScopeList(data?.response_metadata?.acceptedScopes)].map((scope) => normalizeLowercaseStringOrEmpty(scope)).includes("chat:write.customize");
401
+ }
402
+ async function postSlackMessageBestEffort(params) {
403
+ const basePayload = buildSlackPostMessagePayload(params);
404
+ const postChatMessage = params.client.chat.postMessage.bind(params.client.chat);
405
+ try {
406
+ const identity = params.identity;
407
+ if (identity?.iconUrl) return await withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage({
408
+ ...basePayload,
409
+ ...identity.username ? { username: identity.username } : {},
410
+ icon_url: identity.iconUrl
411
+ }));
412
+ if (identity?.iconEmoji) return await withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage({
413
+ ...basePayload,
414
+ ...identity.username ? { username: identity.username } : {},
415
+ icon_emoji: identity.iconEmoji
416
+ }));
417
+ return await withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage({
418
+ ...basePayload,
419
+ ...identity?.username ? { username: identity.username } : {}
420
+ }));
421
+ } catch (err) {
422
+ if (!hasCustomIdentity(params.identity) || !isSlackCustomizeScopeError(err)) throw err;
423
+ logVerbose("slack send: missing chat:write.customize, retrying without custom identity");
424
+ return withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage(basePayload));
425
+ }
426
+ }
427
+ function createSlackSendReceipt(params) {
428
+ return createMessageReceiptFromOutboundResults({
429
+ results: params.platformMessageIds.map((messageId) => messageId.trim()).filter((messageId) => messageId && messageId !== "unknown" && messageId !== "suppressed").map((messageId) => {
430
+ const result = {
431
+ channel: "slack",
432
+ messageId
433
+ };
434
+ if (params.channelId) result.channelId = params.channelId;
435
+ return result;
436
+ }),
437
+ kind: params.kind,
438
+ threadId: params.threadTs
439
+ });
440
+ }
441
+ function resolveToken(params) {
442
+ const explicit = resolveSlackBotToken(params.explicit);
443
+ if (explicit) return explicit;
444
+ const fallback = resolveSlackBotToken(params.fallbackToken);
445
+ if (!fallback) {
446
+ logVerbose(`slack send: missing bot token for account=${params.accountId} explicit=${Boolean(params.explicit)} source=${params.fallbackSource ?? "unknown"}`);
447
+ throw new Error(`Slack bot token missing for account "${params.accountId}" (set channels.slack.accounts.${params.accountId}.botToken or SLACK_BOT_TOKEN for default).`);
448
+ }
449
+ return fallback;
450
+ }
451
+ function parseRecipient(raw) {
452
+ const target = parseSlackTarget(raw);
453
+ if (!target) throw new Error("Recipient is required for Slack sends");
454
+ return {
455
+ kind: target.kind,
456
+ id: target.id
457
+ };
458
+ }
459
+ function createSlackSendQueueKey(params) {
460
+ const recipientKey = `${params.recipient.kind === "user" || /^U[A-Z0-9]+$/i.test(params.recipient.id) ? "user" : params.recipient.kind}:${params.recipient.id}`;
461
+ return `${params.accountId}:${createSlackTokenCacheKey(params.token)}:${recipientKey}:${params.threadTs ?? ""}`;
462
+ }
463
+ async function runQueuedSlackSend(key, task) {
464
+ const previous = slackSendQueues.get(key) ?? Promise.resolve();
465
+ let releaseCurrent;
466
+ const current = new Promise((resolve) => {
467
+ releaseCurrent = resolve;
468
+ });
469
+ const queuedCurrent = previous.catch(() => void 0).then(() => current);
470
+ slackSendQueues.set(key, queuedCurrent);
471
+ await previous.catch(() => void 0);
472
+ try {
473
+ return await task();
474
+ } finally {
475
+ releaseCurrent();
476
+ if (slackSendQueues.get(key) === queuedCurrent) slackSendQueues.delete(key);
477
+ }
478
+ }
479
+ function createSlackDmCacheKey(params) {
480
+ return `${params.accountId ?? "default"}:${createSlackTokenCacheKey(params.token)}:${params.recipientId}`;
481
+ }
482
+ function setSlackDmChannelCache(key, channelId) {
483
+ if (slackDmChannelCache.has(key)) slackDmChannelCache.delete(key);
484
+ else if (slackDmChannelCache.size >= SLACK_DM_CHANNEL_CACHE_MAX) {
485
+ const oldest = slackDmChannelCache.keys().next().value;
486
+ if (oldest) slackDmChannelCache.delete(oldest);
487
+ }
488
+ slackDmChannelCache.set(key, channelId);
489
+ }
490
+ function isSlackUserRecipient(recipient) {
491
+ return recipient.kind === "user" || /^U[A-Z0-9]+$/i.test(recipient.id);
492
+ }
493
+ function resolveDirectUserPostChannelId(params) {
494
+ if (!isSlackUserRecipient(params.recipient) || params.hasMedia || params.threadTs) return;
495
+ return params.recipient.id;
496
+ }
497
+ async function resolveChannelId(client, recipient, params) {
498
+ if (!isSlackUserRecipient(recipient)) return { channelId: recipient.id };
499
+ const cacheKey = createSlackDmCacheKey({
500
+ accountId: params.accountId,
501
+ token: params.token,
502
+ recipientId: recipient.id
503
+ });
504
+ const cachedChannelId = slackDmChannelCache.get(cacheKey);
505
+ if (cachedChannelId) return {
506
+ channelId: cachedChannelId,
507
+ isDm: true,
508
+ cacheHit: true
509
+ };
510
+ const channelId = (await withSlackDnsRequestRetry("conversations.open", () => client.conversations.open({ users: recipient.id }))).channel?.id;
511
+ if (!channelId) throw new Error("Failed to open Slack DM channel");
512
+ setSlackDmChannelCache(cacheKey, channelId);
513
+ return {
514
+ channelId,
515
+ isDm: true,
516
+ cacheHit: false
517
+ };
518
+ }
519
+ async function uploadSlackFile(params) {
520
+ const { buffer, contentType, fileName } = await loadOutboundMediaFromUrl(params.mediaUrl, {
521
+ maxBytes: params.maxBytes,
522
+ mediaAccess: params.mediaAccess,
523
+ mediaLocalRoots: params.mediaLocalRoots,
524
+ mediaReadFile: params.mediaReadFile
525
+ });
526
+ const uploadFileName = params.uploadFileName ?? fileName ?? "upload";
527
+ const uploadTitle = params.uploadTitle ?? uploadFileName;
528
+ const uploadUrlResp = await withSlackDnsRequestRetry("files.getUploadURLExternal", () => params.client.files.getUploadURLExternal({
529
+ filename: uploadFileName,
530
+ length: buffer.length
531
+ }));
532
+ if (!uploadUrlResp.ok || !uploadUrlResp.upload_url || !uploadUrlResp.file_id) throw new Error(`Failed to get upload URL: ${uploadUrlResp.error ?? "unknown error"}`);
533
+ const uploadFileId = uploadUrlResp.file_id;
534
+ const uploadBody = new Uint8Array(buffer);
535
+ const { response: uploadResp, release } = await fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode({
536
+ url: uploadUrlResp.upload_url,
537
+ init: {
538
+ method: "POST",
539
+ ...contentType ? { headers: { "Content-Type": contentType } } : {},
540
+ body: uploadBody
541
+ },
542
+ policy: SLACK_UPLOAD_SSRF_POLICY,
543
+ auditContext: "slack-upload-file"
544
+ }));
545
+ try {
546
+ if (!uploadResp.ok) throw new Error(`Failed to upload file: HTTP ${uploadResp.status}`);
547
+ } finally {
548
+ await release();
549
+ }
550
+ const completeResp = await withSlackDnsRequestRetry("files.completeUploadExternal", () => params.client.files.completeUploadExternal({
551
+ files: [{
552
+ id: uploadFileId,
553
+ title: uploadTitle
554
+ }],
555
+ channel_id: params.channelId,
556
+ ...params.caption ? { initial_comment: params.caption } : {},
557
+ ...params.threadTs ? { thread_ts: params.threadTs } : {}
558
+ }));
559
+ if (!completeResp.ok) throw new Error(`Failed to complete upload: ${completeResp.error ?? "unknown error"}`);
560
+ return uploadFileId;
561
+ }
562
+ async function sendMessageSlack(to, message, opts) {
563
+ const trimmedMessage = normalizeOptionalString(message) ?? "";
564
+ if (isSilentReplyText(trimmedMessage) && !opts.mediaUrl && !opts.blocks) {
565
+ logVerbose("slack send: suppressed NO_REPLY token before API call");
566
+ return {
567
+ messageId: "suppressed",
568
+ channelId: "",
569
+ receipt: createSlackSendReceipt({
570
+ platformMessageIds: [],
571
+ kind: "unknown"
572
+ })
573
+ };
574
+ }
575
+ const blocks = opts.blocks == null ? void 0 : validateSlackBlocksArray(opts.blocks);
576
+ if (!trimmedMessage && !opts.mediaUrl && !blocks) throw new Error("Slack send requires text, blocks, or media");
577
+ const cfg = requireRuntimeConfig(opts.cfg, "Slack send");
578
+ const account = resolveSlackAccount({
579
+ cfg,
580
+ accountId: opts.accountId
581
+ });
582
+ const token = resolveToken({
583
+ explicit: opts.token,
584
+ accountId: account.accountId,
585
+ fallbackToken: account.botToken,
586
+ fallbackSource: account.botTokenSource
587
+ });
588
+ const recipient = parseRecipient(to);
589
+ const result = await runQueuedSlackSend(createSlackSendQueueKey({
590
+ accountId: account.accountId,
591
+ token,
592
+ recipient,
593
+ threadTs: opts.threadTs
594
+ }), () => sendMessageSlackQueued({
595
+ trimmedMessage,
596
+ opts,
597
+ cfg,
598
+ account,
599
+ token,
600
+ recipient,
601
+ blocks
602
+ }));
603
+ const threadTs = normalizeSlackThreadTsCandidate(opts.threadTs);
604
+ if (threadTs && result.channelId && account.accountId) recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
605
+ return result;
606
+ }
607
+ async function sendMessageSlackQueued(params) {
608
+ try {
609
+ return await sendMessageSlackQueuedInner(params);
610
+ } catch (err) {
611
+ throw enrichSlackWebApiError(err);
612
+ }
613
+ }
614
+ async function sendMessageSlackQueuedInner(params) {
615
+ const { opts, cfg, account, token, recipient, blocks, trimmedMessage } = params;
616
+ const client = opts.client ?? getSlackWriteClient(token);
617
+ if (opts.replyBroadcast && opts.mediaUrl) throw new Error("Slack replyBroadcast is only supported for text or block thread replies.");
618
+ const unfurl = {
619
+ unfurlLinks: account.config.unfurlLinks,
620
+ unfurlMedia: account.config.unfurlMedia
621
+ };
622
+ const directUserPostChannelId = resolveDirectUserPostChannelId({
623
+ recipient,
624
+ hasMedia: Boolean(opts.mediaUrl),
625
+ ...opts.threadTs ? { threadTs: opts.threadTs } : {}
626
+ });
627
+ const { channelId } = directUserPostChannelId ? { channelId: directUserPostChannelId } : await resolveChannelId(client, recipient, {
628
+ accountId: account.accountId,
629
+ token
630
+ });
631
+ if (blocks) {
632
+ if (opts.mediaUrl) throw new Error("Slack send does not support blocks with mediaUrl");
633
+ const messageId = (await postSlackMessageBestEffort({
634
+ client,
635
+ channelId,
636
+ text: truncateSlackText(trimmedMessage || buildSlackBlocksFallbackText(blocks), 8e3),
637
+ threadTs: opts.threadTs,
638
+ replyBroadcast: opts.replyBroadcast,
639
+ identity: opts.identity,
640
+ blocks,
641
+ unfurl
642
+ })).ts ?? "unknown";
643
+ return {
644
+ messageId,
645
+ channelId,
646
+ receipt: createSlackSendReceipt({
647
+ platformMessageIds: [messageId],
648
+ channelId,
649
+ kind: "card",
650
+ threadTs: opts.threadTs
651
+ })
652
+ };
653
+ }
654
+ const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
655
+ const chunkLimit = Math.min(textLimit, SLACK_TEXT_LIMIT);
656
+ const tableMode = resolveMarkdownTableMode({
657
+ cfg,
658
+ channel: "slack",
659
+ accountId: account.accountId
660
+ });
661
+ const chunkMode = resolveChunkMode(cfg, "slack", account.accountId);
662
+ const resolvedChunks = resolveTextChunksWithFallback(trimmedMessage, (chunkMode === "newline" ? chunkMarkdownTextWithMode(trimmedMessage, chunkLimit, chunkMode) : [trimmedMessage]).flatMap((markdown) => markdownToSlackMrkdwnChunks(markdown, chunkLimit, { tableMode })));
663
+ const mediaMaxBytes = typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : void 0;
664
+ const sentMessageIds = [];
665
+ let lastMessageId = "";
666
+ if (opts.mediaUrl) {
667
+ const [firstChunk, ...rest] = resolvedChunks;
668
+ lastMessageId = await uploadSlackFile({
669
+ client,
670
+ channelId,
671
+ mediaUrl: opts.mediaUrl,
672
+ mediaAccess: opts.mediaAccess,
673
+ uploadFileName: opts.uploadFileName,
674
+ uploadTitle: opts.uploadTitle,
675
+ mediaLocalRoots: opts.mediaLocalRoots,
676
+ mediaReadFile: opts.mediaReadFile,
677
+ caption: firstChunk,
678
+ threadTs: opts.threadTs,
679
+ maxBytes: mediaMaxBytes
680
+ });
681
+ sentMessageIds.push(lastMessageId);
682
+ for (const chunk of rest) {
683
+ const response = await postSlackMessageBestEffort({
684
+ client,
685
+ channelId,
686
+ text: chunk,
687
+ threadTs: opts.threadTs,
688
+ replyBroadcast: sentMessageIds.length === 0 ? opts.replyBroadcast : void 0,
689
+ identity: opts.identity,
690
+ unfurl
691
+ });
692
+ lastMessageId = response.ts ?? lastMessageId;
693
+ if (response.ts) sentMessageIds.push(response.ts);
694
+ }
695
+ } else for (const chunk of resolvedChunks.length ? resolvedChunks : [""]) {
696
+ const response = await postSlackMessageBestEffort({
697
+ client,
698
+ channelId,
699
+ text: chunk,
700
+ threadTs: opts.threadTs,
701
+ replyBroadcast: sentMessageIds.length === 0 ? opts.replyBroadcast : void 0,
702
+ identity: opts.identity,
703
+ unfurl
704
+ });
705
+ lastMessageId = response.ts ?? lastMessageId;
706
+ if (response.ts) sentMessageIds.push(response.ts);
707
+ }
708
+ const messageId = lastMessageId || "unknown";
709
+ return {
710
+ messageId,
711
+ channelId,
712
+ receipt: createSlackSendReceipt({
713
+ platformMessageIds: sentMessageIds.length ? sentMessageIds : [messageId],
714
+ channelId,
715
+ kind: opts.mediaUrl ? "media" : "text",
716
+ threadTs: opts.threadTs
717
+ })
718
+ };
719
+ }
720
+ //#endregion
721
+ export { recordSlackThreadParticipation as a, buildSlackBlocksFallbackText as c, hasSlackThreadParticipationWithPersistence as i, clearSlackThreadParticipationCache as n, markdownToSlackMrkdwnChunks as o, hasSlackThreadParticipation as r, normalizeSlackOutboundText as s, sendMessageSlack as t };