stitchkit 0.100.2 → 0.101.0
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/CHANGELOG.md +153 -0
- package/README.md +2 -1
- package/dist/entrypoints/agent-runtime/browser.js +119 -119
- package/dist/entrypoints/agent-runtime/coding-tools.js +15 -20
- package/dist/entrypoints/agent-runtime/harness.js +55 -63
- package/dist/entrypoints/agent-runtime/openrouter.js +6 -6
- package/dist/entrypoints/agent-runtime/sandbox.js +20 -25
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +11 -13
- package/dist/entrypoints/agent-runtime/sqlite/node.js +11 -13
- package/dist/entrypoints/agent-runtime/testing.js +32 -34
- package/dist/entrypoints/agent-runtime.js +395 -402
- package/dist/entrypoints/application/diagnostic-journal.js +26 -27
- package/dist/entrypoints/application/directory-inbox.js +16 -17
- package/dist/entrypoints/application/grammy.js +13 -13
- package/dist/entrypoints/application/opentelemetry.js +13 -13
- package/dist/entrypoints/application/schemas.js +24 -24
- package/dist/entrypoints/application.js +209 -210
- package/dist/entrypoints/bin/upgrade-cli.js +2 -2
- package/dist/entrypoints/cli.js +81 -84
- package/dist/entrypoints/contract.js +54 -57
- package/dist/entrypoints/declaration.js +22 -22
- package/dist/entrypoints/files.js +18 -21
- package/dist/entrypoints/geo.js +7 -8
- package/dist/entrypoints/google.js +2 -2
- package/dist/entrypoints/index.js +150 -154
- package/dist/entrypoints/live.js +45 -47
- package/dist/entrypoints/node.js +68 -74
- package/dist/entrypoints/oauth.js +6 -6
- package/dist/entrypoints/observability.d.ts +1 -0
- package/dist/entrypoints/observability.d.ts.map +1 -1
- package/dist/entrypoints/observability.js +116 -87
- package/dist/entrypoints/primitives.js +35 -35
- package/dist/entrypoints/react/keyboard.d.ts +4 -0
- package/dist/entrypoints/react/keyboard.d.ts.map +1 -0
- package/dist/entrypoints/react/keyboard.js +397 -0
- package/dist/entrypoints/react.js +10 -10
- package/dist/entrypoints/release.js +7 -7
- package/dist/entrypoints/remote.js +15 -17
- package/dist/entrypoints/server.js +198 -200
- package/dist/entrypoints/telegram/html.d.ts +5 -0
- package/dist/entrypoints/telegram/html.d.ts.map +1 -0
- package/dist/entrypoints/telegram/html.js +26 -0
- package/dist/entrypoints/telegram/screens.d.ts +14 -0
- package/dist/entrypoints/telegram/screens.d.ts.map +1 -0
- package/dist/entrypoints/telegram/screens.js +2028 -0
- package/dist/entrypoints/telegram.d.ts +11 -3
- package/dist/entrypoints/telegram.d.ts.map +1 -1
- package/dist/entrypoints/telegram.js +779 -159
- package/dist/entrypoints/testing.js +75 -79
- package/dist/entrypoints/tools/connections.js +30 -32
- package/dist/entrypoints/tools/contract.js +18 -20
- package/dist/entrypoints/tools/invoker.js +12 -17
- package/dist/entrypoints/tools.js +224 -229
- package/dist/entrypoints/tracking/server.js +6 -6
- package/dist/entrypoints/tracking.js +40 -42
- package/dist/entrypoints/voice.js +3 -3
- package/dist/{index-eaysmdtq.js → index-0ndk4555.js} +56 -56
- package/dist/{index-2bxbyab0.js → index-1bx1xv1k.js} +1 -1
- package/dist/{index-p5r7qpdd.js → index-1eeqpadm.js} +4 -4
- package/dist/{index-hadmp193.js → index-3ct7hhq6.js} +210 -210
- package/dist/{index-e01r054k.js → index-45tdvqa3.js} +26 -26
- package/dist/{index-dce5wxcq.js → index-4s0fep5q.js} +8 -8
- package/dist/index-50ejfz75.js +9 -0
- package/dist/{index-m9zn6cpq.js → index-5107q189.js} +12 -12
- package/dist/{index-sbdmyz75.js → index-5cc6r990.js} +3 -3
- package/dist/{index-c6bxbsnc.js → index-7kxw1awn.js} +2 -2
- package/dist/{index-0r0fbk7z.js → index-7z9ym7t8.js} +28 -28
- package/dist/{index-p7hgdyhb.js → index-814xpr6d.js} +2 -2
- package/dist/{index-gtj3zc53.js → index-8kzct1d6.js} +99 -99
- package/dist/{index-1zbbzgfh.js → index-9pw0a51a.js} +4 -4
- package/dist/{index-79z7m7dt.js → index-a9pd417t.js} +35 -35
- package/dist/{index-ham5sz8m.js → index-aaj97r4y.js} +45 -45
- package/dist/{index-s812ycat.js → index-aan6hqb9.js} +3 -3
- package/dist/{index-10x2hbx0.js → index-adhgd4pe.js} +44 -44
- package/dist/{index-2gxzjvyr.js → index-afjvjyck.js} +23 -23
- package/dist/{index-qmy6zq75.js → index-ahvsksw5.js} +16 -16
- package/dist/{index-hr4tzbwq.js → index-ap3gqe56.js} +3 -3
- package/dist/{index-znfs20ss.js → index-b2bz0kdm.js} +16 -16
- package/dist/{index-r066jx76.js → index-c6aagrbd.js} +148 -120
- package/dist/{index-k30h66yd.js → index-ck71tbcf.js} +9 -9
- package/dist/{index-eqqvgdb3.js → index-ct75c3at.js} +2 -2
- package/dist/{index-gw8bmyaj.js → index-djjhhgjk.js} +41 -41
- package/dist/{index-8qt2m66q.js → index-dvwj1f43.js} +43 -43
- package/dist/index-e05cjgtf.js +144 -0
- package/dist/{index-yw4fzmdz.js → index-e6c2spsd.js} +79 -30
- package/dist/{index-fhwqwxe0.js → index-ej331hah.js} +1 -1
- package/dist/{index-9t3dffv2.js → index-ewbaywk5.js} +5 -5
- package/dist/{index-7p1f0vp2.js → index-fyphpknh.js} +25 -25
- package/dist/{index-83bdg97s.js → index-g149e9pk.js} +1 -1
- package/dist/{index-ngfhvfpd.js → index-g6a3na27.js} +4 -4
- package/dist/{index-qwhynv6x.js → index-h124epcf.js} +20 -20
- package/dist/{index-h6eeys1v.js → index-h97tqqp1.js} +9 -9
- package/dist/{index-zcgf3gqf.js → index-hby4qhax.js} +10 -10
- package/dist/{index-5jrd0yng.js → index-j8aw2c0e.js} +4 -4
- package/dist/{index-d8m8zpd8.js → index-jzczzcsb.js} +14 -14
- package/dist/index-kqnd6a85.js +26 -0
- package/dist/{index-35mh5bhj.js → index-kw1ws0cs.js} +12 -12
- package/dist/{index-4gcnakhx.js → index-m38ej7e5.js} +2 -2
- package/dist/{index-ndddxtyx.js → index-m79yr938.js} +19 -19
- package/dist/{index-5p2m7we1.js → index-m9fndta6.js} +11 -11
- package/dist/index-mnkk1ffz.js +150 -0
- package/dist/index-n15jny5b.js +4 -0
- package/dist/{index-2vtcmrj9.js → index-n53mswt3.js} +20 -20
- package/dist/{index-bt14acb5.js → index-n5yqxd3d.js} +21 -21
- package/dist/{index-bm5rx7n6.js → index-n9tyz8z3.js} +12 -12
- package/dist/index-nmaqxdx7.js +463 -0
- package/dist/{index-mqv3b5rh.js → index-nx6eazt0.js} +30 -30
- package/dist/{index-3z73fh2c.js → index-q0th8q0z.js} +9 -9
- package/dist/{index-v260df89.js → index-qjeggp8w.js} +20 -20
- package/dist/{index-zfrhfn82.js → index-r1z0rd9c.js} +20 -20
- package/dist/{index-wqgjfstn.js → index-r5c4fedv.js} +3 -3
- package/dist/{index-cyktc4bp.js → index-r5hsgkz5.js} +118 -118
- package/dist/{index-0b1ess5e.js → index-r7a7r7ha.js} +1 -1
- package/dist/{index-1j8pm5t0.js → index-rfkh44ss.js} +15 -15
- package/dist/index-rz3z13e9.js +47 -0
- package/dist/{index-k6gpggb9.js → index-s80awwsw.js} +35 -35
- package/dist/{index-vqr7f7v8.js → index-ssmd41kh.js} +124 -124
- package/dist/index-sznqf8fx.js +20 -0
- package/dist/{index-ensd0r74.js → index-v14qqmys.js} +41 -41
- package/dist/{index-paz6sgdp.js → index-v4a2fjtk.js} +36 -36
- package/dist/{index-d9pk3td4.js → index-w0eqdhyd.js} +14 -14
- package/dist/{index-5qn1kx34.js → index-w575evhq.js} +29 -29
- package/dist/{index-3gye9wzb.js → index-w5s3xftz.js} +26 -26
- package/dist/{index-tm88avnj.js → index-wykebpn9.js} +3 -3
- package/dist/{index-sh3s86na.js → index-xkhk5d6g.js} +17 -17
- package/dist/{index-6k1937bx.js → index-xqhede6x.js} +4 -4
- package/dist/{index-f4fx2tza.js → index-zs4ms3th.js} +181 -181
- package/dist/{index-k5hsfc3j.js → index-zsvzhjtk.js} +1 -1
- package/dist/internal/secret-shapes.d.ts +21 -0
- package/dist/internal/secret-shapes.d.ts.map +1 -0
- package/dist/observability/bounded-logger.d.ts +2 -0
- package/dist/observability/bounded-logger.d.ts.map +1 -1
- package/dist/observability/sanitize.d.ts +6 -0
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/observability/secret-values.d.ts +25 -0
- package/dist/observability/secret-values.d.ts.map +1 -0
- package/dist/react/keyboard/hooks.d.ts +71 -0
- package/dist/react/keyboard/hooks.d.ts.map +1 -0
- package/dist/react/keyboard/layers.d.ts +98 -0
- package/dist/react/keyboard/layers.d.ts.map +1 -0
- package/dist/react/keyboard/zone.d.ts +139 -0
- package/dist/react/keyboard/zone.d.ts.map +1 -0
- package/dist/telegram/bot-api.d.ts +7 -4
- package/dist/telegram/bot-api.d.ts.map +1 -1
- package/dist/telegram/crypto.d.ts +16 -0
- package/dist/telegram/crypto.d.ts.map +1 -0
- package/dist/telegram/html/check.d.ts +26 -0
- package/dist/telegram/html/check.d.ts.map +1 -0
- package/dist/telegram/html/nodes.d.ts +70 -0
- package/dist/telegram/html/nodes.d.ts.map +1 -0
- package/dist/telegram/html/parse.d.ts +35 -0
- package/dist/telegram/html/parse.d.ts.map +1 -0
- package/dist/telegram/html/render.d.ts +39 -0
- package/dist/telegram/html/render.d.ts.map +1 -0
- package/dist/telegram/init-data.d.ts +3 -44
- package/dist/telegram/init-data.d.ts.map +1 -1
- package/dist/telegram/operator-channel.d.ts +19 -3
- package/dist/telegram/operator-channel.d.ts.map +1 -1
- package/dist/telegram/operator-dedupe.d.ts +44 -0
- package/dist/telegram/operator-dedupe.d.ts.map +1 -0
- package/dist/telegram/screens/callback-codec.d.ts +34 -0
- package/dist/telegram/screens/callback-codec.d.ts.map +1 -0
- package/dist/telegram/screens/dispatch.d.ts +22 -0
- package/dist/telegram/screens/dispatch.d.ts.map +1 -0
- package/dist/telegram/screens/display.d.ts +70 -0
- package/dist/telegram/screens/display.d.ts.map +1 -0
- package/dist/telegram/screens/execute.d.ts +57 -0
- package/dist/telegram/screens/execute.d.ts.map +1 -0
- package/dist/telegram/screens/html.d.ts +36 -0
- package/dist/telegram/screens/html.d.ts.map +1 -0
- package/dist/telegram/screens/outcome.d.ts +74 -0
- package/dist/telegram/screens/outcome.d.ts.map +1 -0
- package/dist/telegram/screens/path.d.ts +29 -0
- package/dist/telegram/screens/path.d.ts.map +1 -0
- package/dist/telegram/screens/reconcile.d.ts +67 -0
- package/dist/telegram/screens/reconcile.d.ts.map +1 -0
- package/dist/telegram/screens/registry.d.ts +23 -0
- package/dist/telegram/screens/registry.d.ts.map +1 -0
- package/dist/telegram/screens/render.d.ts +70 -0
- package/dist/telegram/screens/render.d.ts.map +1 -0
- package/dist/telegram/screens/runtime.d.ts +133 -0
- package/dist/telegram/screens/runtime.d.ts.map +1 -0
- package/dist/telegram/screens/screen-types.d.ts +130 -0
- package/dist/telegram/screens/screen-types.d.ts.map +1 -0
- package/dist/telegram/screens/screen.d.ts +138 -0
- package/dist/telegram/screens/screen.d.ts.map +1 -0
- package/dist/telegram/screens/state.d.ts +115 -0
- package/dist/telegram/screens/state.d.ts.map +1 -0
- package/dist/telegram/screens/telegram-screens.d.ts +18 -0
- package/dist/telegram/screens/telegram-screens.d.ts.map +1 -0
- package/dist/telegram/screens/test-chat-telegram.d.ts +47 -0
- package/dist/telegram/screens/test-chat-telegram.d.ts.map +1 -0
- package/dist/telegram/screens/test-chat.d.ts +66 -0
- package/dist/telegram/screens/test-chat.d.ts.map +1 -0
- package/dist/telegram/screens/view.d.ts +89 -0
- package/dist/telegram/screens/view.d.ts.map +1 -0
- package/dist/telegram/send-failure.d.ts +25 -0
- package/dist/telegram/send-failure.d.ts.map +1 -1
- package/dist/telegram/update-intake.d.ts +82 -0
- package/dist/telegram/update-intake.d.ts.map +1 -0
- package/dist/telegram/update-store-check.d.ts +22 -0
- package/dist/telegram/update-store-check.d.ts.map +1 -0
- package/dist/telegram/update-store-postgres.d.ts +33 -0
- package/dist/telegram/update-store-postgres.d.ts.map +1 -0
- package/dist/telegram/update-store-sqlite.d.ts +26 -0
- package/dist/telegram/update-store-sqlite.d.ts.map +1 -0
- package/dist/telegram/update-store.d.ts +67 -0
- package/dist/telegram/update-store.d.ts.map +1 -0
- package/dist/telegram/user.d.ts +51 -0
- package/dist/telegram/user.d.ts.map +1 -0
- package/dist/telegram/webhook.d.ts +105 -0
- package/dist/telegram/webhook.d.ts.map +1 -0
- package/dist/tools/cli/args-fields.d.ts +8 -0
- package/dist/tools/cli/args-fields.d.ts.map +1 -1
- package/dist/tools/cli/args-route.d.ts.map +1 -1
- package/dist/tools/cli/args.d.ts.map +1 -1
- package/dist/tools/cli/help.d.ts.map +1 -1
- package/llms/cli.txt +8 -2
- package/llms/observability.txt +15 -2
- package/llms/react-keyboard.txt +193 -0
- package/llms/server.4.txt +16 -0
- package/llms/stitchkit.txt +10 -4
- package/llms/telegram-html.txt +35 -0
- package/llms/telegram-screens.txt +365 -0
- package/llms/telegram.txt +245 -11
- package/llms/{upgrading-0.74.0-0.100.0.txt → upgrading-0.74.0-0.101.0.txt} +39 -3
- package/llms/upgrading.txt +1 -1
- package/llms-full.txt +889 -19
- package/llms.txt +12 -7
- package/package.json +17 -2
- package/dist/index-2k4yrqkc.js +0 -9
- package/dist/index-crykmqkr.js +0 -26
- package/dist/index-y64k3s0h.js +0 -150
- package/dist/index-y91ry9h2.js +0 -4
- /package/dist/{index-hffjpmq9.js → index-9jfdm5tg.js} +0 -0
- /package/dist/{index-zjjyynm3.js → index-d4pa2bgs.js} +0 -0
- /package/dist/{index-rxfy4cq7.js → index-dwfv9a85.js} +0 -0
- /package/dist/{index-5jm36zv1.js → index-gfk7nrht.js} +0 -0
- /package/dist/{index-b9aefrqd.js → index-n1dqr7gf.js} +0 -0
- /package/dist/{index-q3pyb9n0.js → index-sykb5ag7.js} +0 -0
- /package/dist/{index-48ffdxgk.js → index-xhzq2qvj.js} +0 -0
- /package/dist/{index-kzfs85xp.js → index-zsarb5xp.js} +0 -0
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../index-
|
|
2
|
+
BOT_TOKEN_SECRET
|
|
3
|
+
} from "../index-sznqf8fx.js";
|
|
4
|
+
import {
|
|
5
|
+
TELEGRAM_SEND_FAILURE_REASONS2,
|
|
6
|
+
classifyTelegramSendFailure2
|
|
7
|
+
} from "../index-e05cjgtf.js";
|
|
8
|
+
import {
|
|
9
|
+
TELEGRAM_TEXT_LIMIT2,
|
|
10
|
+
truncateTelegramHtml2
|
|
11
|
+
} from "../index-nmaqxdx7.js";
|
|
12
|
+
import {
|
|
13
|
+
isRecord,
|
|
14
|
+
transportResult
|
|
15
|
+
} from "../index-9jfdm5tg.js";
|
|
4
16
|
|
|
5
17
|
// src/telegram/bot-api.ts
|
|
6
|
-
var TELEGRAM_BOT_TOKEN_PATTERN =
|
|
18
|
+
var TELEGRAM_BOT_TOKEN_PATTERN = BOT_TOKEN_SECRET;
|
|
19
|
+
function redactTelegramBotToken(text) {
|
|
20
|
+
return text.replace(new RegExp(BOT_TOKEN_SECRET.source, "g"), "[redacted]");
|
|
21
|
+
}
|
|
7
22
|
var DEFAULT_API_ROOT = "https://api.telegram.org";
|
|
8
23
|
|
|
9
24
|
class TelegramBotApiError extends Error {
|
|
@@ -175,122 +190,6 @@ async function lockBroadcast(directory, files) {
|
|
|
175
190
|
throw new Error("Telegram broadcast lock could not be taken");
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
// src/telegram/send-failure.ts
|
|
179
|
-
var BY_DESCRIPTION = [
|
|
180
|
-
[/bot was blocked by the user|bot was kicked/i, "blocked-by-user"],
|
|
181
|
-
[/user is deactivated|account is deactivated/i, "user-deactivated"],
|
|
182
|
-
[/can'?t initiate conversation|need to start a conversation/i, "not-started"],
|
|
183
|
-
[/chat not found|peer_id_invalid|user not found/i, "chat-not-found"],
|
|
184
|
-
[/too many requests|retry later|flood/i, "rate-limited"],
|
|
185
|
-
[
|
|
186
|
-
/message is too long|can'?t parse entities|message text is empty|wrong file identifier|button_url_invalid/i,
|
|
187
|
-
"message-invalid"
|
|
188
|
-
]
|
|
189
|
-
];
|
|
190
|
-
var RETRYABLE = new Set([
|
|
191
|
-
"rate-limited",
|
|
192
|
-
"server-error"
|
|
193
|
-
]);
|
|
194
|
-
var UNREACHABLE = new Set([
|
|
195
|
-
"blocked-by-user",
|
|
196
|
-
"user-deactivated",
|
|
197
|
-
"chat-not-found",
|
|
198
|
-
"not-started"
|
|
199
|
-
]);
|
|
200
|
-
function numberAt(value, keys) {
|
|
201
|
-
if (!isRecord(value))
|
|
202
|
-
return;
|
|
203
|
-
for (const key of keys) {
|
|
204
|
-
const candidate = value[key];
|
|
205
|
-
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
206
|
-
return candidate;
|
|
207
|
-
}
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
function statusOf(value, depth = 0) {
|
|
211
|
-
if (!isRecord(value) || depth > 4)
|
|
212
|
-
return;
|
|
213
|
-
const direct = numberAt(value, ["error_code", "statusCode", "status"]);
|
|
214
|
-
if (direct !== undefined && direct >= 400 && direct < 600)
|
|
215
|
-
return direct;
|
|
216
|
-
const nested = value.cause ?? value.response ?? value.error;
|
|
217
|
-
return nested === value ? undefined : statusOf(nested, depth + 1);
|
|
218
|
-
}
|
|
219
|
-
function retryAfterOf(value, depth = 0) {
|
|
220
|
-
if (!isRecord(value) || depth > 4)
|
|
221
|
-
return;
|
|
222
|
-
const direct = numberAt(value.parameters, ["retry_after"]);
|
|
223
|
-
if (direct !== undefined)
|
|
224
|
-
return direct;
|
|
225
|
-
const nested = value.cause ?? value.response ?? value.error;
|
|
226
|
-
return nested === value ? undefined : retryAfterOf(nested, depth + 1);
|
|
227
|
-
}
|
|
228
|
-
function descriptionOf(value, depth = 0) {
|
|
229
|
-
if (typeof value === "string")
|
|
230
|
-
return value;
|
|
231
|
-
if (value instanceof Error) {
|
|
232
|
-
const nested2 = isRecord(value) ? descriptionOf(value.cause, depth + 1) : "";
|
|
233
|
-
return `${value.message} ${nested2}`.trim();
|
|
234
|
-
}
|
|
235
|
-
if (!isRecord(value) || depth > 4)
|
|
236
|
-
return "";
|
|
237
|
-
const own = typeof value.description === "string" ? value.description : "";
|
|
238
|
-
const message = typeof value.message === "string" ? value.message : "";
|
|
239
|
-
const nested = descriptionOf(value.cause ?? value.response ?? value.error, depth + 1);
|
|
240
|
-
return [own, message, nested].filter(Boolean).join(" ");
|
|
241
|
-
}
|
|
242
|
-
function classifyTelegramSendFailure(error) {
|
|
243
|
-
const status = statusOf(error);
|
|
244
|
-
const retryAfterSeconds = retryAfterOf(error);
|
|
245
|
-
const description = descriptionOf(error);
|
|
246
|
-
if (retryAfterSeconds !== undefined) {
|
|
247
|
-
return {
|
|
248
|
-
reason: "rate-limited",
|
|
249
|
-
...status !== undefined && { status },
|
|
250
|
-
retryAfterSeconds,
|
|
251
|
-
retryable: true,
|
|
252
|
-
recipientUnreachable: false,
|
|
253
|
-
evidence: "parameters"
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
for (const [pattern, reason] of BY_DESCRIPTION) {
|
|
257
|
-
if (pattern.test(description)) {
|
|
258
|
-
return {
|
|
259
|
-
reason,
|
|
260
|
-
...status !== undefined && { status },
|
|
261
|
-
retryable: RETRYABLE.has(reason),
|
|
262
|
-
recipientUnreachable: UNREACHABLE.has(reason),
|
|
263
|
-
evidence: "description"
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (status === 429) {
|
|
268
|
-
return {
|
|
269
|
-
reason: "rate-limited",
|
|
270
|
-
status,
|
|
271
|
-
retryable: true,
|
|
272
|
-
recipientUnreachable: false,
|
|
273
|
-
evidence: "status"
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
if (status !== undefined && status >= 500) {
|
|
277
|
-
return {
|
|
278
|
-
reason: "server-error",
|
|
279
|
-
status,
|
|
280
|
-
retryable: true,
|
|
281
|
-
recipientUnreachable: false,
|
|
282
|
-
evidence: "status"
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
reason: "unknown",
|
|
287
|
-
...status !== undefined && { status },
|
|
288
|
-
retryable: false,
|
|
289
|
-
recipientUnreachable: false,
|
|
290
|
-
evidence: "none"
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
193
|
// src/telegram/broadcast.ts
|
|
295
194
|
function pause(milliseconds) {
|
|
296
195
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -376,7 +275,7 @@ async function runTelegramBroadcast(config) {
|
|
|
376
275
|
await config.send({ recipient, attempt });
|
|
377
276
|
return { outcome: "delivered" };
|
|
378
277
|
} catch (error) {
|
|
379
|
-
const failure =
|
|
278
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
380
279
|
if (failure.recipientUnreachable)
|
|
381
280
|
return { outcome: "unreachable", reason: failure.reason };
|
|
382
281
|
if (failure.reason === "message-invalid")
|
|
@@ -444,9 +343,28 @@ function telegramBroadcastSender(config) {
|
|
|
444
343
|
...config.fetch && { fetch: config.fetch }
|
|
445
344
|
});
|
|
446
345
|
}
|
|
447
|
-
// src/telegram/
|
|
346
|
+
// src/telegram/crypto.ts
|
|
347
|
+
var encoder = new TextEncoder;
|
|
348
|
+
async function hmacSha256(key, message) {
|
|
349
|
+
const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
350
|
+
return crypto.subtle.sign("HMAC", imported, encoder.encode(message));
|
|
351
|
+
}
|
|
352
|
+
function toHex(buffer) {
|
|
353
|
+
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
354
|
+
}
|
|
355
|
+
function digestsEqual(a, b) {
|
|
356
|
+
if (a.length !== b.length)
|
|
357
|
+
return false;
|
|
358
|
+
let difference = 0;
|
|
359
|
+
for (let index = 0;index < a.length; index += 1) {
|
|
360
|
+
difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
361
|
+
}
|
|
362
|
+
return difference === 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/telegram/user.ts
|
|
448
366
|
import { z as z2 } from "zod";
|
|
449
|
-
var
|
|
367
|
+
var TelegramUserSchema = z2.object({
|
|
450
368
|
id: z2.int(),
|
|
451
369
|
is_bot: z2.boolean().optional(),
|
|
452
370
|
first_name: z2.string(),
|
|
@@ -467,28 +385,21 @@ var TelegramInitDataUserSchema = z2.object({
|
|
|
467
385
|
...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
|
|
468
386
|
...user.photo_url !== undefined && { photoUrl: user.photo_url }
|
|
469
387
|
}));
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return crypto.subtle.sign("HMAC", imported, encoder.encode(message));
|
|
474
|
-
}
|
|
475
|
-
function toHex(buffer) {
|
|
476
|
-
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
477
|
-
}
|
|
478
|
-
function digestsEqual(a, b) {
|
|
479
|
-
if (a.length !== b.length)
|
|
480
|
-
return false;
|
|
481
|
-
let difference = 0;
|
|
482
|
-
for (let index = 0;index < a.length; index += 1) {
|
|
483
|
-
difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
484
|
-
}
|
|
485
|
-
return difference === 0;
|
|
388
|
+
function parseTelegramUser(value) {
|
|
389
|
+
const parsed = TelegramUserSchema.safeParse(value);
|
|
390
|
+
return parsed.success ? parsed.data : undefined;
|
|
486
391
|
}
|
|
392
|
+
|
|
393
|
+
// src/telegram/init-data.ts
|
|
394
|
+
var encoder2 = new TextEncoder;
|
|
487
395
|
function optionalUser(raw, key) {
|
|
488
396
|
const encoded = raw[key];
|
|
489
397
|
if (encoded === undefined)
|
|
490
398
|
return;
|
|
491
|
-
|
|
399
|
+
const user = parseTelegramUser(JSON.parse(encoded));
|
|
400
|
+
if (user === undefined)
|
|
401
|
+
throw new TypeError(`initData ${key} is not a Telegram user`);
|
|
402
|
+
return user;
|
|
492
403
|
}
|
|
493
404
|
async function verifyTelegramInitData(options) {
|
|
494
405
|
if (!options.botToken)
|
|
@@ -508,7 +419,7 @@ async function verifyTelegramInitData(options) {
|
|
|
508
419
|
}
|
|
509
420
|
const dataCheckString = Object.keys(signed).sort().map((key) => `${key}=${signed[key]}`).join(`
|
|
510
421
|
`);
|
|
511
|
-
const secretKey = await hmacSha256(
|
|
422
|
+
const secretKey = await hmacSha256(encoder2.encode("WebAppData"), options.botToken);
|
|
512
423
|
const expected = toHex(await hmacSha256(secretKey, dataCheckString));
|
|
513
424
|
if (!digestsEqual(expected, hash.toLowerCase())) {
|
|
514
425
|
return { valid: false, reason: "signature-mismatch" };
|
|
@@ -634,8 +545,57 @@ function createTelegramLocalFiles(config) {
|
|
|
634
545
|
}
|
|
635
546
|
};
|
|
636
547
|
}
|
|
548
|
+
// src/telegram/operator-dedupe.ts
|
|
549
|
+
function defaultFingerprint(text, topic) {
|
|
550
|
+
const blanked = text.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "#").replace(/\b[a-z0-9]{20,}\b/gi, "#").replace(/\d+/g, "#");
|
|
551
|
+
return `${topic ?? ""}\x00${blanked.slice(0, 300)}`;
|
|
552
|
+
}
|
|
553
|
+
function createTelegramOperatorDedupe(options = {}) {
|
|
554
|
+
const windowMs = options.windowMs ?? 600000;
|
|
555
|
+
const maxPerWindow = options.maxPerWindow ?? 20;
|
|
556
|
+
const fingerprint = options.fingerprint ?? defaultFingerprint;
|
|
557
|
+
const now = options.now ?? Date.now;
|
|
558
|
+
const repeatedLine = options.repeatedLine ?? ((count) => `(+${count} more like this)`);
|
|
559
|
+
const overBudgetLine = options.overBudgetLine ?? ((count) => `(+${count} more messages over the limit)`);
|
|
560
|
+
const recent = new Map;
|
|
561
|
+
let windowStart = Number.NEGATIVE_INFINITY;
|
|
562
|
+
let sentInWindow = 0;
|
|
563
|
+
let overBudget = 0;
|
|
564
|
+
return (text, topic) => {
|
|
565
|
+
const at = now();
|
|
566
|
+
const key = fingerprint(text, topic);
|
|
567
|
+
const previous = recent.get(key);
|
|
568
|
+
if (previous && at - previous.sentAt < windowMs) {
|
|
569
|
+
previous.held += 1;
|
|
570
|
+
return { send: false, reason: "repeated" };
|
|
571
|
+
}
|
|
572
|
+
if (at - windowStart >= windowMs) {
|
|
573
|
+
windowStart = at;
|
|
574
|
+
sentInWindow = 0;
|
|
575
|
+
}
|
|
576
|
+
if (sentInWindow >= maxPerWindow) {
|
|
577
|
+
overBudget += 1;
|
|
578
|
+
return { send: false, reason: "over-budget" };
|
|
579
|
+
}
|
|
580
|
+
sentInWindow += 1;
|
|
581
|
+
for (const [stale, entry] of recent) {
|
|
582
|
+
if (at - entry.sentAt >= windowMs)
|
|
583
|
+
recent.delete(stale);
|
|
584
|
+
}
|
|
585
|
+
recent.set(key, { sentAt: at, held: 0 });
|
|
586
|
+
const lines = [text];
|
|
587
|
+
if (previous?.held)
|
|
588
|
+
lines.push(repeatedLine(previous.held));
|
|
589
|
+
if (overBudget > 0)
|
|
590
|
+
lines.push(overBudgetLine(overBudget));
|
|
591
|
+
overBudget = 0;
|
|
592
|
+
return { send: true, text: lines.join(`
|
|
593
|
+
|
|
594
|
+
`) };
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
637
598
|
// src/telegram/operator-channel.ts
|
|
638
|
-
var TEXT_LIMIT = 4096;
|
|
639
599
|
var MASK = "[redacted]";
|
|
640
600
|
function masked(text, patterns) {
|
|
641
601
|
let output = text;
|
|
@@ -648,12 +608,12 @@ function literal(value) {
|
|
|
648
608
|
return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
649
609
|
}
|
|
650
610
|
function timer(milliseconds, signal) {
|
|
651
|
-
return new Promise((
|
|
611
|
+
return new Promise((resolve, reject) => {
|
|
652
612
|
if (signal.aborted)
|
|
653
613
|
return reject(signal.reason);
|
|
654
614
|
const handle = setTimeout(() => {
|
|
655
615
|
signal.removeEventListener("abort", stop);
|
|
656
|
-
|
|
616
|
+
resolve();
|
|
657
617
|
}, milliseconds);
|
|
658
618
|
const stop = () => {
|
|
659
619
|
clearTimeout(handle);
|
|
@@ -664,14 +624,19 @@ function timer(milliseconds, signal) {
|
|
|
664
624
|
}
|
|
665
625
|
function fitted(text) {
|
|
666
626
|
const points = Array.from(text);
|
|
667
|
-
return points.length <=
|
|
627
|
+
return points.length <= TELEGRAM_TEXT_LIMIT2 ? text : `${points.slice(0, TELEGRAM_TEXT_LIMIT2 - 1).join("")}…`;
|
|
668
628
|
}
|
|
669
629
|
function createTelegramOperatorChannel(config) {
|
|
670
630
|
const maxQueued = config.maxQueued ?? 200;
|
|
671
631
|
const minIntervalMs = config.minIntervalMs ?? 3000;
|
|
672
632
|
const maxAttempts = config.maxAttempts ?? 3;
|
|
673
633
|
const sleep = config.sleep ?? timer;
|
|
674
|
-
const patterns = [
|
|
634
|
+
const patterns = [
|
|
635
|
+
TELEGRAM_BOT_TOKEN_PATTERN,
|
|
636
|
+
...config.sensitivePatterns ?? [],
|
|
637
|
+
...[...config.sensitiveValues ?? []].filter((value) => value !== "").sort((left, right) => right.length - left.length).map(literal)
|
|
638
|
+
];
|
|
639
|
+
const admit = config.dedupe ? createTelegramOperatorDedupe(config.dedupe) : undefined;
|
|
675
640
|
const queue = [];
|
|
676
641
|
const idle = new Set;
|
|
677
642
|
let lifetime = new AbortController;
|
|
@@ -708,7 +673,7 @@ function createTelegramOperatorChannel(config) {
|
|
|
708
673
|
} catch (error) {
|
|
709
674
|
if (signal.aborted)
|
|
710
675
|
throw error;
|
|
711
|
-
const failure =
|
|
676
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
712
677
|
if (!failure.retryable) {
|
|
713
678
|
drop(item, "refused", failure);
|
|
714
679
|
return;
|
|
@@ -759,6 +724,13 @@ function createTelegramOperatorChannel(config) {
|
|
|
759
724
|
drop(item, "closed");
|
|
760
725
|
return;
|
|
761
726
|
}
|
|
727
|
+
const verdict = admit?.(item.text, topic);
|
|
728
|
+
if (verdict && !verdict.send) {
|
|
729
|
+
drop(item, verdict.reason);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (verdict)
|
|
733
|
+
item.text = verdict.text;
|
|
762
734
|
queue.push(item);
|
|
763
735
|
while (queue.length > maxQueued) {
|
|
764
736
|
const oldest = queue.shift();
|
|
@@ -777,11 +749,11 @@ function createTelegramOperatorChannel(config) {
|
|
|
777
749
|
drain(signal) {
|
|
778
750
|
if (queue.length === 0 && inFlight === 0)
|
|
779
751
|
return Promise.resolve();
|
|
780
|
-
return new Promise((
|
|
752
|
+
return new Promise((resolve) => {
|
|
781
753
|
const done = () => {
|
|
782
754
|
signal?.removeEventListener("abort", done);
|
|
783
755
|
idle.delete(done);
|
|
784
|
-
|
|
756
|
+
resolve();
|
|
785
757
|
};
|
|
786
758
|
idle.add(done);
|
|
787
759
|
signal?.addEventListener("abort", done, { once: true });
|
|
@@ -797,6 +769,9 @@ function createTelegramOperatorChannel(config) {
|
|
|
797
769
|
}
|
|
798
770
|
};
|
|
799
771
|
}
|
|
772
|
+
function markupFitted(text, parseMode) {
|
|
773
|
+
return parseMode === "HTML" ? truncateTelegramHtml2(text) : text;
|
|
774
|
+
}
|
|
800
775
|
function telegramOperatorSender(config) {
|
|
801
776
|
const token = literal(config.token);
|
|
802
777
|
return (message, signal) => callTelegramBotApi({
|
|
@@ -804,7 +779,7 @@ function telegramOperatorSender(config) {
|
|
|
804
779
|
method: "sendMessage",
|
|
805
780
|
params: {
|
|
806
781
|
chat_id: message.chatId,
|
|
807
|
-
text: masked(message.text, [token]),
|
|
782
|
+
text: markupFitted(masked(message.text, [token]), config.parseMode),
|
|
808
783
|
...message.threadId !== undefined && { message_thread_id: message.threadId },
|
|
809
784
|
...config.parseMode && { parse_mode: config.parseMode },
|
|
810
785
|
link_preview_options: { is_disabled: true }
|
|
@@ -814,16 +789,661 @@ function telegramOperatorSender(config) {
|
|
|
814
789
|
...config.fetch && { fetch: config.fetch }
|
|
815
790
|
});
|
|
816
791
|
}
|
|
792
|
+
// src/telegram/update-intake.ts
|
|
793
|
+
var SWEEP_BATCH = 100;
|
|
794
|
+
function defaultRetry(error, attempt) {
|
|
795
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
796
|
+
if (failure.evidence !== "none" && !failure.retryable)
|
|
797
|
+
return false;
|
|
798
|
+
const backoff = Math.min(5000 * 4 ** (attempt - 1), 15 * 60000);
|
|
799
|
+
return Math.max(backoff, (failure.retryAfterSeconds ?? 0) * 1000);
|
|
800
|
+
}
|
|
801
|
+
function lineOf(update) {
|
|
802
|
+
for (const [key, payload] of Object.entries(update)) {
|
|
803
|
+
if (key === "update_id" || !isRecord(payload))
|
|
804
|
+
continue;
|
|
805
|
+
const message = isRecord(payload.message) ? payload.message : undefined;
|
|
806
|
+
for (const holder of [
|
|
807
|
+
payload.chat,
|
|
808
|
+
message?.chat,
|
|
809
|
+
payload.from,
|
|
810
|
+
payload.user,
|
|
811
|
+
payload.voter_chat
|
|
812
|
+
]) {
|
|
813
|
+
if (isRecord(holder) && (typeof holder.id === "number" || typeof holder.id === "string")) {
|
|
814
|
+
return `chat:${holder.id}`;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return `update:${String(update.update_id)}`;
|
|
819
|
+
}
|
|
820
|
+
function parsed(body) {
|
|
821
|
+
try {
|
|
822
|
+
const value = JSON.parse(body);
|
|
823
|
+
return isRecord(value) && Number.isSafeInteger(value.update_id) && Number(value.update_id) >= 0 ? value : undefined;
|
|
824
|
+
} catch {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function handlerError(error) {
|
|
829
|
+
return error instanceof Error && error.name === "BotError" && "ctx" in error && "error" in error ? error.error : error;
|
|
830
|
+
}
|
|
831
|
+
function positive(name, value) {
|
|
832
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
833
|
+
throw new RangeError(`[stitchkit] telegram update intake: ${name} is a positive integer`);
|
|
834
|
+
}
|
|
835
|
+
return value;
|
|
836
|
+
}
|
|
837
|
+
function createTelegramUpdateIntake(config) {
|
|
838
|
+
const { store } = config;
|
|
839
|
+
const now = config.now ?? Date.now;
|
|
840
|
+
const retry = config.retry ?? defaultRetry;
|
|
841
|
+
const maxAttempts = positive("maxAttempts", config.maxAttempts ?? 5);
|
|
842
|
+
const leaseMs = positive("leaseMs", config.leaseMs ?? 120000);
|
|
843
|
+
const pendingGraceMs = config.pendingGraceMs ?? 60000;
|
|
844
|
+
const sweepEveryMs = positive("sweepEveryMs", config.sweepEveryMs ?? 30000);
|
|
845
|
+
const retainMs = config.retainMs ?? 86400000;
|
|
846
|
+
const maxConcurrent = positive("maxConcurrent", config.maxConcurrent ?? 32);
|
|
847
|
+
const lines = new Map;
|
|
848
|
+
const scheduled = new Set;
|
|
849
|
+
const waiting = [];
|
|
850
|
+
let running = 0;
|
|
851
|
+
let started = false;
|
|
852
|
+
let timer;
|
|
853
|
+
const storeError = (error, step) => {
|
|
854
|
+
try {
|
|
855
|
+
config.onStoreError?.(error, step);
|
|
856
|
+
} catch {}
|
|
857
|
+
};
|
|
858
|
+
const slot = async () => {
|
|
859
|
+
if (running < maxConcurrent) {
|
|
860
|
+
running += 1;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
864
|
+
};
|
|
865
|
+
const release = () => {
|
|
866
|
+
const next = waiting.shift();
|
|
867
|
+
if (next)
|
|
868
|
+
next();
|
|
869
|
+
else
|
|
870
|
+
running -= 1;
|
|
871
|
+
};
|
|
872
|
+
const attempt = async (update, updateId) => {
|
|
873
|
+
const taken = now();
|
|
874
|
+
const number = await store.claim(updateId, { now: taken, leaseUntil: taken + leaseMs, maxAttempts }).catch((error) => storeError(error, "claim"));
|
|
875
|
+
if (number === undefined)
|
|
876
|
+
return;
|
|
877
|
+
const renewal = setInterval(() => {
|
|
878
|
+
store.renew(updateId, number, now() + leaseMs).catch((error) => storeError(error, "renew"));
|
|
879
|
+
}, Math.max(1, Math.floor(leaseMs / 4)));
|
|
880
|
+
let settlement;
|
|
881
|
+
try {
|
|
882
|
+
await config.handle(transportResult(update));
|
|
883
|
+
settlement = { state: "completed", at: now() };
|
|
884
|
+
} catch (thrown) {
|
|
885
|
+
const error = handlerError(thrown);
|
|
886
|
+
const delay = number >= maxAttempts ? false : retry(error, number);
|
|
887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
888
|
+
settlement = delay === false ? { state: "abandoned", at: now(), error: message } : { state: "failed", at: now(), retryAt: now() + delay, error: message };
|
|
889
|
+
try {
|
|
890
|
+
config.onFailure?.({ updateId, attempt: number, error, retrying: delay !== false });
|
|
891
|
+
} catch {}
|
|
892
|
+
} finally {
|
|
893
|
+
clearInterval(renewal);
|
|
894
|
+
}
|
|
895
|
+
await store.settle(updateId, number, settlement).catch((error) => storeError(error, "settle"));
|
|
896
|
+
};
|
|
897
|
+
const schedule = (update) => {
|
|
898
|
+
const updateId = Number(update.update_id);
|
|
899
|
+
if (!started || scheduled.has(updateId))
|
|
900
|
+
return;
|
|
901
|
+
scheduled.add(updateId);
|
|
902
|
+
const line = lineOf(update);
|
|
903
|
+
const next = (lines.get(line) ?? Promise.resolve()).then(async () => {
|
|
904
|
+
await slot();
|
|
905
|
+
try {
|
|
906
|
+
await attempt(update, updateId);
|
|
907
|
+
} finally {
|
|
908
|
+
release();
|
|
909
|
+
}
|
|
910
|
+
}).finally(() => {
|
|
911
|
+
scheduled.delete(updateId);
|
|
912
|
+
if (lines.get(line) === next)
|
|
913
|
+
lines.delete(line);
|
|
914
|
+
});
|
|
915
|
+
lines.set(line, next);
|
|
916
|
+
};
|
|
917
|
+
const sweep = async (pendingBefore) => {
|
|
918
|
+
const at = now();
|
|
919
|
+
const due = await store.due({ now: at, pendingBefore, limit: SWEEP_BATCH });
|
|
920
|
+
let count = 0;
|
|
921
|
+
for (const row of due) {
|
|
922
|
+
const update = parsed(row.body);
|
|
923
|
+
if (update && !scheduled.has(row.updateId)) {
|
|
924
|
+
schedule(update);
|
|
925
|
+
count += 1;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
await store.prune(at - retainMs);
|
|
929
|
+
return count;
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
async accept(body) {
|
|
933
|
+
const update = parsed(body);
|
|
934
|
+
if (!update)
|
|
935
|
+
return "invalid";
|
|
936
|
+
const updateId = Number(update.update_id);
|
|
937
|
+
const added = await store.add({ updateId, body, receivedAt: now() });
|
|
938
|
+
if (!added)
|
|
939
|
+
return "duplicate";
|
|
940
|
+
schedule(update);
|
|
941
|
+
return "accepted";
|
|
942
|
+
},
|
|
943
|
+
async start() {
|
|
944
|
+
if (started)
|
|
945
|
+
return;
|
|
946
|
+
started = true;
|
|
947
|
+
await sweep(now());
|
|
948
|
+
timer = setInterval(() => {
|
|
949
|
+
sweep(now() - pendingGraceMs).catch((error) => storeError(error, "sweep"));
|
|
950
|
+
}, sweepEveryMs);
|
|
951
|
+
timer.unref?.();
|
|
952
|
+
},
|
|
953
|
+
sweep: () => sweep(now() - pendingGraceMs),
|
|
954
|
+
async idle() {
|
|
955
|
+
while (lines.size > 0)
|
|
956
|
+
await Promise.all(lines.values());
|
|
957
|
+
},
|
|
958
|
+
async close() {
|
|
959
|
+
started = false;
|
|
960
|
+
if (timer)
|
|
961
|
+
clearInterval(timer);
|
|
962
|
+
timer = undefined;
|
|
963
|
+
while (lines.size > 0)
|
|
964
|
+
await Promise.all(lines.values());
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
// src/telegram/update-store.ts
|
|
969
|
+
function isClaimable(record, now) {
|
|
970
|
+
if (record.state === "pending")
|
|
971
|
+
return true;
|
|
972
|
+
if (record.state === "failed")
|
|
973
|
+
return record.dueAt <= now;
|
|
974
|
+
return record.state === "processing" && record.dueAt < now;
|
|
975
|
+
}
|
|
976
|
+
function memoryTelegramUpdateStore() {
|
|
977
|
+
const records = new Map;
|
|
978
|
+
const holding = (updateId, attempt) => {
|
|
979
|
+
const record = records.get(updateId);
|
|
980
|
+
return record?.state === "processing" && record.attempts === attempt ? record : undefined;
|
|
981
|
+
};
|
|
982
|
+
return {
|
|
983
|
+
async add(update) {
|
|
984
|
+
if (records.has(update.updateId))
|
|
985
|
+
return false;
|
|
986
|
+
records.set(update.updateId, {
|
|
987
|
+
...update,
|
|
988
|
+
state: "pending",
|
|
989
|
+
attempts: 0,
|
|
990
|
+
dueAt: update.receivedAt,
|
|
991
|
+
settledAt: undefined,
|
|
992
|
+
error: undefined
|
|
993
|
+
});
|
|
994
|
+
return true;
|
|
995
|
+
},
|
|
996
|
+
async claim(updateId, options) {
|
|
997
|
+
const record = records.get(updateId);
|
|
998
|
+
if (!record || !isClaimable(record, options.now))
|
|
999
|
+
return;
|
|
1000
|
+
if (record.attempts >= options.maxAttempts) {
|
|
1001
|
+
Object.assign(record, {
|
|
1002
|
+
state: "abandoned",
|
|
1003
|
+
settledAt: options.now,
|
|
1004
|
+
error: "attempts spent"
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
record.state = "processing";
|
|
1009
|
+
record.attempts += 1;
|
|
1010
|
+
record.dueAt = options.leaseUntil;
|
|
1011
|
+
return record.attempts;
|
|
1012
|
+
},
|
|
1013
|
+
async renew(updateId, attempt, leaseUntil) {
|
|
1014
|
+
const record = holding(updateId, attempt);
|
|
1015
|
+
if (record)
|
|
1016
|
+
record.dueAt = leaseUntil;
|
|
1017
|
+
return record !== undefined;
|
|
1018
|
+
},
|
|
1019
|
+
async settle(updateId, attempt, settlement) {
|
|
1020
|
+
const record = holding(updateId, attempt);
|
|
1021
|
+
if (!record)
|
|
1022
|
+
return;
|
|
1023
|
+
record.state = settlement.state;
|
|
1024
|
+
record.error = settlement.state === "completed" ? undefined : settlement.error;
|
|
1025
|
+
if (settlement.state === "failed")
|
|
1026
|
+
record.dueAt = settlement.retryAt;
|
|
1027
|
+
else
|
|
1028
|
+
record.settledAt = settlement.at;
|
|
1029
|
+
},
|
|
1030
|
+
async due(query) {
|
|
1031
|
+
return [...records.values()].filter((record) => record.state === "pending" ? record.receivedAt <= query.pendingBefore : isClaimable(record, query.now)).sort((left, right) => left.updateId - right.updateId).slice(0, query.limit).map(({ updateId, body }) => ({ updateId, body }));
|
|
1032
|
+
},
|
|
1033
|
+
async prune(before) {
|
|
1034
|
+
let pruned = 0;
|
|
1035
|
+
for (const [updateId, record] of records) {
|
|
1036
|
+
if (record.settledAt !== undefined && record.settledAt < before) {
|
|
1037
|
+
records.delete(updateId);
|
|
1038
|
+
pruned += 1;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return pruned;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// src/telegram/update-store-check.ts
|
|
1046
|
+
function same(actual, expected) {
|
|
1047
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
1048
|
+
}
|
|
1049
|
+
var claim = { maxAttempts: 2 };
|
|
1050
|
+
var RULES = {
|
|
1051
|
+
"one record per update_id": async (store, violated) => {
|
|
1052
|
+
if (!await store.add({ updateId: 7, body: "b", receivedAt: 0 }))
|
|
1053
|
+
violated("add of a new update returned false");
|
|
1054
|
+
if (await store.add({ updateId: 7, body: "other", receivedAt: 1 }))
|
|
1055
|
+
violated("add of a recorded update_id returned true");
|
|
1056
|
+
},
|
|
1057
|
+
"a live lease is not taken, a lapsed one is": async (store, violated) => {
|
|
1058
|
+
await store.add({ updateId: 7, body: "b", receivedAt: 0 });
|
|
1059
|
+
if (await store.claim(7, { ...claim, now: 10, leaseUntil: 100 }) !== 1)
|
|
1060
|
+
violated("the first claim was not attempt 1");
|
|
1061
|
+
if (await store.claim(7, { ...claim, now: 50, leaseUntil: 150 }) !== undefined)
|
|
1062
|
+
violated("a claim took an update under a live lease");
|
|
1063
|
+
if (!same(await store.due({ now: 50, pendingBefore: 50, limit: 10 }), []))
|
|
1064
|
+
violated("due listed an update under a live lease");
|
|
1065
|
+
if (!same(await store.due({ now: 101, pendingBefore: 0, limit: 10 }), [
|
|
1066
|
+
{ updateId: 7, body: "b" }
|
|
1067
|
+
]))
|
|
1068
|
+
violated("due did not list an update whose lease lapsed, with its body");
|
|
1069
|
+
if (await store.claim(7, { ...claim, now: 101, leaseUntil: 200 }) !== 2)
|
|
1070
|
+
violated("a lapsed lease was not taken as attempt 2");
|
|
1071
|
+
},
|
|
1072
|
+
"only the attempt holding the update renews or settles it": async (store, violated) => {
|
|
1073
|
+
await store.add({ updateId: 1, body: "b", receivedAt: 0 });
|
|
1074
|
+
await store.claim(1, { ...claim, now: 0, leaseUntil: 10 });
|
|
1075
|
+
await store.claim(1, { ...claim, now: 11, leaseUntil: 50 });
|
|
1076
|
+
if (await store.renew(1, 1, 500))
|
|
1077
|
+
violated("an attempt that lost its lease renewed it");
|
|
1078
|
+
if (!await store.renew(1, 2, 60))
|
|
1079
|
+
violated("the holding attempt could not renew");
|
|
1080
|
+
await store.settle(1, 1, { state: "completed", at: 20 });
|
|
1081
|
+
if ((await store.due({ now: 61, pendingBefore: 0, limit: 10 })).length !== 1)
|
|
1082
|
+
violated("an attempt that lost its lease settled the update");
|
|
1083
|
+
await store.settle(1, 2, { state: "completed", at: 30 });
|
|
1084
|
+
if (!same(await store.due({ now: 1000, pendingBefore: 1000, limit: 10 }), []))
|
|
1085
|
+
violated("a completed update is still due");
|
|
1086
|
+
},
|
|
1087
|
+
"a failed update is due at its retry, spent attempts abandon it": async (store, violated) => {
|
|
1088
|
+
await store.add({ updateId: 2, body: "b", receivedAt: 0 });
|
|
1089
|
+
await store.add({ updateId: 1, body: "a", receivedAt: 5 });
|
|
1090
|
+
await store.claim(2, { ...claim, now: 0, leaseUntil: 10 });
|
|
1091
|
+
await store.settle(2, 1, { state: "failed", at: 1, retryAt: 30, error: "x" });
|
|
1092
|
+
if (!same(await store.due({ now: 29, pendingBefore: 0, limit: 10 }), []))
|
|
1093
|
+
violated("a failed update was due before its retry, or a pending one before its grace");
|
|
1094
|
+
const due = await store.due({ now: 30, pendingBefore: 5, limit: 10 });
|
|
1095
|
+
if (!same(due.map((row) => row.updateId), [1, 2]))
|
|
1096
|
+
violated("due did not list a retry and a pending update in update_id order");
|
|
1097
|
+
if ((await store.due({ now: 30, pendingBefore: 5, limit: 1 })).length !== 1)
|
|
1098
|
+
violated("due ignored its limit");
|
|
1099
|
+
await store.claim(2, { ...claim, now: 30, leaseUntil: 40 });
|
|
1100
|
+
await store.settle(2, 2, { state: "failed", at: 31, retryAt: 35, error: "x" });
|
|
1101
|
+
if (await store.claim(2, { ...claim, now: 36, leaseUntil: 50 }) !== undefined)
|
|
1102
|
+
violated("an update past maxAttempts was claimed");
|
|
1103
|
+
if (!same(await store.due({ now: 100, pendingBefore: 0, limit: 10 }), []))
|
|
1104
|
+
violated("an update past maxAttempts was not abandoned");
|
|
1105
|
+
},
|
|
1106
|
+
"settled updates are forgotten after they are pruned": async (store, violated) => {
|
|
1107
|
+
await store.add({ updateId: 3, body: "b", receivedAt: 0 });
|
|
1108
|
+
await store.claim(3, { ...claim, now: 0, leaseUntil: 10 });
|
|
1109
|
+
await store.settle(3, 1, { state: "abandoned", at: 36, error: "x" });
|
|
1110
|
+
await store.add({ updateId: 4, body: "b", receivedAt: 0 });
|
|
1111
|
+
if (await store.prune(36) !== 0)
|
|
1112
|
+
violated("prune forgot an update settled at its bound");
|
|
1113
|
+
if (await store.prune(37) !== 1)
|
|
1114
|
+
violated("prune did not forget exactly the settled update before its bound");
|
|
1115
|
+
if (!await store.add({ updateId: 3, body: "b", receivedAt: 40 }))
|
|
1116
|
+
violated("a pruned update_id could not be recorded again");
|
|
1117
|
+
if (await store.add({ updateId: 4, body: "b", receivedAt: 40 }))
|
|
1118
|
+
violated("prune forgot an update that was never settled");
|
|
1119
|
+
},
|
|
1120
|
+
"of concurrent claims exactly one takes the update": async (store, violated) => {
|
|
1121
|
+
await store.add({ updateId: 9, body: "b", receivedAt: 0 });
|
|
1122
|
+
const taken = await Promise.all(Array.from({ length: 8 }, () => store.claim(9, { maxAttempts: 5, now: 1, leaseUntil: 100 })));
|
|
1123
|
+
if (taken.filter((attempt) => attempt !== undefined).length !== 1)
|
|
1124
|
+
violated(`${taken.filter((attempt) => attempt !== undefined).length} of 8 concurrent claims took one update`);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
async function checkTelegramUpdateStore(make) {
|
|
1128
|
+
const violations = [];
|
|
1129
|
+
for (const [name, rule] of Object.entries(RULES)) {
|
|
1130
|
+
try {
|
|
1131
|
+
await rule(await make(), (what) => violations.push(`${name}: ${what}`));
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
violations.push(`${name}: threw ${error instanceof Error ? error.message : String(error)}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return violations;
|
|
1137
|
+
}
|
|
1138
|
+
// src/telegram/update-store-postgres.ts
|
|
1139
|
+
var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
1140
|
+
function checkedTable(table) {
|
|
1141
|
+
if (!TABLE_NAME.test(table)) {
|
|
1142
|
+
throw new TypeError("[stitchkit] telegram update store: a table name is [A-Za-z_][A-Za-z0-9_]*");
|
|
1143
|
+
}
|
|
1144
|
+
return table;
|
|
1145
|
+
}
|
|
1146
|
+
function postgresTelegramUpdateStoreSchema(table = "telegram_updates") {
|
|
1147
|
+
const name = checkedTable(table);
|
|
1148
|
+
return `CREATE TABLE IF NOT EXISTS ${name} (
|
|
1149
|
+
update_id BIGINT PRIMARY KEY,
|
|
1150
|
+
body TEXT NOT NULL,
|
|
1151
|
+
state TEXT NOT NULL,
|
|
1152
|
+
attempts INTEGER NOT NULL,
|
|
1153
|
+
due_at BIGINT NOT NULL,
|
|
1154
|
+
received_at BIGINT NOT NULL,
|
|
1155
|
+
settled_at BIGINT,
|
|
1156
|
+
error TEXT
|
|
1157
|
+
);
|
|
1158
|
+
CREATE INDEX IF NOT EXISTS ${name}_due ON ${name} (state, due_at);`;
|
|
1159
|
+
}
|
|
1160
|
+
var CLAIMABLE = `(state = 'pending' OR (state = 'failed' AND due_at <= $1::bigint)
|
|
1161
|
+
OR (state = 'processing' AND due_at < $1::bigint))`;
|
|
1162
|
+
function integer(value) {
|
|
1163
|
+
if (typeof value === "number")
|
|
1164
|
+
return value;
|
|
1165
|
+
if (typeof value === "bigint" || typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
1166
|
+
return Number(value);
|
|
1167
|
+
}
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
function field(row, name) {
|
|
1171
|
+
return typeof row === "object" && row !== null && name in row ? Reflect.get(row, name) : undefined;
|
|
1172
|
+
}
|
|
1173
|
+
function postgresTelegramUpdateStore(config) {
|
|
1174
|
+
const table = checkedTable(config.table ?? "telegram_updates");
|
|
1175
|
+
const { query } = config;
|
|
1176
|
+
let ready;
|
|
1177
|
+
const run = async (text, parameters) => {
|
|
1178
|
+
if (config.createTable !== false) {
|
|
1179
|
+
ready ??= postgresTelegramUpdateStoreSchema(table).split(";").map((statement) => statement.trim()).filter((statement) => statement !== "").reduce((previous, statement) => previous.then(() => query(statement, [])), Promise.resolve()).then(() => {
|
|
1180
|
+
return;
|
|
1181
|
+
}, (error) => {
|
|
1182
|
+
ready = undefined;
|
|
1183
|
+
throw error;
|
|
1184
|
+
});
|
|
1185
|
+
await ready;
|
|
1186
|
+
}
|
|
1187
|
+
return query(text, parameters);
|
|
1188
|
+
};
|
|
1189
|
+
return {
|
|
1190
|
+
async add(update) {
|
|
1191
|
+
const rows = await run(`INSERT INTO ${table} (update_id, body, state, attempts, due_at, received_at)
|
|
1192
|
+
VALUES ($1::bigint, $2::text, 'pending', 0, $3::bigint, $3::bigint)
|
|
1193
|
+
ON CONFLICT (update_id) DO NOTHING RETURNING update_id`, [update.updateId, update.body, update.receivedAt]);
|
|
1194
|
+
return rows.length > 0;
|
|
1195
|
+
},
|
|
1196
|
+
async claim(updateId, options) {
|
|
1197
|
+
const [row] = await run(`UPDATE ${table} SET state = 'processing', attempts = attempts + 1,
|
|
1198
|
+
due_at = $2::bigint, error = NULL
|
|
1199
|
+
WHERE update_id = $3::bigint AND attempts < $4::integer AND ${CLAIMABLE}
|
|
1200
|
+
RETURNING attempts`, [options.now, options.leaseUntil, updateId, options.maxAttempts]);
|
|
1201
|
+
const attempts = integer(field(row, "attempts"));
|
|
1202
|
+
if (attempts !== undefined)
|
|
1203
|
+
return attempts;
|
|
1204
|
+
await run(`UPDATE ${table} SET state = 'abandoned', settled_at = $1::bigint, error = 'attempts spent'
|
|
1205
|
+
WHERE update_id = $2::bigint AND attempts >= $3::integer AND ${CLAIMABLE}
|
|
1206
|
+
RETURNING update_id`, [options.now, updateId, options.maxAttempts]);
|
|
1207
|
+
return;
|
|
1208
|
+
},
|
|
1209
|
+
async renew(updateId, attempt, leaseUntil) {
|
|
1210
|
+
const rows = await run(`UPDATE ${table} SET due_at = $1::bigint
|
|
1211
|
+
WHERE update_id = $2::bigint AND attempts = $3::integer AND state = 'processing'
|
|
1212
|
+
RETURNING update_id`, [leaseUntil, updateId, attempt]);
|
|
1213
|
+
return rows.length > 0;
|
|
1214
|
+
},
|
|
1215
|
+
async settle(updateId, attempt, settlement) {
|
|
1216
|
+
await run(`UPDATE ${table} SET state = $1::text, due_at = COALESCE($2::bigint, due_at),
|
|
1217
|
+
settled_at = $3::bigint, error = $4::text
|
|
1218
|
+
WHERE update_id = $5::bigint AND attempts = $6::integer AND state = 'processing'
|
|
1219
|
+
RETURNING update_id`, [
|
|
1220
|
+
settlement.state,
|
|
1221
|
+
settlement.state === "failed" ? settlement.retryAt : null,
|
|
1222
|
+
settlement.state === "failed" ? null : settlement.at,
|
|
1223
|
+
settlement.state === "completed" ? null : settlement.error,
|
|
1224
|
+
updateId,
|
|
1225
|
+
attempt
|
|
1226
|
+
]);
|
|
1227
|
+
},
|
|
1228
|
+
async due(options) {
|
|
1229
|
+
const rows = await run(`SELECT update_id, body FROM ${table}
|
|
1230
|
+
WHERE (state = 'pending' AND received_at <= $2::bigint)
|
|
1231
|
+
OR (state <> 'pending' AND ${CLAIMABLE})
|
|
1232
|
+
ORDER BY update_id LIMIT $3::integer`, [options.now, options.pendingBefore, options.limit]);
|
|
1233
|
+
return rows.flatMap((row) => {
|
|
1234
|
+
const updateId = integer(field(row, "update_id"));
|
|
1235
|
+
const body = field(row, "body");
|
|
1236
|
+
return updateId !== undefined && typeof body === "string" ? [{ updateId, body }] : [];
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
async prune(before) {
|
|
1240
|
+
const rows = await run(`DELETE FROM ${table} WHERE state IN ('completed', 'abandoned') AND settled_at < $1::bigint
|
|
1241
|
+
RETURNING update_id`, [before]);
|
|
1242
|
+
return rows.length;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
// src/telegram/update-store-sqlite.ts
|
|
1247
|
+
var TABLE_NAME2 = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
1248
|
+
var CLAIMABLE2 = `(state = 'pending' OR (state = 'failed' AND due_at <= ?1)
|
|
1249
|
+
OR (state = 'processing' AND due_at < ?1))`;
|
|
1250
|
+
function rowOf(row) {
|
|
1251
|
+
if (typeof row !== "object" || row === null)
|
|
1252
|
+
return;
|
|
1253
|
+
const updateId = "update_id" in row ? row.update_id : undefined;
|
|
1254
|
+
const body = "body" in row ? row.body : undefined;
|
|
1255
|
+
return typeof updateId === "number" && typeof body === "string" ? { updateId, body } : undefined;
|
|
1256
|
+
}
|
|
1257
|
+
function sqliteTelegramUpdateStore(config) {
|
|
1258
|
+
const table = config.table ?? "telegram_updates";
|
|
1259
|
+
if (!TABLE_NAME2.test(table)) {
|
|
1260
|
+
throw new TypeError("[stitchkit] telegram update store: a table name is [A-Za-z_][A-Za-z0-9_]*");
|
|
1261
|
+
}
|
|
1262
|
+
const database = config.database;
|
|
1263
|
+
database.exec(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
1264
|
+
update_id INTEGER PRIMARY KEY,
|
|
1265
|
+
body TEXT NOT NULL,
|
|
1266
|
+
state TEXT NOT NULL,
|
|
1267
|
+
attempts INTEGER NOT NULL,
|
|
1268
|
+
due_at INTEGER NOT NULL,
|
|
1269
|
+
received_at INTEGER NOT NULL,
|
|
1270
|
+
settled_at INTEGER,
|
|
1271
|
+
error TEXT
|
|
1272
|
+
);
|
|
1273
|
+
CREATE INDEX IF NOT EXISTS ${table}_due ON ${table} (state, due_at);`);
|
|
1274
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO ${table} (update_id, body, state, attempts, due_at, received_at)
|
|
1275
|
+
VALUES (?1, ?2, 'pending', 0, ?3, ?3)`);
|
|
1276
|
+
const take = database.prepare(`UPDATE ${table} SET state = 'processing', attempts = attempts + 1, due_at = ?2, error = NULL
|
|
1277
|
+
WHERE update_id = ?3 AND attempts < ?4 AND ${CLAIMABLE2} RETURNING attempts`);
|
|
1278
|
+
const spend = database.prepare(`UPDATE ${table} SET state = 'abandoned', settled_at = ?1, error = 'attempts spent'
|
|
1279
|
+
WHERE update_id = ?2 AND attempts >= ?3 AND ${CLAIMABLE2}`);
|
|
1280
|
+
const renew = database.prepare(`UPDATE ${table} SET due_at = ?1 WHERE update_id = ?2 AND attempts = ?3 AND state = 'processing'`);
|
|
1281
|
+
const settle = database.prepare(`UPDATE ${table} SET state = ?1, due_at = COALESCE(?2, due_at), settled_at = ?3, error = ?4
|
|
1282
|
+
WHERE update_id = ?5 AND attempts = ?6 AND state = 'processing'`);
|
|
1283
|
+
const due = database.prepare(`SELECT update_id, body FROM ${table}
|
|
1284
|
+
WHERE (state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE2})
|
|
1285
|
+
ORDER BY update_id LIMIT ?3`);
|
|
1286
|
+
const prune = database.prepare(`DELETE FROM ${table} WHERE state IN ('completed', 'abandoned') AND settled_at < ?1`);
|
|
1287
|
+
return {
|
|
1288
|
+
async add(update) {
|
|
1289
|
+
return Number(insert.run(update.updateId, update.body, update.receivedAt).changes) > 0;
|
|
1290
|
+
},
|
|
1291
|
+
async claim(updateId, options) {
|
|
1292
|
+
const [row] = take.all(options.now, options.leaseUntil, updateId, options.maxAttempts);
|
|
1293
|
+
const attempts = typeof row === "object" && row !== null && "attempts" in row ? row.attempts : undefined;
|
|
1294
|
+
if (typeof attempts === "number")
|
|
1295
|
+
return attempts;
|
|
1296
|
+
spend.run(options.now, updateId, options.maxAttempts);
|
|
1297
|
+
return;
|
|
1298
|
+
},
|
|
1299
|
+
async renew(updateId, attempt, leaseUntil) {
|
|
1300
|
+
return Number(renew.run(leaseUntil, updateId, attempt).changes) > 0;
|
|
1301
|
+
},
|
|
1302
|
+
async settle(updateId, attempt, settlement) {
|
|
1303
|
+
settle.run(settlement.state, settlement.state === "failed" ? settlement.retryAt : null, settlement.state === "failed" ? null : settlement.at, settlement.state === "completed" ? null : settlement.error, updateId, attempt);
|
|
1304
|
+
},
|
|
1305
|
+
async due(query) {
|
|
1306
|
+
return due.all(query.now, query.pendingBefore, query.limit).flatMap((row) => rowOf(row) ?? []);
|
|
1307
|
+
},
|
|
1308
|
+
async prune(before) {
|
|
1309
|
+
return Number(prune.run(before).changes);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
// src/telegram/webhook.ts
|
|
1314
|
+
var OWNER_PARAMETER = "owner";
|
|
1315
|
+
var OWNER_CONTEXT = "stitchkit:telegram-webhook-owner";
|
|
1316
|
+
var SECRET_SHAPE = /^[A-Za-z0-9_-]{1,256}$/;
|
|
1317
|
+
var SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
1318
|
+
var TELEGRAM_WEBHOOK_NONE = "none";
|
|
1319
|
+
|
|
1320
|
+
class TelegramWebhookClaimError extends Error {
|
|
1321
|
+
reason;
|
|
1322
|
+
ownerHost;
|
|
1323
|
+
constructor(reason, ownerHost) {
|
|
1324
|
+
super(reason === "not-confirmed" ? `Telegram webhook was set but points to ${ownerHost} after it` : reason === "other-secret" ? `Telegram webhook at ${ownerHost} is registered with another secret; refusing to ` + `replace it. takeoverFrom: '${ownerHost}' replaces it` : `Telegram webhook belongs to ${ownerHost}; refusing to take it over. ` + `takeoverFrom: '${ownerHost}' moves it here`);
|
|
1325
|
+
this.name = "TelegramWebhookClaimError";
|
|
1326
|
+
this.reason = reason;
|
|
1327
|
+
this.ownerHost = ownerHost;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function hostOf(url) {
|
|
1331
|
+
if (url === "")
|
|
1332
|
+
return TELEGRAM_WEBHOOK_NONE;
|
|
1333
|
+
return URL.canParse(url) ? new URL(url).host : "unknown";
|
|
1334
|
+
}
|
|
1335
|
+
function withoutTag(url) {
|
|
1336
|
+
if (!URL.canParse(url))
|
|
1337
|
+
return url;
|
|
1338
|
+
const parsed = new URL(url);
|
|
1339
|
+
parsed.searchParams.delete(OWNER_PARAMETER);
|
|
1340
|
+
return parsed.toString();
|
|
1341
|
+
}
|
|
1342
|
+
async function telegramWebhookUrl(url, secret) {
|
|
1343
|
+
if (!SECRET_SHAPE.test(secret)) {
|
|
1344
|
+
throw new TypeError("[stitchkit] telegram webhook: a secret is 1–256 of A-Z, a-z, 0-9, _ and -");
|
|
1345
|
+
}
|
|
1346
|
+
const tagged = new URL(url);
|
|
1347
|
+
const tag = toHex(await hmacSha256(new TextEncoder().encode(secret), OWNER_CONTEXT));
|
|
1348
|
+
tagged.searchParams.set(OWNER_PARAMETER, tag.slice(0, 16));
|
|
1349
|
+
return tagged.toString();
|
|
1350
|
+
}
|
|
1351
|
+
async function webhookInfo(config) {
|
|
1352
|
+
const result = await callTelegramBotApi({ ...botCall(config), method: "getWebhookInfo" });
|
|
1353
|
+
const info = isRecord(result) ? result : {};
|
|
1354
|
+
const at = info.last_error_date;
|
|
1355
|
+
const message = info.last_error_message;
|
|
1356
|
+
return {
|
|
1357
|
+
url: typeof info.url === "string" ? info.url : "",
|
|
1358
|
+
pendingUpdates: typeof info.pending_update_count === "number" ? info.pending_update_count : 0,
|
|
1359
|
+
...typeof at === "number" && typeof message === "string" && { lastError: { at: at * 1000, message } }
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function botCall(config) {
|
|
1363
|
+
return {
|
|
1364
|
+
token: config.token,
|
|
1365
|
+
...config.apiRoot && { apiRoot: config.apiRoot },
|
|
1366
|
+
...config.fetch && { fetch: config.fetch },
|
|
1367
|
+
...config.signal && { signal: config.signal }
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
async function claimTelegramWebhook(config) {
|
|
1371
|
+
const url = await telegramWebhookUrl(config.url, config.secret);
|
|
1372
|
+
const current = await webhookInfo(config);
|
|
1373
|
+
const ownerHost = hostOf(current.url);
|
|
1374
|
+
const owned = current.url === url;
|
|
1375
|
+
if (!owned && config.takeoverFrom !== ownerHost) {
|
|
1376
|
+
const sameAddress = current.url !== "" && withoutTag(current.url) === withoutTag(url);
|
|
1377
|
+
throw new TelegramWebhookClaimError(sameAddress ? "other-secret" : "owned-elsewhere", ownerHost);
|
|
1378
|
+
}
|
|
1379
|
+
await callTelegramBotApi({
|
|
1380
|
+
...botCall(config),
|
|
1381
|
+
method: "setWebhook",
|
|
1382
|
+
params: {
|
|
1383
|
+
url,
|
|
1384
|
+
secret_token: config.secret,
|
|
1385
|
+
drop_pending_updates: config.dropPendingUpdates ?? false,
|
|
1386
|
+
...config.allowedUpdates && { allowed_updates: config.allowedUpdates },
|
|
1387
|
+
...config.maxConnections !== undefined && { max_connections: config.maxConnections }
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
const confirmed = await webhookInfo(config);
|
|
1391
|
+
if (confirmed.url !== url) {
|
|
1392
|
+
throw new TelegramWebhookClaimError("not-confirmed", hostOf(confirmed.url));
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
url,
|
|
1396
|
+
...!owned && { takenFrom: ownerHost },
|
|
1397
|
+
pendingUpdates: confirmed.pendingUpdates
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
async function checkTelegramWebhook(config) {
|
|
1401
|
+
const url = await telegramWebhookUrl(config.url, config.secret);
|
|
1402
|
+
const info = await webhookInfo(config);
|
|
1403
|
+
return {
|
|
1404
|
+
owned: info.url === url,
|
|
1405
|
+
ownerHost: hostOf(info.url),
|
|
1406
|
+
pendingUpdates: info.pendingUpdates,
|
|
1407
|
+
...info.lastError && { lastError: info.lastError }
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
async function receiveTelegramWebhook(request, options) {
|
|
1411
|
+
if (request.method !== "POST") {
|
|
1412
|
+
return new Response(null, { status: 405, headers: { allow: "POST" } });
|
|
1413
|
+
}
|
|
1414
|
+
const secret = request.headers.get(SECRET_HEADER);
|
|
1415
|
+
if (secret === null || !digestsEqual(secret, options.secret)) {
|
|
1416
|
+
return new Response(null, { status: 403 });
|
|
1417
|
+
}
|
|
1418
|
+
const acceptance = await options.accept(await request.text());
|
|
1419
|
+
return new Response(null, { status: acceptance === "invalid" ? 400 : 200 });
|
|
1420
|
+
}
|
|
817
1421
|
export {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
runTelegramBroadcast,
|
|
822
|
-
createTelegramOperatorChannel,
|
|
823
|
-
createTelegramLocalFiles,
|
|
824
|
-
classifyTelegramSendFailure,
|
|
825
|
-
callTelegramBotApi,
|
|
826
|
-
TelegramLocalFileError,
|
|
1422
|
+
TELEGRAM_BOT_TOKEN_PATTERN,
|
|
1423
|
+
TELEGRAM_SEND_FAILURE_REASONS2 as TELEGRAM_SEND_FAILURE_REASONS,
|
|
1424
|
+
TELEGRAM_WEBHOOK_NONE,
|
|
827
1425
|
TelegramBotApiError,
|
|
828
|
-
|
|
1426
|
+
TelegramLocalFileError,
|
|
1427
|
+
TelegramWebhookClaimError,
|
|
1428
|
+
callTelegramBotApi,
|
|
1429
|
+
checkTelegramUpdateStore,
|
|
1430
|
+
checkTelegramWebhook,
|
|
1431
|
+
claimTelegramWebhook,
|
|
1432
|
+
classifyTelegramSendFailure2 as classifyTelegramSendFailure,
|
|
1433
|
+
createTelegramLocalFiles,
|
|
1434
|
+
createTelegramOperatorChannel,
|
|
1435
|
+
createTelegramOperatorDedupe,
|
|
1436
|
+
createTelegramUpdateIntake,
|
|
1437
|
+
memoryTelegramUpdateStore,
|
|
1438
|
+
parseTelegramUser,
|
|
1439
|
+
postgresTelegramUpdateStore,
|
|
1440
|
+
postgresTelegramUpdateStoreSchema,
|
|
1441
|
+
receiveTelegramWebhook,
|
|
1442
|
+
redactTelegramBotToken,
|
|
1443
|
+
runTelegramBroadcast,
|
|
1444
|
+
sqliteTelegramUpdateStore,
|
|
1445
|
+
telegramBroadcastSender,
|
|
1446
|
+
telegramOperatorSender,
|
|
1447
|
+
telegramWebhookUrl,
|
|
1448
|
+
verifyTelegramInitData
|
|
829
1449
|
};
|