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.
Files changed (242) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +2 -1
  3. package/dist/entrypoints/agent-runtime/browser.js +119 -119
  4. package/dist/entrypoints/agent-runtime/coding-tools.js +15 -20
  5. package/dist/entrypoints/agent-runtime/harness.js +55 -63
  6. package/dist/entrypoints/agent-runtime/openrouter.js +6 -6
  7. package/dist/entrypoints/agent-runtime/sandbox.js +20 -25
  8. package/dist/entrypoints/agent-runtime/sqlite/bun.js +11 -13
  9. package/dist/entrypoints/agent-runtime/sqlite/node.js +11 -13
  10. package/dist/entrypoints/agent-runtime/testing.js +32 -34
  11. package/dist/entrypoints/agent-runtime.js +395 -402
  12. package/dist/entrypoints/application/diagnostic-journal.js +26 -27
  13. package/dist/entrypoints/application/directory-inbox.js +16 -17
  14. package/dist/entrypoints/application/grammy.js +13 -13
  15. package/dist/entrypoints/application/opentelemetry.js +13 -13
  16. package/dist/entrypoints/application/schemas.js +24 -24
  17. package/dist/entrypoints/application.js +209 -210
  18. package/dist/entrypoints/bin/upgrade-cli.js +2 -2
  19. package/dist/entrypoints/cli.js +81 -84
  20. package/dist/entrypoints/contract.js +54 -57
  21. package/dist/entrypoints/declaration.js +22 -22
  22. package/dist/entrypoints/files.js +18 -21
  23. package/dist/entrypoints/geo.js +7 -8
  24. package/dist/entrypoints/google.js +2 -2
  25. package/dist/entrypoints/index.js +150 -154
  26. package/dist/entrypoints/live.js +45 -47
  27. package/dist/entrypoints/node.js +68 -74
  28. package/dist/entrypoints/oauth.js +6 -6
  29. package/dist/entrypoints/observability.d.ts +1 -0
  30. package/dist/entrypoints/observability.d.ts.map +1 -1
  31. package/dist/entrypoints/observability.js +116 -87
  32. package/dist/entrypoints/primitives.js +35 -35
  33. package/dist/entrypoints/react/keyboard.d.ts +4 -0
  34. package/dist/entrypoints/react/keyboard.d.ts.map +1 -0
  35. package/dist/entrypoints/react/keyboard.js +397 -0
  36. package/dist/entrypoints/react.js +10 -10
  37. package/dist/entrypoints/release.js +7 -7
  38. package/dist/entrypoints/remote.js +15 -17
  39. package/dist/entrypoints/server.js +198 -200
  40. package/dist/entrypoints/telegram/html.d.ts +5 -0
  41. package/dist/entrypoints/telegram/html.d.ts.map +1 -0
  42. package/dist/entrypoints/telegram/html.js +26 -0
  43. package/dist/entrypoints/telegram/screens.d.ts +14 -0
  44. package/dist/entrypoints/telegram/screens.d.ts.map +1 -0
  45. package/dist/entrypoints/telegram/screens.js +2028 -0
  46. package/dist/entrypoints/telegram.d.ts +11 -3
  47. package/dist/entrypoints/telegram.d.ts.map +1 -1
  48. package/dist/entrypoints/telegram.js +779 -159
  49. package/dist/entrypoints/testing.js +75 -79
  50. package/dist/entrypoints/tools/connections.js +30 -32
  51. package/dist/entrypoints/tools/contract.js +18 -20
  52. package/dist/entrypoints/tools/invoker.js +12 -17
  53. package/dist/entrypoints/tools.js +224 -229
  54. package/dist/entrypoints/tracking/server.js +6 -6
  55. package/dist/entrypoints/tracking.js +40 -42
  56. package/dist/entrypoints/voice.js +3 -3
  57. package/dist/{index-eaysmdtq.js → index-0ndk4555.js} +56 -56
  58. package/dist/{index-2bxbyab0.js → index-1bx1xv1k.js} +1 -1
  59. package/dist/{index-p5r7qpdd.js → index-1eeqpadm.js} +4 -4
  60. package/dist/{index-hadmp193.js → index-3ct7hhq6.js} +210 -210
  61. package/dist/{index-e01r054k.js → index-45tdvqa3.js} +26 -26
  62. package/dist/{index-dce5wxcq.js → index-4s0fep5q.js} +8 -8
  63. package/dist/index-50ejfz75.js +9 -0
  64. package/dist/{index-m9zn6cpq.js → index-5107q189.js} +12 -12
  65. package/dist/{index-sbdmyz75.js → index-5cc6r990.js} +3 -3
  66. package/dist/{index-c6bxbsnc.js → index-7kxw1awn.js} +2 -2
  67. package/dist/{index-0r0fbk7z.js → index-7z9ym7t8.js} +28 -28
  68. package/dist/{index-p7hgdyhb.js → index-814xpr6d.js} +2 -2
  69. package/dist/{index-gtj3zc53.js → index-8kzct1d6.js} +99 -99
  70. package/dist/{index-1zbbzgfh.js → index-9pw0a51a.js} +4 -4
  71. package/dist/{index-79z7m7dt.js → index-a9pd417t.js} +35 -35
  72. package/dist/{index-ham5sz8m.js → index-aaj97r4y.js} +45 -45
  73. package/dist/{index-s812ycat.js → index-aan6hqb9.js} +3 -3
  74. package/dist/{index-10x2hbx0.js → index-adhgd4pe.js} +44 -44
  75. package/dist/{index-2gxzjvyr.js → index-afjvjyck.js} +23 -23
  76. package/dist/{index-qmy6zq75.js → index-ahvsksw5.js} +16 -16
  77. package/dist/{index-hr4tzbwq.js → index-ap3gqe56.js} +3 -3
  78. package/dist/{index-znfs20ss.js → index-b2bz0kdm.js} +16 -16
  79. package/dist/{index-r066jx76.js → index-c6aagrbd.js} +148 -120
  80. package/dist/{index-k30h66yd.js → index-ck71tbcf.js} +9 -9
  81. package/dist/{index-eqqvgdb3.js → index-ct75c3at.js} +2 -2
  82. package/dist/{index-gw8bmyaj.js → index-djjhhgjk.js} +41 -41
  83. package/dist/{index-8qt2m66q.js → index-dvwj1f43.js} +43 -43
  84. package/dist/index-e05cjgtf.js +144 -0
  85. package/dist/{index-yw4fzmdz.js → index-e6c2spsd.js} +79 -30
  86. package/dist/{index-fhwqwxe0.js → index-ej331hah.js} +1 -1
  87. package/dist/{index-9t3dffv2.js → index-ewbaywk5.js} +5 -5
  88. package/dist/{index-7p1f0vp2.js → index-fyphpknh.js} +25 -25
  89. package/dist/{index-83bdg97s.js → index-g149e9pk.js} +1 -1
  90. package/dist/{index-ngfhvfpd.js → index-g6a3na27.js} +4 -4
  91. package/dist/{index-qwhynv6x.js → index-h124epcf.js} +20 -20
  92. package/dist/{index-h6eeys1v.js → index-h97tqqp1.js} +9 -9
  93. package/dist/{index-zcgf3gqf.js → index-hby4qhax.js} +10 -10
  94. package/dist/{index-5jrd0yng.js → index-j8aw2c0e.js} +4 -4
  95. package/dist/{index-d8m8zpd8.js → index-jzczzcsb.js} +14 -14
  96. package/dist/index-kqnd6a85.js +26 -0
  97. package/dist/{index-35mh5bhj.js → index-kw1ws0cs.js} +12 -12
  98. package/dist/{index-4gcnakhx.js → index-m38ej7e5.js} +2 -2
  99. package/dist/{index-ndddxtyx.js → index-m79yr938.js} +19 -19
  100. package/dist/{index-5p2m7we1.js → index-m9fndta6.js} +11 -11
  101. package/dist/index-mnkk1ffz.js +150 -0
  102. package/dist/index-n15jny5b.js +4 -0
  103. package/dist/{index-2vtcmrj9.js → index-n53mswt3.js} +20 -20
  104. package/dist/{index-bt14acb5.js → index-n5yqxd3d.js} +21 -21
  105. package/dist/{index-bm5rx7n6.js → index-n9tyz8z3.js} +12 -12
  106. package/dist/index-nmaqxdx7.js +463 -0
  107. package/dist/{index-mqv3b5rh.js → index-nx6eazt0.js} +30 -30
  108. package/dist/{index-3z73fh2c.js → index-q0th8q0z.js} +9 -9
  109. package/dist/{index-v260df89.js → index-qjeggp8w.js} +20 -20
  110. package/dist/{index-zfrhfn82.js → index-r1z0rd9c.js} +20 -20
  111. package/dist/{index-wqgjfstn.js → index-r5c4fedv.js} +3 -3
  112. package/dist/{index-cyktc4bp.js → index-r5hsgkz5.js} +118 -118
  113. package/dist/{index-0b1ess5e.js → index-r7a7r7ha.js} +1 -1
  114. package/dist/{index-1j8pm5t0.js → index-rfkh44ss.js} +15 -15
  115. package/dist/index-rz3z13e9.js +47 -0
  116. package/dist/{index-k6gpggb9.js → index-s80awwsw.js} +35 -35
  117. package/dist/{index-vqr7f7v8.js → index-ssmd41kh.js} +124 -124
  118. package/dist/index-sznqf8fx.js +20 -0
  119. package/dist/{index-ensd0r74.js → index-v14qqmys.js} +41 -41
  120. package/dist/{index-paz6sgdp.js → index-v4a2fjtk.js} +36 -36
  121. package/dist/{index-d9pk3td4.js → index-w0eqdhyd.js} +14 -14
  122. package/dist/{index-5qn1kx34.js → index-w575evhq.js} +29 -29
  123. package/dist/{index-3gye9wzb.js → index-w5s3xftz.js} +26 -26
  124. package/dist/{index-tm88avnj.js → index-wykebpn9.js} +3 -3
  125. package/dist/{index-sh3s86na.js → index-xkhk5d6g.js} +17 -17
  126. package/dist/{index-6k1937bx.js → index-xqhede6x.js} +4 -4
  127. package/dist/{index-f4fx2tza.js → index-zs4ms3th.js} +181 -181
  128. package/dist/{index-k5hsfc3j.js → index-zsvzhjtk.js} +1 -1
  129. package/dist/internal/secret-shapes.d.ts +21 -0
  130. package/dist/internal/secret-shapes.d.ts.map +1 -0
  131. package/dist/observability/bounded-logger.d.ts +2 -0
  132. package/dist/observability/bounded-logger.d.ts.map +1 -1
  133. package/dist/observability/sanitize.d.ts +6 -0
  134. package/dist/observability/sanitize.d.ts.map +1 -1
  135. package/dist/observability/secret-values.d.ts +25 -0
  136. package/dist/observability/secret-values.d.ts.map +1 -0
  137. package/dist/react/keyboard/hooks.d.ts +71 -0
  138. package/dist/react/keyboard/hooks.d.ts.map +1 -0
  139. package/dist/react/keyboard/layers.d.ts +98 -0
  140. package/dist/react/keyboard/layers.d.ts.map +1 -0
  141. package/dist/react/keyboard/zone.d.ts +139 -0
  142. package/dist/react/keyboard/zone.d.ts.map +1 -0
  143. package/dist/telegram/bot-api.d.ts +7 -4
  144. package/dist/telegram/bot-api.d.ts.map +1 -1
  145. package/dist/telegram/crypto.d.ts +16 -0
  146. package/dist/telegram/crypto.d.ts.map +1 -0
  147. package/dist/telegram/html/check.d.ts +26 -0
  148. package/dist/telegram/html/check.d.ts.map +1 -0
  149. package/dist/telegram/html/nodes.d.ts +70 -0
  150. package/dist/telegram/html/nodes.d.ts.map +1 -0
  151. package/dist/telegram/html/parse.d.ts +35 -0
  152. package/dist/telegram/html/parse.d.ts.map +1 -0
  153. package/dist/telegram/html/render.d.ts +39 -0
  154. package/dist/telegram/html/render.d.ts.map +1 -0
  155. package/dist/telegram/init-data.d.ts +3 -44
  156. package/dist/telegram/init-data.d.ts.map +1 -1
  157. package/dist/telegram/operator-channel.d.ts +19 -3
  158. package/dist/telegram/operator-channel.d.ts.map +1 -1
  159. package/dist/telegram/operator-dedupe.d.ts +44 -0
  160. package/dist/telegram/operator-dedupe.d.ts.map +1 -0
  161. package/dist/telegram/screens/callback-codec.d.ts +34 -0
  162. package/dist/telegram/screens/callback-codec.d.ts.map +1 -0
  163. package/dist/telegram/screens/dispatch.d.ts +22 -0
  164. package/dist/telegram/screens/dispatch.d.ts.map +1 -0
  165. package/dist/telegram/screens/display.d.ts +70 -0
  166. package/dist/telegram/screens/display.d.ts.map +1 -0
  167. package/dist/telegram/screens/execute.d.ts +57 -0
  168. package/dist/telegram/screens/execute.d.ts.map +1 -0
  169. package/dist/telegram/screens/html.d.ts +36 -0
  170. package/dist/telegram/screens/html.d.ts.map +1 -0
  171. package/dist/telegram/screens/outcome.d.ts +74 -0
  172. package/dist/telegram/screens/outcome.d.ts.map +1 -0
  173. package/dist/telegram/screens/path.d.ts +29 -0
  174. package/dist/telegram/screens/path.d.ts.map +1 -0
  175. package/dist/telegram/screens/reconcile.d.ts +67 -0
  176. package/dist/telegram/screens/reconcile.d.ts.map +1 -0
  177. package/dist/telegram/screens/registry.d.ts +23 -0
  178. package/dist/telegram/screens/registry.d.ts.map +1 -0
  179. package/dist/telegram/screens/render.d.ts +70 -0
  180. package/dist/telegram/screens/render.d.ts.map +1 -0
  181. package/dist/telegram/screens/runtime.d.ts +133 -0
  182. package/dist/telegram/screens/runtime.d.ts.map +1 -0
  183. package/dist/telegram/screens/screen-types.d.ts +130 -0
  184. package/dist/telegram/screens/screen-types.d.ts.map +1 -0
  185. package/dist/telegram/screens/screen.d.ts +138 -0
  186. package/dist/telegram/screens/screen.d.ts.map +1 -0
  187. package/dist/telegram/screens/state.d.ts +115 -0
  188. package/dist/telegram/screens/state.d.ts.map +1 -0
  189. package/dist/telegram/screens/telegram-screens.d.ts +18 -0
  190. package/dist/telegram/screens/telegram-screens.d.ts.map +1 -0
  191. package/dist/telegram/screens/test-chat-telegram.d.ts +47 -0
  192. package/dist/telegram/screens/test-chat-telegram.d.ts.map +1 -0
  193. package/dist/telegram/screens/test-chat.d.ts +66 -0
  194. package/dist/telegram/screens/test-chat.d.ts.map +1 -0
  195. package/dist/telegram/screens/view.d.ts +89 -0
  196. package/dist/telegram/screens/view.d.ts.map +1 -0
  197. package/dist/telegram/send-failure.d.ts +25 -0
  198. package/dist/telegram/send-failure.d.ts.map +1 -1
  199. package/dist/telegram/update-intake.d.ts +82 -0
  200. package/dist/telegram/update-intake.d.ts.map +1 -0
  201. package/dist/telegram/update-store-check.d.ts +22 -0
  202. package/dist/telegram/update-store-check.d.ts.map +1 -0
  203. package/dist/telegram/update-store-postgres.d.ts +33 -0
  204. package/dist/telegram/update-store-postgres.d.ts.map +1 -0
  205. package/dist/telegram/update-store-sqlite.d.ts +26 -0
  206. package/dist/telegram/update-store-sqlite.d.ts.map +1 -0
  207. package/dist/telegram/update-store.d.ts +67 -0
  208. package/dist/telegram/update-store.d.ts.map +1 -0
  209. package/dist/telegram/user.d.ts +51 -0
  210. package/dist/telegram/user.d.ts.map +1 -0
  211. package/dist/telegram/webhook.d.ts +105 -0
  212. package/dist/telegram/webhook.d.ts.map +1 -0
  213. package/dist/tools/cli/args-fields.d.ts +8 -0
  214. package/dist/tools/cli/args-fields.d.ts.map +1 -1
  215. package/dist/tools/cli/args-route.d.ts.map +1 -1
  216. package/dist/tools/cli/args.d.ts.map +1 -1
  217. package/dist/tools/cli/help.d.ts.map +1 -1
  218. package/llms/cli.txt +8 -2
  219. package/llms/observability.txt +15 -2
  220. package/llms/react-keyboard.txt +193 -0
  221. package/llms/server.4.txt +16 -0
  222. package/llms/stitchkit.txt +10 -4
  223. package/llms/telegram-html.txt +35 -0
  224. package/llms/telegram-screens.txt +365 -0
  225. package/llms/telegram.txt +245 -11
  226. package/llms/{upgrading-0.74.0-0.100.0.txt → upgrading-0.74.0-0.101.0.txt} +39 -3
  227. package/llms/upgrading.txt +1 -1
  228. package/llms-full.txt +889 -19
  229. package/llms.txt +12 -7
  230. package/package.json +17 -2
  231. package/dist/index-2k4yrqkc.js +0 -9
  232. package/dist/index-crykmqkr.js +0 -26
  233. package/dist/index-y64k3s0h.js +0 -150
  234. package/dist/index-y91ry9h2.js +0 -4
  235. /package/dist/{index-hffjpmq9.js → index-9jfdm5tg.js} +0 -0
  236. /package/dist/{index-zjjyynm3.js → index-d4pa2bgs.js} +0 -0
  237. /package/dist/{index-rxfy4cq7.js → index-dwfv9a85.js} +0 -0
  238. /package/dist/{index-5jm36zv1.js → index-gfk7nrht.js} +0 -0
  239. /package/dist/{index-b9aefrqd.js → index-n1dqr7gf.js} +0 -0
  240. /package/dist/{index-q3pyb9n0.js → index-sykb5ag7.js} +0 -0
  241. /package/dist/{index-48ffdxgk.js → index-xhzq2qvj.js} +0 -0
  242. /package/dist/{index-kzfs85xp.js → index-zsarb5xp.js} +0 -0
@@ -1,9 +1,24 @@
1
1
  import {
2
- isRecord
3
- } from "../index-hffjpmq9.js";
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 = /\d{5,}:[A-Za-z0-9_-]{30,}/;
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 = classifyTelegramSendFailure(error);
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/init-data.ts
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 TelegramInitDataUserSchema = z2.object({
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
- 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;
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
- return TelegramInitDataUserSchema.parse(JSON.parse(encoded));
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(encoder.encode("WebAppData"), options.botToken);
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((resolve2, reject) => {
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
- resolve2();
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 <= TEXT_LIMIT ? text : `${points.slice(0, TEXT_LIMIT - 1).join("")}…`;
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 = [TELEGRAM_BOT_TOKEN_PATTERN, ...config.sensitivePatterns ?? []];
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 = classifyTelegramSendFailure(error);
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((resolve2) => {
752
+ return new Promise((resolve) => {
781
753
  const done = () => {
782
754
  signal?.removeEventListener("abort", done);
783
755
  idle.delete(done);
784
- resolve2();
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
- verifyTelegramInitData,
819
- telegramOperatorSender,
820
- telegramBroadcastSender,
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
- TELEGRAM_BOT_TOKEN_PATTERN
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
  };