@openclaw/crabline 0.1.9 → 0.1.11
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 +47 -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 +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +43 -0
- package/dist/src/openclaw/artifact-generation.js +363 -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 +28 -0
- package/dist/src/openclaw/private-file.js +465 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +21 -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 +1004 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +334 -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 +549 -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.js +465 -51
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +306 -58
- 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 +5 -0
- package/dist/src/servers/recorder.js +262 -3
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +2 -0
- package/dist/src/servers/signal.js +444 -56
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +5 -0
- package/dist/src/servers/slack.js +515 -49
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +111 -0
- package/dist/src/servers/telegram.js +830 -158
- 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 +12 -7
- package/dist/src/servers/whatsapp.js +269 -107
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +14 -0
- package/dist/src/servers/zalo.js +521 -88
- 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";
|
|
1
|
+
import { createHmac, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { adminAuthError, hasAdminToken, jsonResponse,
|
|
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
|
-
import { recordServerEvent } from "./recorder.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,14 +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
|
-
|
|
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));
|
|
131
253
|
}
|
|
132
254
|
function redactSlackAuthFields(value) {
|
|
133
255
|
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
@@ -171,11 +293,175 @@ function asSlackEventCallback(message) {
|
|
|
171
293
|
type: "event_callback",
|
|
172
294
|
};
|
|
173
295
|
}
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
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";
|
|
178
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) {
|
|
179
465
|
switch (params.method) {
|
|
180
466
|
case "auth.test":
|
|
181
467
|
return slackOk({
|
|
@@ -196,17 +482,30 @@ async function handleSlackApi(params) {
|
|
|
196
482
|
user_id: params.state.botUserId,
|
|
197
483
|
});
|
|
198
484
|
case "chat.postMessage": {
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
491
|
}
|
|
202
492
|
const channel = requireSlackSendTargetId(params.body.channel);
|
|
203
493
|
if (channel instanceof Response) {
|
|
204
494
|
return channel;
|
|
205
495
|
}
|
|
206
|
-
const text =
|
|
207
|
-
const attachments =
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
509
|
if (!text &&
|
|
211
510
|
!hasStructuredMessageContent(blocks) &&
|
|
212
511
|
!hasStructuredMessageContent(attachments)) {
|
|
@@ -237,12 +536,23 @@ async function handleSlackApi(params) {
|
|
|
237
536
|
return slackOk({ channel, message, ts: message.ts });
|
|
238
537
|
}
|
|
239
538
|
case "conversations.open": {
|
|
240
|
-
const users =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
});
|
|
245
554
|
}
|
|
555
|
+
const user = users[0];
|
|
246
556
|
return slackOk({
|
|
247
557
|
channel: {
|
|
248
558
|
id: dmChannelForUser(params.state, user),
|
|
@@ -256,12 +566,20 @@ async function handleSlackApi(params) {
|
|
|
256
566
|
if (channel instanceof Response) {
|
|
257
567
|
return channel;
|
|
258
568
|
}
|
|
569
|
+
const mpim = [...params.state.userMpimChannels.values()].find((candidate) => candidate.id === channel);
|
|
259
570
|
return slackOk({
|
|
260
571
|
channel: {
|
|
261
572
|
id: channel,
|
|
262
573
|
is_channel: channel.startsWith("C"),
|
|
263
|
-
is_group: channel.startsWith("G"),
|
|
574
|
+
is_group: mpim ? false : channel.startsWith("G"),
|
|
264
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
|
+
: {}),
|
|
265
583
|
name: "crabline",
|
|
266
584
|
},
|
|
267
585
|
});
|
|
@@ -279,8 +597,18 @@ async function handleSlackApi(params) {
|
|
|
279
597
|
if (latest instanceof Response) {
|
|
280
598
|
return latest;
|
|
281
599
|
}
|
|
282
|
-
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
|
+
}
|
|
283
608
|
const messages = messagesForChannel(params.state, channel).filter((message) => {
|
|
609
|
+
if (message.thread_ts !== undefined && message.reply_broadcast !== true) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
284
612
|
if (oldest && message.ts <= oldest) {
|
|
285
613
|
return false;
|
|
286
614
|
}
|
|
@@ -289,9 +617,18 @@ async function handleSlackApi(params) {
|
|
|
289
617
|
}
|
|
290
618
|
return true;
|
|
291
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;
|
|
292
626
|
return slackOk({
|
|
293
|
-
has_more:
|
|
294
|
-
messages:
|
|
627
|
+
has_more: hasMore,
|
|
628
|
+
messages: page,
|
|
629
|
+
response_metadata: {
|
|
630
|
+
next_cursor: hasMore && nextBoundary ? encodeSlackCursor("history", nextBoundary) : "",
|
|
631
|
+
},
|
|
295
632
|
});
|
|
296
633
|
}
|
|
297
634
|
case "conversations.replies": {
|
|
@@ -306,12 +643,30 @@ async function handleSlackApi(params) {
|
|
|
306
643
|
if (!ts) {
|
|
307
644
|
return slackError("message_not_found");
|
|
308
645
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
if (messages.length === 0) {
|
|
646
|
+
const threadTs = resolveThreadTs(params.state, { channel, ts });
|
|
647
|
+
if (!threadTs) {
|
|
312
648
|
return slackError("thread_not_found");
|
|
313
649
|
}
|
|
314
|
-
|
|
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
|
+
});
|
|
315
670
|
}
|
|
316
671
|
default:
|
|
317
672
|
return slackError("unknown_method", 404);
|
|
@@ -322,9 +677,17 @@ async function handleAdminInbound(params) {
|
|
|
322
677
|
if (channel instanceof Response) {
|
|
323
678
|
return channel;
|
|
324
679
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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;
|
|
328
691
|
}
|
|
329
692
|
const user = requireSlackUserId(params.body.user ?? "UCRABUSER");
|
|
330
693
|
if (user instanceof Response) {
|
|
@@ -334,18 +697,25 @@ async function handleAdminInbound(params) {
|
|
|
334
697
|
if (threadTs instanceof Response) {
|
|
335
698
|
return threadTs;
|
|
336
699
|
}
|
|
700
|
+
if (threadTs && !hasThreadParent(params.state, { channel, threadTs })) {
|
|
701
|
+
return slackError("thread_not_found");
|
|
702
|
+
}
|
|
337
703
|
const ts = requireSlackThreadTs(params.body.ts);
|
|
338
704
|
if (ts instanceof Response) {
|
|
339
705
|
return ts;
|
|
340
706
|
}
|
|
341
707
|
const message = appendMessage(params.state, createSlackMessage(params.state, {
|
|
708
|
+
attachments,
|
|
709
|
+
blocks,
|
|
342
710
|
channel,
|
|
343
711
|
text,
|
|
344
712
|
threadTs,
|
|
345
713
|
ts,
|
|
346
714
|
user,
|
|
347
715
|
}));
|
|
348
|
-
|
|
716
|
+
const event = asSlackEventCallback(message);
|
|
717
|
+
scheduleSlackEventDelivery(params.state, event);
|
|
718
|
+
return slackOk({ event, message });
|
|
349
719
|
}
|
|
350
720
|
async function handleRequest(params) {
|
|
351
721
|
const url = new URL(params.request.url ?? "/", "http://127.0.0.1");
|
|
@@ -354,9 +724,13 @@ async function handleRequest(params) {
|
|
|
354
724
|
return new Response("not found", { status: 404 });
|
|
355
725
|
}
|
|
356
726
|
if (!hasAdminToken(params.request, params.state.adminToken)) {
|
|
727
|
+
params.request.resume();
|
|
357
728
|
return adminAuthError();
|
|
358
729
|
}
|
|
359
|
-
const body = await
|
|
730
|
+
const body = await parseUnknownRequestBody(params.request);
|
|
731
|
+
if (!isJsonObject(body)) {
|
|
732
|
+
return slackError("invalid_json", 400);
|
|
733
|
+
}
|
|
360
734
|
await appendEvent(params.state, {
|
|
361
735
|
at: new Date().toISOString(),
|
|
362
736
|
body,
|
|
@@ -368,49 +742,138 @@ async function handleRequest(params) {
|
|
|
368
742
|
return await handleAdminInbound({ body, state: params.state });
|
|
369
743
|
}
|
|
370
744
|
const query = queryRecord(url);
|
|
371
|
-
const body = params.request.method === "GET" ? query : await parseRequestBody(params.request);
|
|
372
|
-
await appendEvent(params.state, {
|
|
373
|
-
at: new Date().toISOString(),
|
|
374
|
-
body: redactSlackAuthFields(body),
|
|
375
|
-
method: params.request.method ?? "GET",
|
|
376
|
-
path: url.pathname,
|
|
377
|
-
query: redactSlackAuthQuery(query),
|
|
378
|
-
type: "api",
|
|
379
|
-
});
|
|
380
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
|
+
}
|
|
381
773
|
if (body.type === "url_verification") {
|
|
382
774
|
return jsonResponse({ challenge: readTrimmedString(body.challenge) ?? "" });
|
|
383
775
|
}
|
|
384
|
-
|
|
776
|
+
if (!readTrimmedString(body.type)) {
|
|
777
|
+
return slackError("invalid_payload", 400);
|
|
778
|
+
}
|
|
779
|
+
return new Response(null, { status: 200 });
|
|
385
780
|
}
|
|
386
781
|
const methodMatch = /^\/api\/([a-z]+(?:\.[a-zA-Z]+)*)$/u.exec(url.pathname);
|
|
387
782
|
if (!methodMatch?.[1]) {
|
|
388
783
|
return new Response("not found", { status: 404 });
|
|
389
784
|
}
|
|
390
|
-
|
|
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({
|
|
391
817
|
body,
|
|
392
818
|
method: methodMatch[1],
|
|
393
|
-
request: params.request,
|
|
394
819
|
state: params.state,
|
|
395
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;
|
|
396
834
|
}
|
|
397
835
|
export async function startSlackServer(params = {}) {
|
|
836
|
+
const host = params.host ?? "127.0.0.1";
|
|
837
|
+
const externallyBound = !isLoopbackHost(host);
|
|
398
838
|
const state = {
|
|
839
|
+
activeEventDeliveries: new Set(),
|
|
399
840
|
adminToken: params.adminToken ?? randomBytes(24).toString("base64url"),
|
|
400
841
|
botId: params.botId ?? "BCRABLINE",
|
|
401
842
|
botToken: params.botToken ?? "xoxb-crabline-slack-token",
|
|
402
843
|
botUserId: params.botUserId ?? "UCRABBOT",
|
|
844
|
+
chatPostMessageRateLimit: resolveChatPostMessageRateLimit(params.chatPostMessageRateLimit),
|
|
845
|
+
closing: false,
|
|
846
|
+
deliveryAbortController: new AbortController(),
|
|
847
|
+
eventsRequestUrl: params.eventsRequestUrl,
|
|
403
848
|
nextDmIndex: 1,
|
|
849
|
+
nextMpimIndex: 1,
|
|
404
850
|
nextTsIndex: 100,
|
|
405
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: {
|