@openclaw/crabline 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +23 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +195 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +2 -0
- package/dist/src/servers/matrix.js +466 -52
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +7 -0
- package/dist/src/servers/mattermost.js +307 -59
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +12 -0
- package/dist/src/servers/recorder.js +51 -0
- package/dist/src/servers/recorder.js.map +1 -0
- package/dist/src/servers/signal.d.ts +4 -0
- package/dist/src/servers/signal.js +445 -57
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +7 -0
- package/dist/src/servers/slack.js +516 -50
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +113 -0
- package/dist/src/servers/telegram.js +814 -155
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +14 -7
- package/dist/src/servers/whatsapp.js +272 -110
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +16 -0
- package/dist/src/servers/zalo.js +524 -91
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
1
|
+
import { createHmac, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { adminAuthError, hasAdminToken, jsonResponse,
|
|
3
|
+
import { adminAuthError, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, parseUnknownRequestBody, queryRecord, readBody, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
|
|
5
4
|
import { SLACK_CHANNEL_ID_RULE, SLACK_SEND_TARGET_ID_RULE, SLACK_TS_RULE, SLACK_USER_ID_RULE, } from "../providers/slack-ids.js";
|
|
5
|
+
import { recordCommittedServerEvent, recordServerEvent, } from "./recorder.js";
|
|
6
|
+
const SLACK_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
7
|
+
const SLACK_EVENT_MAX_RETRIES = 3;
|
|
8
|
+
const SLACK_EVENT_MAX_REDIRECTS = 2;
|
|
9
|
+
const SLACK_EVENT_RETRY_DELAYS_MS = [0, 60_000, 5 * 60_000];
|
|
6
10
|
function slackOk(result = {}) {
|
|
7
11
|
return jsonResponse({ ok: true, ...result });
|
|
8
12
|
}
|
|
@@ -18,10 +22,22 @@ function slackRateLimited(retryAfterSeconds = 1) {
|
|
|
18
22
|
status: 429,
|
|
19
23
|
});
|
|
20
24
|
}
|
|
25
|
+
function resolveChatPostMessageRateLimit(value) {
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isSafeInteger(value.remaining) || value.remaining < 0) {
|
|
30
|
+
throw new Error("chatPostMessageRateLimit.remaining must be a non-negative safe integer.");
|
|
31
|
+
}
|
|
32
|
+
if (!Number.isSafeInteger(value.retryAfterSeconds) || value.retryAfterSeconds < 1) {
|
|
33
|
+
throw new Error("chatPostMessageRateLimit.retryAfterSeconds must be a positive safe integer.");
|
|
34
|
+
}
|
|
35
|
+
return { ...value };
|
|
36
|
+
}
|
|
21
37
|
function requireSlackToken(request, body, state) {
|
|
22
38
|
const authorization = request.headers.authorization;
|
|
23
|
-
const tokenFromHeader = typeof authorization === "string"
|
|
24
|
-
? authorization.
|
|
39
|
+
const tokenFromHeader = typeof authorization === "string"
|
|
40
|
+
? /^Bearer\s+(.+)$/iu.exec(authorization.trim())?.[1]?.trim()
|
|
25
41
|
: undefined;
|
|
26
42
|
const token = tokenFromHeader ?? readTrimmedString(body.token);
|
|
27
43
|
if (!token) {
|
|
@@ -76,6 +92,9 @@ function readBoolean(value) {
|
|
|
76
92
|
}
|
|
77
93
|
return undefined;
|
|
78
94
|
}
|
|
95
|
+
function readSlackText(value) {
|
|
96
|
+
return typeof value === "string" ? value : undefined;
|
|
97
|
+
}
|
|
79
98
|
function readStructuredValue(value) {
|
|
80
99
|
if (typeof value !== "string") {
|
|
81
100
|
return value;
|
|
@@ -91,6 +110,34 @@ function readStructuredValue(value) {
|
|
|
91
110
|
return value;
|
|
92
111
|
}
|
|
93
112
|
}
|
|
113
|
+
function readStructuredArray(value, error) {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const parsed = readStructuredValue(value);
|
|
118
|
+
return Array.isArray(parsed) ? parsed : slackError(error);
|
|
119
|
+
}
|
|
120
|
+
function readSlackMetadata(value) {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
let parsed = value;
|
|
125
|
+
if (typeof value === "string") {
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(value);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return slackError("invalid_metadata_format");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!isJsonObject(parsed)) {
|
|
134
|
+
return slackError("invalid_metadata_format");
|
|
135
|
+
}
|
|
136
|
+
if (!readTrimmedString(parsed.event_type) || !isJsonObject(parsed.event_payload)) {
|
|
137
|
+
return slackError("invalid_metadata_schema");
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
94
141
|
function hasStructuredMessageContent(value) {
|
|
95
142
|
if (Array.isArray(value)) {
|
|
96
143
|
return value.length > 0;
|
|
@@ -101,16 +148,27 @@ function messagesForChannel(state, channel) {
|
|
|
101
148
|
return state.messagesByChannel.get(channel) ?? [];
|
|
102
149
|
}
|
|
103
150
|
function hasThreadParent(state, params) {
|
|
104
|
-
return messagesForChannel(state, params.channel).some((message) => message.ts === params.threadTs
|
|
151
|
+
return messagesForChannel(state, params.channel).some((message) => message.ts === params.threadTs && message.thread_ts === undefined);
|
|
152
|
+
}
|
|
153
|
+
function resolveThreadTs(state, params) {
|
|
154
|
+
const message = messagesForChannel(state, params.channel).find((candidate) => candidate.ts === params.ts);
|
|
155
|
+
return message?.thread_ts ?? message?.ts;
|
|
105
156
|
}
|
|
106
157
|
function nextSlackTs(state) {
|
|
107
158
|
const index = state.nextTsIndex++;
|
|
108
159
|
return `${1_700_000_000 + Math.floor(index / 1_000_000)}.${String(index % 1_000_000).padStart(6, "0")}`;
|
|
109
160
|
}
|
|
161
|
+
function randomDecimalDigits(length) {
|
|
162
|
+
return Array.from({ length }, () => randomInt(10)).join("");
|
|
163
|
+
}
|
|
110
164
|
function nextDmChannelId(state) {
|
|
111
165
|
const index = state.nextDmIndex++;
|
|
112
166
|
return `D${String(index).padStart(9, "0")}`;
|
|
113
167
|
}
|
|
168
|
+
function nextMpimChannelId(state) {
|
|
169
|
+
const index = state.nextMpimIndex++;
|
|
170
|
+
return `G${String(index).padStart(9, "0")}`;
|
|
171
|
+
}
|
|
114
172
|
function dmChannelForUser(state, userId) {
|
|
115
173
|
const existing = state.userDmChannels.get(userId);
|
|
116
174
|
if (existing) {
|
|
@@ -120,15 +178,78 @@ function dmChannelForUser(state, userId) {
|
|
|
120
178
|
state.userDmChannels.set(userId, channelId);
|
|
121
179
|
return channelId;
|
|
122
180
|
}
|
|
181
|
+
function mpimChannelForUsers(state, users) {
|
|
182
|
+
const key = [...users].sort().join(",");
|
|
183
|
+
const existing = state.userMpimChannels.get(key);
|
|
184
|
+
if (existing) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const channel = { id: nextMpimChannelId(state), users: [...users] };
|
|
188
|
+
state.userMpimChannels.set(key, channel);
|
|
189
|
+
return channel;
|
|
190
|
+
}
|
|
191
|
+
function requireSlackUsers(value, botUserId) {
|
|
192
|
+
const rawUsers = readTrimmedString(value);
|
|
193
|
+
if (!rawUsers) {
|
|
194
|
+
return slackError("users_list_not_supplied");
|
|
195
|
+
}
|
|
196
|
+
const users = rawUsers.split(",").map((user) => user.trim());
|
|
197
|
+
if (users.length > 8) {
|
|
198
|
+
return slackError("too_many_users");
|
|
199
|
+
}
|
|
200
|
+
if (users.some((user) => !SLACK_USER_ID_RULE.pattern.test(user))) {
|
|
201
|
+
return slackError("user_not_found");
|
|
202
|
+
}
|
|
203
|
+
if (new Set(users).size !== users.length) {
|
|
204
|
+
return slackError("invalid_user_combination");
|
|
205
|
+
}
|
|
206
|
+
if (users.includes(botUserId)) {
|
|
207
|
+
return slackError("invalid_user_combination");
|
|
208
|
+
}
|
|
209
|
+
return users;
|
|
210
|
+
}
|
|
211
|
+
function requireSlackLimit(value) {
|
|
212
|
+
const limit = value === undefined ? 100 : readInteger(value);
|
|
213
|
+
if (limit === undefined || limit < 1 || limit > 1_000) {
|
|
214
|
+
return slackError("invalid_limit");
|
|
215
|
+
}
|
|
216
|
+
return limit;
|
|
217
|
+
}
|
|
218
|
+
function decodeSlackCursor(value, kind) {
|
|
219
|
+
if (value === undefined || value === "") {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/u.test(value)) {
|
|
223
|
+
return slackError("invalid_cursor");
|
|
224
|
+
}
|
|
225
|
+
const cursor = value;
|
|
226
|
+
try {
|
|
227
|
+
const decoded = Buffer.from(cursor, "base64url").toString("utf8");
|
|
228
|
+
const prefix = `${kind}:`;
|
|
229
|
+
if (!decoded.startsWith(prefix)) {
|
|
230
|
+
return slackError("invalid_cursor");
|
|
231
|
+
}
|
|
232
|
+
const boundary = decoded.slice(prefix.length);
|
|
233
|
+
return SLACK_TS_RULE.pattern.test(boundary) && encodeSlackCursor(kind, boundary) === cursor
|
|
234
|
+
? boundary
|
|
235
|
+
: slackError("invalid_cursor");
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return slackError("invalid_cursor");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function encodeSlackCursor(kind, boundary) {
|
|
242
|
+
return Buffer.from(`${kind}:${boundary}`, "utf8").toString("base64url");
|
|
243
|
+
}
|
|
123
244
|
function appendMessage(state, message) {
|
|
124
245
|
const messages = state.messagesByChannel.get(message.channel) ?? [];
|
|
125
246
|
messages.push(message);
|
|
126
247
|
state.messagesByChannel.set(message.channel, messages);
|
|
127
248
|
return message;
|
|
128
249
|
}
|
|
129
|
-
async function appendEvent(state, event) {
|
|
130
|
-
|
|
131
|
-
await
|
|
250
|
+
async function appendEvent(state, event, committed = false) {
|
|
251
|
+
const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
|
|
252
|
+
await (committed ? recordCommittedServerEvent(params) : recordServerEvent(params));
|
|
132
253
|
}
|
|
133
254
|
function redactSlackAuthFields(value) {
|
|
134
255
|
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
@@ -172,11 +293,175 @@ function asSlackEventCallback(message) {
|
|
|
172
293
|
type: "event_callback",
|
|
173
294
|
};
|
|
174
295
|
}
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
296
|
+
function slackRequestSignature(signingSecret, timestamp, body) {
|
|
297
|
+
const digest = createHmac("sha256", signingSecret)
|
|
298
|
+
.update(`v0:${timestamp}:${body}`)
|
|
299
|
+
.digest("hex");
|
|
300
|
+
return `v0=${digest}`;
|
|
301
|
+
}
|
|
302
|
+
function authenticateSlackEventsRequest(request, state, rawBody) {
|
|
303
|
+
const timestampHeader = request.headers["x-slack-request-timestamp"];
|
|
304
|
+
const signatureHeader = request.headers["x-slack-signature"];
|
|
305
|
+
const timestamp = Array.isArray(timestampHeader) ? timestampHeader[0] : timestampHeader;
|
|
306
|
+
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
307
|
+
const timestampSeconds = timestamp ? Number(timestamp) : Number.NaN;
|
|
308
|
+
if (!timestamp ||
|
|
309
|
+
!signature ||
|
|
310
|
+
!Number.isSafeInteger(timestampSeconds) ||
|
|
311
|
+
Math.abs(Date.now() / 1000 - timestampSeconds) > SLACK_SIGNATURE_TOLERANCE_SECONDS) {
|
|
312
|
+
return new Response("unauthorized", { status: 401 });
|
|
313
|
+
}
|
|
314
|
+
const expected = Buffer.from(slackRequestSignature(state.signingSecret, timestamp, rawBody));
|
|
315
|
+
const actual = Buffer.from(signature);
|
|
316
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
317
|
+
return new Response("unauthorized", { status: 401 });
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
function errorChain(error) {
|
|
322
|
+
const chain = [];
|
|
323
|
+
let current = error;
|
|
324
|
+
while (current && typeof current === "object" && chain.length < 4) {
|
|
325
|
+
const entry = current;
|
|
326
|
+
chain.push(entry);
|
|
327
|
+
current = entry.cause;
|
|
328
|
+
}
|
|
329
|
+
return chain;
|
|
330
|
+
}
|
|
331
|
+
function classifySlackRetryReason(error) {
|
|
332
|
+
const chain = errorChain(error);
|
|
333
|
+
const names = chain.map((entry) => String(entry.name ?? ""));
|
|
334
|
+
const codes = chain.map((entry) => String(entry.code ?? "").toUpperCase());
|
|
335
|
+
const messages = chain.map((entry) => String(entry.message ?? "").toLowerCase());
|
|
336
|
+
if (codes.includes("UND_ERR_REDIRECT") ||
|
|
337
|
+
messages.some((message) => message.includes("redirect count exceeded"))) {
|
|
338
|
+
return "too_many_redirects";
|
|
339
|
+
}
|
|
340
|
+
if (names.some((name) => name === "AbortError" || name === "TimeoutError") ||
|
|
341
|
+
codes.some((code) => ["ETIMEDOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT"].includes(code))) {
|
|
342
|
+
return "http_timeout";
|
|
343
|
+
}
|
|
344
|
+
if (codes.some((code) => code.includes("CERT") ||
|
|
345
|
+
code.includes("SSL") ||
|
|
346
|
+
code.includes("TLS") ||
|
|
347
|
+
code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE")) {
|
|
348
|
+
return "ssl_error";
|
|
179
349
|
}
|
|
350
|
+
if (codes.some((code) => [
|
|
351
|
+
"EAI_AGAIN",
|
|
352
|
+
"ECONNREFUSED",
|
|
353
|
+
"ECONNRESET",
|
|
354
|
+
"EHOSTUNREACH",
|
|
355
|
+
"ENETUNREACH",
|
|
356
|
+
"ENOTFOUND",
|
|
357
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
358
|
+
"UND_ERR_SOCKET",
|
|
359
|
+
].includes(code))) {
|
|
360
|
+
return "connection_failed";
|
|
361
|
+
}
|
|
362
|
+
return "unknown_error";
|
|
363
|
+
}
|
|
364
|
+
async function waitForSlackRetry(delayMs, signal) {
|
|
365
|
+
if (signal.aborted) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
return await new Promise((resolve) => {
|
|
369
|
+
let settled = false;
|
|
370
|
+
const finish = (completed) => {
|
|
371
|
+
if (settled) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
settled = true;
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
signal.removeEventListener("abort", onAbort);
|
|
377
|
+
resolve(completed);
|
|
378
|
+
};
|
|
379
|
+
const onAbort = () => finish(false);
|
|
380
|
+
const timer = setTimeout(() => finish(true), delayMs);
|
|
381
|
+
timer.unref();
|
|
382
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
383
|
+
if (signal.aborted) {
|
|
384
|
+
onAbort();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async function deliverSlackEvent(state, event, lifecycleSignal) {
|
|
389
|
+
if (!state.eventsRequestUrl) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const body = JSON.stringify(event);
|
|
393
|
+
let retryReason;
|
|
394
|
+
for (let attempt = 0; attempt <= SLACK_EVENT_MAX_RETRIES; attempt += 1) {
|
|
395
|
+
if (lifecycleSignal.aborted) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
400
|
+
const headers = {
|
|
401
|
+
"content-type": "application/json",
|
|
402
|
+
...(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
|
+
};
|
|
411
|
+
const signal = AbortSignal.any([lifecycleSignal, AbortSignal.timeout(3_000)]);
|
|
412
|
+
let requestUrl = state.eventsRequestUrl;
|
|
413
|
+
let response;
|
|
414
|
+
for (let redirectCount = 0;; redirectCount += 1) {
|
|
415
|
+
response = await fetch(requestUrl, {
|
|
416
|
+
body,
|
|
417
|
+
headers,
|
|
418
|
+
method: "POST",
|
|
419
|
+
redirect: "manual",
|
|
420
|
+
signal,
|
|
421
|
+
});
|
|
422
|
+
const location = response.headers.get("location");
|
|
423
|
+
if (![301, 302].includes(response.status) || !location) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
await response.body?.cancel();
|
|
427
|
+
if (redirectCount >= SLACK_EVENT_MAX_REDIRECTS) {
|
|
428
|
+
response = undefined;
|
|
429
|
+
retryReason = "too_many_redirects";
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
requestUrl = new URL(location, requestUrl).toString();
|
|
433
|
+
}
|
|
434
|
+
if (response) {
|
|
435
|
+
const noRetry = response.headers.get("x-slack-no-retry") === "1";
|
|
436
|
+
await response.body?.cancel();
|
|
437
|
+
if (response.ok || noRetry) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
retryReason = "http_error";
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
if (lifecycleSignal.aborted) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
retryReason = classifySlackRetryReason(error);
|
|
448
|
+
}
|
|
449
|
+
const retryDelay = SLACK_EVENT_RETRY_DELAYS_MS[attempt];
|
|
450
|
+
if (retryDelay !== undefined && !(await waitForSlackRetry(retryDelay, lifecycleSignal))) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function scheduleSlackEventDelivery(state, event) {
|
|
456
|
+
if (!state.eventsRequestUrl || state.closing) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const delivery = deliverSlackEvent(state, event, state.deliveryAbortController.signal).finally(() => {
|
|
460
|
+
state.activeEventDeliveries.delete(delivery);
|
|
461
|
+
});
|
|
462
|
+
state.activeEventDeliveries.add(delivery);
|
|
463
|
+
}
|
|
464
|
+
async function handleSlackApi(params) {
|
|
180
465
|
switch (params.method) {
|
|
181
466
|
case "auth.test":
|
|
182
467
|
return slackOk({
|
|
@@ -197,17 +482,30 @@ async function handleSlackApi(params) {
|
|
|
197
482
|
user_id: params.state.botUserId,
|
|
198
483
|
});
|
|
199
484
|
case "chat.postMessage": {
|
|
200
|
-
|
|
201
|
-
|
|
485
|
+
const rateLimit = params.state.chatPostMessageRateLimit;
|
|
486
|
+
if (rateLimit && rateLimit.remaining <= 0) {
|
|
487
|
+
return slackRateLimited(rateLimit.retryAfterSeconds);
|
|
488
|
+
}
|
|
489
|
+
if (rateLimit) {
|
|
490
|
+
rateLimit.remaining -= 1;
|
|
202
491
|
}
|
|
203
492
|
const channel = requireSlackSendTargetId(params.body.channel);
|
|
204
493
|
if (channel instanceof Response) {
|
|
205
494
|
return channel;
|
|
206
495
|
}
|
|
207
|
-
const text =
|
|
208
|
-
const attachments =
|
|
209
|
-
|
|
210
|
-
|
|
496
|
+
const text = readSlackText(params.body.text) ?? "";
|
|
497
|
+
const attachments = readStructuredArray(params.body.attachments, "invalid_attachments");
|
|
498
|
+
if (attachments instanceof Response) {
|
|
499
|
+
return attachments;
|
|
500
|
+
}
|
|
501
|
+
const blocks = readStructuredArray(params.body.blocks, "invalid_blocks");
|
|
502
|
+
if (blocks instanceof Response) {
|
|
503
|
+
return blocks;
|
|
504
|
+
}
|
|
505
|
+
const metadata = readSlackMetadata(params.body.metadata);
|
|
506
|
+
if (metadata instanceof Response) {
|
|
507
|
+
return metadata;
|
|
508
|
+
}
|
|
211
509
|
if (!text &&
|
|
212
510
|
!hasStructuredMessageContent(blocks) &&
|
|
213
511
|
!hasStructuredMessageContent(attachments)) {
|
|
@@ -238,12 +536,23 @@ async function handleSlackApi(params) {
|
|
|
238
536
|
return slackOk({ channel, message, ts: message.ts });
|
|
239
537
|
}
|
|
240
538
|
case "conversations.open": {
|
|
241
|
-
const users =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
539
|
+
const users = requireSlackUsers(params.body.users, params.state.botUserId);
|
|
540
|
+
if (users instanceof Response) {
|
|
541
|
+
return users;
|
|
542
|
+
}
|
|
543
|
+
if (users.length > 1) {
|
|
544
|
+
const channel = mpimChannelForUsers(params.state, users);
|
|
545
|
+
return slackOk({
|
|
546
|
+
channel: {
|
|
547
|
+
id: channel.id,
|
|
548
|
+
is_group: false,
|
|
549
|
+
is_mpim: true,
|
|
550
|
+
is_private: true,
|
|
551
|
+
members: [params.state.botUserId, ...channel.users],
|
|
552
|
+
},
|
|
553
|
+
});
|
|
246
554
|
}
|
|
555
|
+
const user = users[0];
|
|
247
556
|
return slackOk({
|
|
248
557
|
channel: {
|
|
249
558
|
id: dmChannelForUser(params.state, user),
|
|
@@ -257,12 +566,20 @@ async function handleSlackApi(params) {
|
|
|
257
566
|
if (channel instanceof Response) {
|
|
258
567
|
return channel;
|
|
259
568
|
}
|
|
569
|
+
const mpim = [...params.state.userMpimChannels.values()].find((candidate) => candidate.id === channel);
|
|
260
570
|
return slackOk({
|
|
261
571
|
channel: {
|
|
262
572
|
id: channel,
|
|
263
573
|
is_channel: channel.startsWith("C"),
|
|
264
|
-
is_group: channel.startsWith("G"),
|
|
574
|
+
is_group: mpim ? false : channel.startsWith("G"),
|
|
265
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
|
+
: {}),
|
|
266
583
|
name: "crabline",
|
|
267
584
|
},
|
|
268
585
|
});
|
|
@@ -280,8 +597,18 @@ async function handleSlackApi(params) {
|
|
|
280
597
|
if (latest instanceof Response) {
|
|
281
598
|
return latest;
|
|
282
599
|
}
|
|
283
|
-
const limit =
|
|
600
|
+
const limit = requireSlackLimit(params.body.limit);
|
|
601
|
+
if (limit instanceof Response) {
|
|
602
|
+
return limit;
|
|
603
|
+
}
|
|
604
|
+
const boundary = decodeSlackCursor(params.body.cursor, "history");
|
|
605
|
+
if (boundary instanceof Response) {
|
|
606
|
+
return boundary;
|
|
607
|
+
}
|
|
284
608
|
const messages = messagesForChannel(params.state, channel).filter((message) => {
|
|
609
|
+
if (message.thread_ts !== undefined && message.reply_broadcast !== true) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
285
612
|
if (oldest && message.ts <= oldest) {
|
|
286
613
|
return false;
|
|
287
614
|
}
|
|
@@ -290,9 +617,18 @@ async function handleSlackApi(params) {
|
|
|
290
617
|
}
|
|
291
618
|
return true;
|
|
292
619
|
});
|
|
620
|
+
const ordered = [...messages]
|
|
621
|
+
.reverse()
|
|
622
|
+
.filter((message) => boundary === undefined || message.ts < boundary);
|
|
623
|
+
const page = ordered.slice(0, limit);
|
|
624
|
+
const hasMore = page.length < ordered.length;
|
|
625
|
+
const nextBoundary = page.at(-1)?.ts;
|
|
293
626
|
return slackOk({
|
|
294
|
-
has_more:
|
|
295
|
-
messages:
|
|
627
|
+
has_more: hasMore,
|
|
628
|
+
messages: page,
|
|
629
|
+
response_metadata: {
|
|
630
|
+
next_cursor: hasMore && nextBoundary ? encodeSlackCursor("history", nextBoundary) : "",
|
|
631
|
+
},
|
|
296
632
|
});
|
|
297
633
|
}
|
|
298
634
|
case "conversations.replies": {
|
|
@@ -307,12 +643,30 @@ async function handleSlackApi(params) {
|
|
|
307
643
|
if (!ts) {
|
|
308
644
|
return slackError("message_not_found");
|
|
309
645
|
}
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
if (messages.length === 0) {
|
|
646
|
+
const threadTs = resolveThreadTs(params.state, { channel, ts });
|
|
647
|
+
if (!threadTs) {
|
|
313
648
|
return slackError("thread_not_found");
|
|
314
649
|
}
|
|
315
|
-
|
|
650
|
+
const limit = requireSlackLimit(params.body.limit);
|
|
651
|
+
if (limit instanceof Response) {
|
|
652
|
+
return limit;
|
|
653
|
+
}
|
|
654
|
+
const boundary = decodeSlackCursor(params.body.cursor, "replies");
|
|
655
|
+
if (boundary instanceof Response) {
|
|
656
|
+
return boundary;
|
|
657
|
+
}
|
|
658
|
+
const messages = messagesForChannel(params.state, channel).filter((message) => (message.ts === threadTs || message.thread_ts === threadTs) &&
|
|
659
|
+
(boundary === undefined || message.ts > boundary));
|
|
660
|
+
const page = messages.slice(0, limit);
|
|
661
|
+
const hasMore = page.length < messages.length;
|
|
662
|
+
const nextBoundary = page.at(-1)?.ts;
|
|
663
|
+
return slackOk({
|
|
664
|
+
has_more: hasMore,
|
|
665
|
+
messages: page,
|
|
666
|
+
response_metadata: {
|
|
667
|
+
next_cursor: hasMore && nextBoundary ? encodeSlackCursor("replies", nextBoundary) : "",
|
|
668
|
+
},
|
|
669
|
+
});
|
|
316
670
|
}
|
|
317
671
|
default:
|
|
318
672
|
return slackError("unknown_method", 404);
|
|
@@ -323,9 +677,17 @@ async function handleAdminInbound(params) {
|
|
|
323
677
|
if (channel instanceof Response) {
|
|
324
678
|
return channel;
|
|
325
679
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
680
|
+
if (params.body.text !== undefined && typeof params.body.text !== "string") {
|
|
681
|
+
return slackError("invalid_text", 400);
|
|
682
|
+
}
|
|
683
|
+
const text = readSlackText(params.body.text) ?? "";
|
|
684
|
+
const attachments = readStructuredArray(params.body.attachments, "invalid_attachments");
|
|
685
|
+
if (attachments instanceof Response) {
|
|
686
|
+
return attachments;
|
|
687
|
+
}
|
|
688
|
+
const blocks = readStructuredArray(params.body.blocks, "invalid_blocks");
|
|
689
|
+
if (blocks instanceof Response) {
|
|
690
|
+
return blocks;
|
|
329
691
|
}
|
|
330
692
|
const user = requireSlackUserId(params.body.user ?? "UCRABUSER");
|
|
331
693
|
if (user instanceof Response) {
|
|
@@ -335,18 +697,25 @@ async function handleAdminInbound(params) {
|
|
|
335
697
|
if (threadTs instanceof Response) {
|
|
336
698
|
return threadTs;
|
|
337
699
|
}
|
|
700
|
+
if (threadTs && !hasThreadParent(params.state, { channel, threadTs })) {
|
|
701
|
+
return slackError("thread_not_found");
|
|
702
|
+
}
|
|
338
703
|
const ts = requireSlackThreadTs(params.body.ts);
|
|
339
704
|
if (ts instanceof Response) {
|
|
340
705
|
return ts;
|
|
341
706
|
}
|
|
342
707
|
const message = appendMessage(params.state, createSlackMessage(params.state, {
|
|
708
|
+
attachments,
|
|
709
|
+
blocks,
|
|
343
710
|
channel,
|
|
344
711
|
text,
|
|
345
712
|
threadTs,
|
|
346
713
|
ts,
|
|
347
714
|
user,
|
|
348
715
|
}));
|
|
349
|
-
|
|
716
|
+
const event = asSlackEventCallback(message);
|
|
717
|
+
scheduleSlackEventDelivery(params.state, event);
|
|
718
|
+
return slackOk({ event, message });
|
|
350
719
|
}
|
|
351
720
|
async function handleRequest(params) {
|
|
352
721
|
const url = new URL(params.request.url ?? "/", "http://127.0.0.1");
|
|
@@ -355,9 +724,13 @@ async function handleRequest(params) {
|
|
|
355
724
|
return new Response("not found", { status: 404 });
|
|
356
725
|
}
|
|
357
726
|
if (!hasAdminToken(params.request, params.state.adminToken)) {
|
|
727
|
+
params.request.resume();
|
|
358
728
|
return adminAuthError();
|
|
359
729
|
}
|
|
360
|
-
const body = await
|
|
730
|
+
const body = await parseUnknownRequestBody(params.request);
|
|
731
|
+
if (!isJsonObject(body)) {
|
|
732
|
+
return slackError("invalid_json", 400);
|
|
733
|
+
}
|
|
361
734
|
await appendEvent(params.state, {
|
|
362
735
|
at: new Date().toISOString(),
|
|
363
736
|
body,
|
|
@@ -369,48 +742,138 @@ async function handleRequest(params) {
|
|
|
369
742
|
return await handleAdminInbound({ body, state: params.state });
|
|
370
743
|
}
|
|
371
744
|
const query = queryRecord(url);
|
|
372
|
-
const body = params.request.method === "GET" ? query : await parseRequestBody(params.request);
|
|
373
|
-
await appendEvent(params.state, {
|
|
374
|
-
at: new Date().toISOString(),
|
|
375
|
-
body: redactSlackAuthFields(body),
|
|
376
|
-
method: params.request.method ?? "GET",
|
|
377
|
-
path: url.pathname,
|
|
378
|
-
query: redactSlackAuthQuery(query),
|
|
379
|
-
type: "api",
|
|
380
|
-
});
|
|
381
745
|
if (url.pathname === "/slack/events") {
|
|
746
|
+
if (params.request.method !== "POST") {
|
|
747
|
+
params.request.resume();
|
|
748
|
+
return new Response("method not allowed", {
|
|
749
|
+
headers: { allow: "POST" },
|
|
750
|
+
status: 405,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (!params.request.headers["x-slack-request-timestamp"] ||
|
|
754
|
+
!params.request.headers["x-slack-signature"]) {
|
|
755
|
+
params.request.resume();
|
|
756
|
+
return new Response("unauthorized", { status: 401 });
|
|
757
|
+
}
|
|
758
|
+
const rawBody = (await readBody(params.request)).toString("utf8");
|
|
759
|
+
const authError = authenticateSlackEventsRequest(params.request, params.state, rawBody);
|
|
760
|
+
if (authError) {
|
|
761
|
+
return authError;
|
|
762
|
+
}
|
|
763
|
+
let body;
|
|
764
|
+
try {
|
|
765
|
+
body = rawBody ? JSON.parse(rawBody) : {};
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
throw new InvalidJsonBodyError(error);
|
|
769
|
+
}
|
|
770
|
+
if (!isJsonObject(body)) {
|
|
771
|
+
return slackError("invalid_json", 400);
|
|
772
|
+
}
|
|
382
773
|
if (body.type === "url_verification") {
|
|
383
774
|
return jsonResponse({ challenge: readTrimmedString(body.challenge) ?? "" });
|
|
384
775
|
}
|
|
385
|
-
|
|
776
|
+
if (!readTrimmedString(body.type)) {
|
|
777
|
+
return slackError("invalid_payload", 400);
|
|
778
|
+
}
|
|
779
|
+
return new Response(null, { status: 200 });
|
|
386
780
|
}
|
|
387
781
|
const methodMatch = /^\/api\/([a-z]+(?:\.[a-zA-Z]+)*)$/u.exec(url.pathname);
|
|
388
782
|
if (!methodMatch?.[1]) {
|
|
389
783
|
return new Response("not found", { status: 404 });
|
|
390
784
|
}
|
|
391
|
-
|
|
785
|
+
const requestMethod = params.request.method ?? "GET";
|
|
786
|
+
if (requestMethod !== "GET" && requestMethod !== "POST") {
|
|
787
|
+
params.request.resume();
|
|
788
|
+
return new Response("method not allowed", {
|
|
789
|
+
headers: { allow: "GET, POST" },
|
|
790
|
+
status: 405,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (params.request.headers.authorization) {
|
|
794
|
+
const headerAuthError = requireSlackToken(params.request, {}, params.state);
|
|
795
|
+
if (headerAuthError) {
|
|
796
|
+
params.request.resume();
|
|
797
|
+
return headerAuthError;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const body = requestMethod === "GET" ? query : await parseUnknownRequestBody(params.request);
|
|
801
|
+
if (!isJsonObject(body)) {
|
|
802
|
+
return slackError("json_not_object", 400);
|
|
803
|
+
}
|
|
804
|
+
const authError = requireSlackToken(params.request, body, params.state);
|
|
805
|
+
if (authError) {
|
|
806
|
+
return authError;
|
|
807
|
+
}
|
|
808
|
+
const event = {
|
|
809
|
+
at: new Date().toISOString(),
|
|
810
|
+
body: redactSlackAuthFields(body),
|
|
811
|
+
method: params.request.method ?? "GET",
|
|
812
|
+
path: url.pathname,
|
|
813
|
+
query: redactSlackAuthQuery(query),
|
|
814
|
+
type: "api",
|
|
815
|
+
};
|
|
816
|
+
const response = await handleSlackApi({
|
|
392
817
|
body,
|
|
393
818
|
method: methodMatch[1],
|
|
394
|
-
request: params.request,
|
|
395
819
|
state: params.state,
|
|
396
820
|
});
|
|
821
|
+
const mutation = ["chat.postMessage", "conversations.open"].includes(methodMatch[1]);
|
|
822
|
+
const payload = mutation
|
|
823
|
+
? (await response
|
|
824
|
+
.clone()
|
|
825
|
+
.json()
|
|
826
|
+
.catch(() => undefined))
|
|
827
|
+
: undefined;
|
|
828
|
+
const committed = isJsonObject(payload) && payload.ok === true;
|
|
829
|
+
if (methodMatch[1] === "chat.postMessage") {
|
|
830
|
+
event.accepted = committed;
|
|
831
|
+
}
|
|
832
|
+
await appendEvent(params.state, event, committed);
|
|
833
|
+
return response;
|
|
397
834
|
}
|
|
398
835
|
export async function startSlackServer(params = {}) {
|
|
836
|
+
const host = params.host ?? "127.0.0.1";
|
|
837
|
+
const externallyBound = !isLoopbackHost(host);
|
|
399
838
|
const state = {
|
|
839
|
+
activeEventDeliveries: new Set(),
|
|
400
840
|
adminToken: params.adminToken ?? randomBytes(24).toString("base64url"),
|
|
401
841
|
botId: params.botId ?? "BCRABLINE",
|
|
402
842
|
botToken: params.botToken ?? "xoxb-crabline-slack-token",
|
|
403
843
|
botUserId: params.botUserId ?? "UCRABBOT",
|
|
844
|
+
chatPostMessageRateLimit: resolveChatPostMessageRateLimit(params.chatPostMessageRateLimit),
|
|
845
|
+
closing: false,
|
|
846
|
+
deliveryAbortController: new AbortController(),
|
|
847
|
+
eventsRequestUrl: params.eventsRequestUrl,
|
|
404
848
|
nextDmIndex: 1,
|
|
849
|
+
nextMpimIndex: 1,
|
|
405
850
|
nextTsIndex: 100,
|
|
851
|
+
onEvent: params.onEvent,
|
|
406
852
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "slack.jsonl"),
|
|
407
853
|
signingSecret: params.signingSecret ?? "crabline-slack-signing-secret",
|
|
408
854
|
userDmChannels: new Map(),
|
|
855
|
+
userMpimChannels: new Map(),
|
|
409
856
|
messagesByChannel: new Map(),
|
|
410
857
|
};
|
|
411
|
-
|
|
858
|
+
if (externallyBound && !params.botToken) {
|
|
859
|
+
const generatedBotValue = `xoxb-${randomDecimalDigits(12)}-${randomDecimalDigits(12)}-${randomBytes(18).toString("base64url")}`;
|
|
860
|
+
state.botToken = generatedBotValue;
|
|
861
|
+
}
|
|
862
|
+
if (externallyBound && !params.signingSecret) {
|
|
863
|
+
const generatedSigningValue = randomBytes(16).toString("hex");
|
|
864
|
+
state.signingSecret = generatedSigningValue;
|
|
865
|
+
}
|
|
412
866
|
const httpServer = await startHttpJsonServer({
|
|
413
867
|
handle: (request) => handleRequest({ request, state }),
|
|
868
|
+
handleError: (error) => {
|
|
869
|
+
if (error instanceof InvalidJsonBodyError) {
|
|
870
|
+
return slackError("invalid_json", 400);
|
|
871
|
+
}
|
|
872
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
873
|
+
return slackError("request_too_large", 413);
|
|
874
|
+
}
|
|
875
|
+
return undefined;
|
|
876
|
+
},
|
|
414
877
|
host,
|
|
415
878
|
port: params.port ?? 0,
|
|
416
879
|
serverName: "Slack",
|
|
@@ -419,6 +882,9 @@ export async function startSlackServer(params = {}) {
|
|
|
419
882
|
const apiRoot = `${baseUrl}/api/`;
|
|
420
883
|
return {
|
|
421
884
|
async close() {
|
|
885
|
+
state.closing = true;
|
|
886
|
+
state.deliveryAbortController.abort();
|
|
887
|
+
await Promise.allSettled(state.activeEventDeliveries);
|
|
422
888
|
await httpServer.close();
|
|
423
889
|
},
|
|
424
890
|
manifest: {
|