stitchkit 0.100.2 → 0.100.3
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 +86 -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 +208 -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 +115 -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 +5 -1
- package/dist/entrypoints/telegram.d.ts.map +1 -1
- package/dist/entrypoints/telegram.js +548 -156
- 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 +223 -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-6k1svmjh.js +141 -0
- 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-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-yw4fzmdz.js → index-kzz6rq0w.js} +28 -17
- 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-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/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/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.map +1 -1
- package/dist/telegram/operator-channel.d.ts +36 -3
- package/dist/telegram/operator-channel.d.ts.map +1 -1
- 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 +19 -0
- package/dist/telegram/send-failure.d.ts.map +1 -1
- package/dist/telegram/update-intake.d.ts +78 -0
- package/dist/telegram/update-intake.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/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 +2 -1
- package/llms/react-keyboard.txt +193 -0
- package/llms/stitchkit.txt +7 -1
- package/llms/telegram-html.txt +35 -0
- package/llms/telegram-screens.txt +365 -0
- package/llms/telegram.txt +160 -4
- package/llms-full.txt +736 -8
- package/llms.txt +10 -5
- 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,6 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../index-
|
|
2
|
+
classifyTelegramSendFailure2
|
|
3
|
+
} from "../index-6k1svmjh.js";
|
|
4
|
+
import {
|
|
5
|
+
TELEGRAM_TEXT_LIMIT2,
|
|
6
|
+
truncateTelegramHtml2
|
|
7
|
+
} from "../index-nmaqxdx7.js";
|
|
8
|
+
import {
|
|
9
|
+
isRecord,
|
|
10
|
+
transportResult
|
|
11
|
+
} from "../index-9jfdm5tg.js";
|
|
4
12
|
|
|
5
13
|
// src/telegram/bot-api.ts
|
|
6
14
|
var TELEGRAM_BOT_TOKEN_PATTERN = /\d{5,}:[A-Za-z0-9_-]{30,}/;
|
|
@@ -175,122 +183,6 @@ async function lockBroadcast(directory, files) {
|
|
|
175
183
|
throw new Error("Telegram broadcast lock could not be taken");
|
|
176
184
|
}
|
|
177
185
|
|
|
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
186
|
// src/telegram/broadcast.ts
|
|
295
187
|
function pause(milliseconds) {
|
|
296
188
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -376,7 +268,7 @@ async function runTelegramBroadcast(config) {
|
|
|
376
268
|
await config.send({ recipient, attempt });
|
|
377
269
|
return { outcome: "delivered" };
|
|
378
270
|
} catch (error) {
|
|
379
|
-
const failure =
|
|
271
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
380
272
|
if (failure.recipientUnreachable)
|
|
381
273
|
return { outcome: "unreachable", reason: failure.reason };
|
|
382
274
|
if (failure.reason === "message-invalid")
|
|
@@ -446,6 +338,28 @@ function telegramBroadcastSender(config) {
|
|
|
446
338
|
}
|
|
447
339
|
// src/telegram/init-data.ts
|
|
448
340
|
import { z as z2 } from "zod";
|
|
341
|
+
|
|
342
|
+
// src/telegram/crypto.ts
|
|
343
|
+
var encoder = new TextEncoder;
|
|
344
|
+
async function hmacSha256(key, message) {
|
|
345
|
+
const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
346
|
+
return crypto.subtle.sign("HMAC", imported, encoder.encode(message));
|
|
347
|
+
}
|
|
348
|
+
function toHex(buffer) {
|
|
349
|
+
return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
350
|
+
}
|
|
351
|
+
function digestsEqual(a, b) {
|
|
352
|
+
if (a.length !== b.length)
|
|
353
|
+
return false;
|
|
354
|
+
let difference = 0;
|
|
355
|
+
for (let index = 0;index < a.length; index += 1) {
|
|
356
|
+
difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
|
|
357
|
+
}
|
|
358
|
+
return difference === 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/telegram/init-data.ts
|
|
362
|
+
var encoder2 = new TextEncoder;
|
|
449
363
|
var TelegramInitDataUserSchema = z2.object({
|
|
450
364
|
id: z2.int(),
|
|
451
365
|
is_bot: z2.boolean().optional(),
|
|
@@ -467,23 +381,6 @@ var TelegramInitDataUserSchema = z2.object({
|
|
|
467
381
|
...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
|
|
468
382
|
...user.photo_url !== undefined && { photoUrl: user.photo_url }
|
|
469
383
|
}));
|
|
470
|
-
var encoder = new TextEncoder;
|
|
471
|
-
async function hmacSha256(key, message) {
|
|
472
|
-
const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
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;
|
|
486
|
-
}
|
|
487
384
|
function optionalUser(raw, key) {
|
|
488
385
|
const encoded = raw[key];
|
|
489
386
|
if (encoded === undefined)
|
|
@@ -508,7 +405,7 @@ async function verifyTelegramInitData(options) {
|
|
|
508
405
|
}
|
|
509
406
|
const dataCheckString = Object.keys(signed).sort().map((key) => `${key}=${signed[key]}`).join(`
|
|
510
407
|
`);
|
|
511
|
-
const secretKey = await hmacSha256(
|
|
408
|
+
const secretKey = await hmacSha256(encoder2.encode("WebAppData"), options.botToken);
|
|
512
409
|
const expected = toHex(await hmacSha256(secretKey, dataCheckString));
|
|
513
410
|
if (!digestsEqual(expected, hash.toLowerCase())) {
|
|
514
411
|
return { valid: false, reason: "signature-mismatch" };
|
|
@@ -635,7 +532,6 @@ function createTelegramLocalFiles(config) {
|
|
|
635
532
|
};
|
|
636
533
|
}
|
|
637
534
|
// src/telegram/operator-channel.ts
|
|
638
|
-
var TEXT_LIMIT = 4096;
|
|
639
535
|
var MASK = "[redacted]";
|
|
640
536
|
function masked(text, patterns) {
|
|
641
537
|
let output = text;
|
|
@@ -648,12 +544,12 @@ function literal(value) {
|
|
|
648
544
|
return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
649
545
|
}
|
|
650
546
|
function timer(milliseconds, signal) {
|
|
651
|
-
return new Promise((
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
652
548
|
if (signal.aborted)
|
|
653
549
|
return reject(signal.reason);
|
|
654
550
|
const handle = setTimeout(() => {
|
|
655
551
|
signal.removeEventListener("abort", stop);
|
|
656
|
-
|
|
552
|
+
resolve();
|
|
657
553
|
}, milliseconds);
|
|
658
554
|
const stop = () => {
|
|
659
555
|
clearTimeout(handle);
|
|
@@ -664,14 +560,67 @@ function timer(milliseconds, signal) {
|
|
|
664
560
|
}
|
|
665
561
|
function fitted(text) {
|
|
666
562
|
const points = Array.from(text);
|
|
667
|
-
return points.length <=
|
|
563
|
+
return points.length <= TELEGRAM_TEXT_LIMIT2 ? text : `${points.slice(0, TELEGRAM_TEXT_LIMIT2 - 1).join("")}…`;
|
|
564
|
+
}
|
|
565
|
+
function defaultFingerprint(text, topic) {
|
|
566
|
+
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, "#");
|
|
567
|
+
return `${topic ?? ""}\x00${blanked.slice(0, 300)}`;
|
|
568
|
+
}
|
|
569
|
+
function dedupeWindow(options) {
|
|
570
|
+
const windowMs = options.windowMs ?? 600000;
|
|
571
|
+
const maxPerWindow = options.maxPerWindow ?? 20;
|
|
572
|
+
const fingerprint = options.fingerprint ?? defaultFingerprint;
|
|
573
|
+
const now = options.now ?? Date.now;
|
|
574
|
+
const repeatedLine = options.repeatedLine ?? ((count) => `(+${count} more like this)`);
|
|
575
|
+
const overBudgetLine = options.overBudgetLine ?? ((count) => `(+${count} more messages over the limit)`);
|
|
576
|
+
const recent = new Map;
|
|
577
|
+
let windowStart = Number.NEGATIVE_INFINITY;
|
|
578
|
+
let sentInWindow = 0;
|
|
579
|
+
let overBudget = 0;
|
|
580
|
+
return (text, topic) => {
|
|
581
|
+
const at = now();
|
|
582
|
+
const key = fingerprint(text, topic);
|
|
583
|
+
const previous = recent.get(key);
|
|
584
|
+
if (previous && at - previous.sentAt < windowMs) {
|
|
585
|
+
previous.held += 1;
|
|
586
|
+
return { send: false, reason: "repeated" };
|
|
587
|
+
}
|
|
588
|
+
if (at - windowStart >= windowMs) {
|
|
589
|
+
windowStart = at;
|
|
590
|
+
sentInWindow = 0;
|
|
591
|
+
}
|
|
592
|
+
if (sentInWindow >= maxPerWindow) {
|
|
593
|
+
overBudget += 1;
|
|
594
|
+
return { send: false, reason: "over-budget" };
|
|
595
|
+
}
|
|
596
|
+
sentInWindow += 1;
|
|
597
|
+
for (const [stale, entry] of recent) {
|
|
598
|
+
if (at - entry.sentAt >= windowMs)
|
|
599
|
+
recent.delete(stale);
|
|
600
|
+
}
|
|
601
|
+
recent.set(key, { sentAt: at, held: 0 });
|
|
602
|
+
const lines = [text];
|
|
603
|
+
if (previous?.held)
|
|
604
|
+
lines.push(repeatedLine(previous.held));
|
|
605
|
+
if (overBudget > 0)
|
|
606
|
+
lines.push(overBudgetLine(overBudget));
|
|
607
|
+
overBudget = 0;
|
|
608
|
+
return { send: true, text: lines.join(`
|
|
609
|
+
|
|
610
|
+
`) };
|
|
611
|
+
};
|
|
668
612
|
}
|
|
669
613
|
function createTelegramOperatorChannel(config) {
|
|
670
614
|
const maxQueued = config.maxQueued ?? 200;
|
|
671
615
|
const minIntervalMs = config.minIntervalMs ?? 3000;
|
|
672
616
|
const maxAttempts = config.maxAttempts ?? 3;
|
|
673
617
|
const sleep = config.sleep ?? timer;
|
|
674
|
-
const patterns = [
|
|
618
|
+
const patterns = [
|
|
619
|
+
TELEGRAM_BOT_TOKEN_PATTERN,
|
|
620
|
+
...config.sensitivePatterns ?? [],
|
|
621
|
+
...[...config.sensitiveValues ?? []].filter((value) => value !== "").sort((left, right) => right.length - left.length).map(literal)
|
|
622
|
+
];
|
|
623
|
+
const admit = config.dedupe ? dedupeWindow(config.dedupe) : undefined;
|
|
675
624
|
const queue = [];
|
|
676
625
|
const idle = new Set;
|
|
677
626
|
let lifetime = new AbortController;
|
|
@@ -708,7 +657,7 @@ function createTelegramOperatorChannel(config) {
|
|
|
708
657
|
} catch (error) {
|
|
709
658
|
if (signal.aborted)
|
|
710
659
|
throw error;
|
|
711
|
-
const failure =
|
|
660
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
712
661
|
if (!failure.retryable) {
|
|
713
662
|
drop(item, "refused", failure);
|
|
714
663
|
return;
|
|
@@ -759,6 +708,13 @@ function createTelegramOperatorChannel(config) {
|
|
|
759
708
|
drop(item, "closed");
|
|
760
709
|
return;
|
|
761
710
|
}
|
|
711
|
+
const verdict = admit?.(item.text, topic);
|
|
712
|
+
if (verdict && !verdict.send) {
|
|
713
|
+
drop(item, verdict.reason);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (verdict)
|
|
717
|
+
item.text = verdict.text;
|
|
762
718
|
queue.push(item);
|
|
763
719
|
while (queue.length > maxQueued) {
|
|
764
720
|
const oldest = queue.shift();
|
|
@@ -777,11 +733,11 @@ function createTelegramOperatorChannel(config) {
|
|
|
777
733
|
drain(signal) {
|
|
778
734
|
if (queue.length === 0 && inFlight === 0)
|
|
779
735
|
return Promise.resolve();
|
|
780
|
-
return new Promise((
|
|
736
|
+
return new Promise((resolve) => {
|
|
781
737
|
const done = () => {
|
|
782
738
|
signal?.removeEventListener("abort", done);
|
|
783
739
|
idle.delete(done);
|
|
784
|
-
|
|
740
|
+
resolve();
|
|
785
741
|
};
|
|
786
742
|
idle.add(done);
|
|
787
743
|
signal?.addEventListener("abort", done, { once: true });
|
|
@@ -797,6 +753,9 @@ function createTelegramOperatorChannel(config) {
|
|
|
797
753
|
}
|
|
798
754
|
};
|
|
799
755
|
}
|
|
756
|
+
function markupFitted(text, parseMode) {
|
|
757
|
+
return parseMode === "HTML" ? truncateTelegramHtml2(text) : text;
|
|
758
|
+
}
|
|
800
759
|
function telegramOperatorSender(config) {
|
|
801
760
|
const token = literal(config.token);
|
|
802
761
|
return (message, signal) => callTelegramBotApi({
|
|
@@ -804,7 +763,7 @@ function telegramOperatorSender(config) {
|
|
|
804
763
|
method: "sendMessage",
|
|
805
764
|
params: {
|
|
806
765
|
chat_id: message.chatId,
|
|
807
|
-
text: masked(message.text, [token]),
|
|
766
|
+
text: markupFitted(masked(message.text, [token]), config.parseMode),
|
|
808
767
|
...message.threadId !== undefined && { message_thread_id: message.threadId },
|
|
809
768
|
...config.parseMode && { parse_mode: config.parseMode },
|
|
810
769
|
link_preview_options: { is_disabled: true }
|
|
@@ -814,16 +773,449 @@ function telegramOperatorSender(config) {
|
|
|
814
773
|
...config.fetch && { fetch: config.fetch }
|
|
815
774
|
});
|
|
816
775
|
}
|
|
776
|
+
// src/telegram/update-intake.ts
|
|
777
|
+
var SWEEP_BATCH = 100;
|
|
778
|
+
function defaultRetry(error, attempt) {
|
|
779
|
+
const failure = classifyTelegramSendFailure2(error);
|
|
780
|
+
if (failure.evidence !== "none" && !failure.retryable)
|
|
781
|
+
return false;
|
|
782
|
+
const backoff = Math.min(5000 * 4 ** (attempt - 1), 15 * 60000);
|
|
783
|
+
return Math.max(backoff, (failure.retryAfterSeconds ?? 0) * 1000);
|
|
784
|
+
}
|
|
785
|
+
function lineOf(update) {
|
|
786
|
+
for (const [key, payload] of Object.entries(update)) {
|
|
787
|
+
if (key === "update_id" || !isRecord(payload))
|
|
788
|
+
continue;
|
|
789
|
+
const message = isRecord(payload.message) ? payload.message : undefined;
|
|
790
|
+
for (const holder of [
|
|
791
|
+
payload.chat,
|
|
792
|
+
message?.chat,
|
|
793
|
+
payload.from,
|
|
794
|
+
payload.user,
|
|
795
|
+
payload.voter_chat
|
|
796
|
+
]) {
|
|
797
|
+
if (isRecord(holder) && (typeof holder.id === "number" || typeof holder.id === "string")) {
|
|
798
|
+
return `chat:${holder.id}`;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return `update:${String(update.update_id)}`;
|
|
803
|
+
}
|
|
804
|
+
function parsed(body) {
|
|
805
|
+
try {
|
|
806
|
+
const value = JSON.parse(body);
|
|
807
|
+
return isRecord(value) && Number.isSafeInteger(value.update_id) && Number(value.update_id) >= 0 ? value : undefined;
|
|
808
|
+
} catch {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function positive(name, value) {
|
|
813
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
814
|
+
throw new RangeError(`[stitchkit] telegram update intake: ${name} is a positive integer`);
|
|
815
|
+
}
|
|
816
|
+
return value;
|
|
817
|
+
}
|
|
818
|
+
function createTelegramUpdateIntake(config) {
|
|
819
|
+
const { store } = config;
|
|
820
|
+
const now = config.now ?? Date.now;
|
|
821
|
+
const retry = config.retry ?? defaultRetry;
|
|
822
|
+
const maxAttempts = positive("maxAttempts", config.maxAttempts ?? 5);
|
|
823
|
+
const leaseMs = positive("leaseMs", config.leaseMs ?? 120000);
|
|
824
|
+
const pendingGraceMs = config.pendingGraceMs ?? 60000;
|
|
825
|
+
const sweepEveryMs = positive("sweepEveryMs", config.sweepEveryMs ?? 30000);
|
|
826
|
+
const retainMs = config.retainMs ?? 86400000;
|
|
827
|
+
const maxConcurrent = positive("maxConcurrent", config.maxConcurrent ?? 32);
|
|
828
|
+
const lines = new Map;
|
|
829
|
+
const scheduled = new Set;
|
|
830
|
+
const waiting = [];
|
|
831
|
+
let running = 0;
|
|
832
|
+
let started = false;
|
|
833
|
+
let timer;
|
|
834
|
+
const storeError = (error, step) => {
|
|
835
|
+
try {
|
|
836
|
+
config.onStoreError?.(error, step);
|
|
837
|
+
} catch {}
|
|
838
|
+
};
|
|
839
|
+
const slot = async () => {
|
|
840
|
+
if (running < maxConcurrent) {
|
|
841
|
+
running += 1;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
845
|
+
};
|
|
846
|
+
const release = () => {
|
|
847
|
+
const next = waiting.shift();
|
|
848
|
+
if (next)
|
|
849
|
+
next();
|
|
850
|
+
else
|
|
851
|
+
running -= 1;
|
|
852
|
+
};
|
|
853
|
+
const attempt = async (update, updateId) => {
|
|
854
|
+
const taken = now();
|
|
855
|
+
const number = await store.claim(updateId, { now: taken, leaseUntil: taken + leaseMs, maxAttempts }).catch((error) => storeError(error, "claim"));
|
|
856
|
+
if (number === undefined)
|
|
857
|
+
return;
|
|
858
|
+
const renewal = setInterval(() => {
|
|
859
|
+
store.renew(updateId, number, now() + leaseMs).catch((error) => storeError(error, "renew"));
|
|
860
|
+
}, Math.max(1, Math.floor(leaseMs / 4)));
|
|
861
|
+
let settlement;
|
|
862
|
+
try {
|
|
863
|
+
await config.handle(transportResult(update));
|
|
864
|
+
settlement = { state: "completed", at: now() };
|
|
865
|
+
} catch (error) {
|
|
866
|
+
const delay = number >= maxAttempts ? false : retry(error, number);
|
|
867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
868
|
+
settlement = delay === false ? { state: "abandoned", at: now(), error: message } : { state: "failed", at: now(), retryAt: now() + delay, error: message };
|
|
869
|
+
try {
|
|
870
|
+
config.onFailure?.({ updateId, attempt: number, error, retrying: delay !== false });
|
|
871
|
+
} catch {}
|
|
872
|
+
} finally {
|
|
873
|
+
clearInterval(renewal);
|
|
874
|
+
}
|
|
875
|
+
await store.settle(updateId, number, settlement).catch((error) => storeError(error, "settle"));
|
|
876
|
+
};
|
|
877
|
+
const schedule = (update) => {
|
|
878
|
+
const updateId = Number(update.update_id);
|
|
879
|
+
if (!started || scheduled.has(updateId))
|
|
880
|
+
return;
|
|
881
|
+
scheduled.add(updateId);
|
|
882
|
+
const line = lineOf(update);
|
|
883
|
+
const next = (lines.get(line) ?? Promise.resolve()).then(async () => {
|
|
884
|
+
await slot();
|
|
885
|
+
try {
|
|
886
|
+
await attempt(update, updateId);
|
|
887
|
+
} finally {
|
|
888
|
+
release();
|
|
889
|
+
}
|
|
890
|
+
}).finally(() => {
|
|
891
|
+
scheduled.delete(updateId);
|
|
892
|
+
if (lines.get(line) === next)
|
|
893
|
+
lines.delete(line);
|
|
894
|
+
});
|
|
895
|
+
lines.set(line, next);
|
|
896
|
+
};
|
|
897
|
+
const sweep = async (pendingBefore) => {
|
|
898
|
+
const at = now();
|
|
899
|
+
const due = await store.due({ now: at, pendingBefore, limit: SWEEP_BATCH });
|
|
900
|
+
let count = 0;
|
|
901
|
+
for (const row of due) {
|
|
902
|
+
const update = parsed(row.body);
|
|
903
|
+
if (update && !scheduled.has(row.updateId)) {
|
|
904
|
+
schedule(update);
|
|
905
|
+
count += 1;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
await store.prune(at - retainMs);
|
|
909
|
+
return count;
|
|
910
|
+
};
|
|
911
|
+
return {
|
|
912
|
+
async accept(body) {
|
|
913
|
+
const update = parsed(body);
|
|
914
|
+
if (!update)
|
|
915
|
+
return "invalid";
|
|
916
|
+
const updateId = Number(update.update_id);
|
|
917
|
+
const added = await store.add({ updateId, body, receivedAt: now() });
|
|
918
|
+
if (!added)
|
|
919
|
+
return "duplicate";
|
|
920
|
+
schedule(update);
|
|
921
|
+
return "accepted";
|
|
922
|
+
},
|
|
923
|
+
async start() {
|
|
924
|
+
if (started)
|
|
925
|
+
return;
|
|
926
|
+
started = true;
|
|
927
|
+
await sweep(now());
|
|
928
|
+
timer = setInterval(() => {
|
|
929
|
+
sweep(now() - pendingGraceMs).catch((error) => storeError(error, "sweep"));
|
|
930
|
+
}, sweepEveryMs);
|
|
931
|
+
timer.unref?.();
|
|
932
|
+
},
|
|
933
|
+
sweep: () => sweep(now() - pendingGraceMs),
|
|
934
|
+
async idle() {
|
|
935
|
+
while (lines.size > 0)
|
|
936
|
+
await Promise.all(lines.values());
|
|
937
|
+
},
|
|
938
|
+
async close() {
|
|
939
|
+
started = false;
|
|
940
|
+
if (timer)
|
|
941
|
+
clearInterval(timer);
|
|
942
|
+
timer = undefined;
|
|
943
|
+
while (lines.size > 0)
|
|
944
|
+
await Promise.all(lines.values());
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
// src/telegram/update-store.ts
|
|
949
|
+
function isClaimable(record, now) {
|
|
950
|
+
if (record.state === "pending")
|
|
951
|
+
return true;
|
|
952
|
+
if (record.state === "failed")
|
|
953
|
+
return record.dueAt <= now;
|
|
954
|
+
return record.state === "processing" && record.dueAt < now;
|
|
955
|
+
}
|
|
956
|
+
function memoryTelegramUpdateStore() {
|
|
957
|
+
const records = new Map;
|
|
958
|
+
const holding = (updateId, attempt) => {
|
|
959
|
+
const record = records.get(updateId);
|
|
960
|
+
return record?.state === "processing" && record.attempts === attempt ? record : undefined;
|
|
961
|
+
};
|
|
962
|
+
return {
|
|
963
|
+
async add(update) {
|
|
964
|
+
if (records.has(update.updateId))
|
|
965
|
+
return false;
|
|
966
|
+
records.set(update.updateId, {
|
|
967
|
+
...update,
|
|
968
|
+
state: "pending",
|
|
969
|
+
attempts: 0,
|
|
970
|
+
dueAt: update.receivedAt,
|
|
971
|
+
settledAt: undefined,
|
|
972
|
+
error: undefined
|
|
973
|
+
});
|
|
974
|
+
return true;
|
|
975
|
+
},
|
|
976
|
+
async claim(updateId, options) {
|
|
977
|
+
const record = records.get(updateId);
|
|
978
|
+
if (!record || !isClaimable(record, options.now))
|
|
979
|
+
return;
|
|
980
|
+
if (record.attempts >= options.maxAttempts) {
|
|
981
|
+
Object.assign(record, {
|
|
982
|
+
state: "abandoned",
|
|
983
|
+
settledAt: options.now,
|
|
984
|
+
error: "attempts spent"
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
record.state = "processing";
|
|
989
|
+
record.attempts += 1;
|
|
990
|
+
record.dueAt = options.leaseUntil;
|
|
991
|
+
return record.attempts;
|
|
992
|
+
},
|
|
993
|
+
async renew(updateId, attempt, leaseUntil) {
|
|
994
|
+
const record = holding(updateId, attempt);
|
|
995
|
+
if (record)
|
|
996
|
+
record.dueAt = leaseUntil;
|
|
997
|
+
return record !== undefined;
|
|
998
|
+
},
|
|
999
|
+
async settle(updateId, attempt, settlement) {
|
|
1000
|
+
const record = holding(updateId, attempt);
|
|
1001
|
+
if (!record)
|
|
1002
|
+
return;
|
|
1003
|
+
record.state = settlement.state;
|
|
1004
|
+
record.error = settlement.state === "completed" ? undefined : settlement.error;
|
|
1005
|
+
if (settlement.state === "failed")
|
|
1006
|
+
record.dueAt = settlement.retryAt;
|
|
1007
|
+
else
|
|
1008
|
+
record.settledAt = settlement.at;
|
|
1009
|
+
},
|
|
1010
|
+
async due(query) {
|
|
1011
|
+
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 }));
|
|
1012
|
+
},
|
|
1013
|
+
async prune(before) {
|
|
1014
|
+
let pruned = 0;
|
|
1015
|
+
for (const [updateId, record] of records) {
|
|
1016
|
+
if (record.settledAt !== undefined && record.settledAt < before) {
|
|
1017
|
+
records.delete(updateId);
|
|
1018
|
+
pruned += 1;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return pruned;
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
// src/telegram/update-store-sqlite.ts
|
|
1026
|
+
var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
1027
|
+
var CLAIMABLE = `(state = 'pending' OR (state = 'failed' AND due_at <= ?1)
|
|
1028
|
+
OR (state = 'processing' AND due_at < ?1))`;
|
|
1029
|
+
function rowOf(row) {
|
|
1030
|
+
if (typeof row !== "object" || row === null)
|
|
1031
|
+
return;
|
|
1032
|
+
const updateId = "update_id" in row ? row.update_id : undefined;
|
|
1033
|
+
const body = "body" in row ? row.body : undefined;
|
|
1034
|
+
return typeof updateId === "number" && typeof body === "string" ? { updateId, body } : undefined;
|
|
1035
|
+
}
|
|
1036
|
+
function sqliteTelegramUpdateStore(config) {
|
|
1037
|
+
const table = config.table ?? "telegram_updates";
|
|
1038
|
+
if (!TABLE_NAME.test(table)) {
|
|
1039
|
+
throw new TypeError("[stitchkit] telegram update store: a table name is [A-Za-z_][A-Za-z0-9_]*");
|
|
1040
|
+
}
|
|
1041
|
+
const database = config.database;
|
|
1042
|
+
database.exec(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
1043
|
+
update_id INTEGER PRIMARY KEY,
|
|
1044
|
+
body TEXT NOT NULL,
|
|
1045
|
+
state TEXT NOT NULL,
|
|
1046
|
+
attempts INTEGER NOT NULL,
|
|
1047
|
+
due_at INTEGER NOT NULL,
|
|
1048
|
+
received_at INTEGER NOT NULL,
|
|
1049
|
+
settled_at INTEGER,
|
|
1050
|
+
error TEXT
|
|
1051
|
+
);
|
|
1052
|
+
CREATE INDEX IF NOT EXISTS ${table}_due ON ${table} (state, due_at);`);
|
|
1053
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO ${table} (update_id, body, state, attempts, due_at, received_at)
|
|
1054
|
+
VALUES (?1, ?2, 'pending', 0, ?3, ?3)`);
|
|
1055
|
+
const take = database.prepare(`UPDATE ${table} SET state = 'processing', attempts = attempts + 1, due_at = ?2, error = NULL
|
|
1056
|
+
WHERE update_id = ?3 AND attempts < ?4 AND ${CLAIMABLE} RETURNING attempts`);
|
|
1057
|
+
const spend = database.prepare(`UPDATE ${table} SET state = 'abandoned', settled_at = ?1, error = 'attempts spent'
|
|
1058
|
+
WHERE update_id = ?2 AND attempts >= ?3 AND ${CLAIMABLE}`);
|
|
1059
|
+
const renew = database.prepare(`UPDATE ${table} SET due_at = ?1 WHERE update_id = ?2 AND attempts = ?3 AND state = 'processing'`);
|
|
1060
|
+
const settle = database.prepare(`UPDATE ${table} SET state = ?1, due_at = COALESCE(?2, due_at), settled_at = ?3, error = ?4
|
|
1061
|
+
WHERE update_id = ?5 AND attempts = ?6 AND state = 'processing'`);
|
|
1062
|
+
const due = database.prepare(`SELECT update_id, body FROM ${table}
|
|
1063
|
+
WHERE (state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE})
|
|
1064
|
+
ORDER BY update_id LIMIT ?3`);
|
|
1065
|
+
const prune = database.prepare(`DELETE FROM ${table} WHERE state IN ('completed', 'abandoned') AND settled_at < ?1`);
|
|
1066
|
+
return {
|
|
1067
|
+
async add(update) {
|
|
1068
|
+
return Number(insert.run(update.updateId, update.body, update.receivedAt).changes) > 0;
|
|
1069
|
+
},
|
|
1070
|
+
async claim(updateId, options) {
|
|
1071
|
+
const [row] = take.all(options.now, options.leaseUntil, updateId, options.maxAttempts);
|
|
1072
|
+
const attempts = typeof row === "object" && row !== null && "attempts" in row ? row.attempts : undefined;
|
|
1073
|
+
if (typeof attempts === "number")
|
|
1074
|
+
return attempts;
|
|
1075
|
+
spend.run(options.now, updateId, options.maxAttempts);
|
|
1076
|
+
return;
|
|
1077
|
+
},
|
|
1078
|
+
async renew(updateId, attempt, leaseUntil) {
|
|
1079
|
+
return Number(renew.run(leaseUntil, updateId, attempt).changes) > 0;
|
|
1080
|
+
},
|
|
1081
|
+
async settle(updateId, attempt, settlement) {
|
|
1082
|
+
settle.run(settlement.state, settlement.state === "failed" ? settlement.retryAt : null, settlement.state === "failed" ? null : settlement.at, settlement.state === "completed" ? null : settlement.error, updateId, attempt);
|
|
1083
|
+
},
|
|
1084
|
+
async due(query) {
|
|
1085
|
+
return due.all(query.now, query.pendingBefore, query.limit).flatMap((row) => rowOf(row) ?? []);
|
|
1086
|
+
},
|
|
1087
|
+
async prune(before) {
|
|
1088
|
+
return Number(prune.run(before).changes);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
// src/telegram/webhook.ts
|
|
1093
|
+
var OWNER_PARAMETER = "owner";
|
|
1094
|
+
var OWNER_CONTEXT = "stitchkit:telegram-webhook-owner";
|
|
1095
|
+
var SECRET_SHAPE = /^[A-Za-z0-9_-]{1,256}$/;
|
|
1096
|
+
var SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
1097
|
+
var TELEGRAM_WEBHOOK_NONE = "none";
|
|
1098
|
+
|
|
1099
|
+
class TelegramWebhookClaimError extends Error {
|
|
1100
|
+
reason;
|
|
1101
|
+
ownerHost;
|
|
1102
|
+
constructor(reason, ownerHost) {
|
|
1103
|
+
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`);
|
|
1104
|
+
this.name = "TelegramWebhookClaimError";
|
|
1105
|
+
this.reason = reason;
|
|
1106
|
+
this.ownerHost = ownerHost;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function hostOf(url) {
|
|
1110
|
+
if (url === "")
|
|
1111
|
+
return TELEGRAM_WEBHOOK_NONE;
|
|
1112
|
+
return URL.canParse(url) ? new URL(url).host : "unknown";
|
|
1113
|
+
}
|
|
1114
|
+
function withoutTag(url) {
|
|
1115
|
+
if (!URL.canParse(url))
|
|
1116
|
+
return url;
|
|
1117
|
+
const parsed = new URL(url);
|
|
1118
|
+
parsed.searchParams.delete(OWNER_PARAMETER);
|
|
1119
|
+
return parsed.toString();
|
|
1120
|
+
}
|
|
1121
|
+
async function telegramWebhookUrl(url, secret) {
|
|
1122
|
+
if (!SECRET_SHAPE.test(secret)) {
|
|
1123
|
+
throw new TypeError("[stitchkit] telegram webhook: a secret is 1–256 of A-Z, a-z, 0-9, _ and -");
|
|
1124
|
+
}
|
|
1125
|
+
const tagged = new URL(url);
|
|
1126
|
+
const tag = toHex(await hmacSha256(new TextEncoder().encode(secret), OWNER_CONTEXT));
|
|
1127
|
+
tagged.searchParams.set(OWNER_PARAMETER, tag.slice(0, 16));
|
|
1128
|
+
return tagged.toString();
|
|
1129
|
+
}
|
|
1130
|
+
async function webhookInfo(config) {
|
|
1131
|
+
const result = await callTelegramBotApi({ ...botCall(config), method: "getWebhookInfo" });
|
|
1132
|
+
const info = isRecord(result) ? result : {};
|
|
1133
|
+
const at = info.last_error_date;
|
|
1134
|
+
const message = info.last_error_message;
|
|
1135
|
+
return {
|
|
1136
|
+
url: typeof info.url === "string" ? info.url : "",
|
|
1137
|
+
pendingUpdates: typeof info.pending_update_count === "number" ? info.pending_update_count : 0,
|
|
1138
|
+
...typeof at === "number" && typeof message === "string" && { lastError: { at: at * 1000, message } }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function botCall(config) {
|
|
1142
|
+
return {
|
|
1143
|
+
token: config.token,
|
|
1144
|
+
...config.apiRoot && { apiRoot: config.apiRoot },
|
|
1145
|
+
...config.fetch && { fetch: config.fetch },
|
|
1146
|
+
...config.signal && { signal: config.signal }
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
async function claimTelegramWebhook(config) {
|
|
1150
|
+
const url = await telegramWebhookUrl(config.url, config.secret);
|
|
1151
|
+
const current = await webhookInfo(config);
|
|
1152
|
+
const ownerHost = hostOf(current.url);
|
|
1153
|
+
const owned = current.url === url;
|
|
1154
|
+
if (!owned && config.takeoverFrom !== ownerHost) {
|
|
1155
|
+
const sameAddress = current.url !== "" && withoutTag(current.url) === withoutTag(url);
|
|
1156
|
+
throw new TelegramWebhookClaimError(sameAddress ? "other-secret" : "owned-elsewhere", ownerHost);
|
|
1157
|
+
}
|
|
1158
|
+
await callTelegramBotApi({
|
|
1159
|
+
...botCall(config),
|
|
1160
|
+
method: "setWebhook",
|
|
1161
|
+
params: {
|
|
1162
|
+
url,
|
|
1163
|
+
secret_token: config.secret,
|
|
1164
|
+
drop_pending_updates: config.dropPendingUpdates ?? false,
|
|
1165
|
+
...config.allowedUpdates && { allowed_updates: config.allowedUpdates },
|
|
1166
|
+
...config.maxConnections !== undefined && { max_connections: config.maxConnections }
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
const confirmed = await webhookInfo(config);
|
|
1170
|
+
if (confirmed.url !== url) {
|
|
1171
|
+
throw new TelegramWebhookClaimError("not-confirmed", hostOf(confirmed.url));
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
url,
|
|
1175
|
+
...!owned && { takenFrom: ownerHost },
|
|
1176
|
+
pendingUpdates: confirmed.pendingUpdates
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
async function checkTelegramWebhook(config) {
|
|
1180
|
+
const url = await telegramWebhookUrl(config.url, config.secret);
|
|
1181
|
+
const info = await webhookInfo(config);
|
|
1182
|
+
return {
|
|
1183
|
+
owned: info.url === url,
|
|
1184
|
+
ownerHost: hostOf(info.url),
|
|
1185
|
+
pendingUpdates: info.pendingUpdates,
|
|
1186
|
+
...info.lastError && { lastError: info.lastError }
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
async function receiveTelegramWebhook(request, options) {
|
|
1190
|
+
if (request.method !== "POST") {
|
|
1191
|
+
return new Response(null, { status: 405, headers: { allow: "POST" } });
|
|
1192
|
+
}
|
|
1193
|
+
const secret = request.headers.get(SECRET_HEADER);
|
|
1194
|
+
if (secret === null || !digestsEqual(secret, options.secret)) {
|
|
1195
|
+
return new Response(null, { status: 403 });
|
|
1196
|
+
}
|
|
1197
|
+
const acceptance = await options.accept(await request.text());
|
|
1198
|
+
return new Response(null, { status: acceptance === "invalid" ? 400 : 200 });
|
|
1199
|
+
}
|
|
817
1200
|
export {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
telegramBroadcastSender,
|
|
821
|
-
runTelegramBroadcast,
|
|
822
|
-
createTelegramOperatorChannel,
|
|
823
|
-
createTelegramLocalFiles,
|
|
824
|
-
classifyTelegramSendFailure,
|
|
825
|
-
callTelegramBotApi,
|
|
826
|
-
TelegramLocalFileError,
|
|
1201
|
+
TELEGRAM_BOT_TOKEN_PATTERN,
|
|
1202
|
+
TELEGRAM_WEBHOOK_NONE,
|
|
827
1203
|
TelegramBotApiError,
|
|
828
|
-
|
|
1204
|
+
TelegramLocalFileError,
|
|
1205
|
+
TelegramWebhookClaimError,
|
|
1206
|
+
callTelegramBotApi,
|
|
1207
|
+
checkTelegramWebhook,
|
|
1208
|
+
claimTelegramWebhook,
|
|
1209
|
+
classifyTelegramSendFailure2 as classifyTelegramSendFailure,
|
|
1210
|
+
createTelegramLocalFiles,
|
|
1211
|
+
createTelegramOperatorChannel,
|
|
1212
|
+
createTelegramUpdateIntake,
|
|
1213
|
+
memoryTelegramUpdateStore,
|
|
1214
|
+
receiveTelegramWebhook,
|
|
1215
|
+
runTelegramBroadcast,
|
|
1216
|
+
sqliteTelegramUpdateStore,
|
|
1217
|
+
telegramBroadcastSender,
|
|
1218
|
+
telegramOperatorSender,
|
|
1219
|
+
telegramWebhookUrl,
|
|
1220
|
+
verifyTelegramInitData
|
|
829
1221
|
};
|