stitchkit 0.95.3 → 0.96.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 (52) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/application/grammy-bot.d.ts +44 -0
  3. package/dist/application/grammy-bot.d.ts.map +1 -0
  4. package/dist/application/grammy-update-admission.d.ts +35 -0
  5. package/dist/application/grammy-update-admission.d.ts.map +1 -0
  6. package/dist/application/grammy.d.ts +23 -4
  7. package/dist/application/grammy.d.ts.map +1 -1
  8. package/dist/application/kernel-admission.d.ts +11 -0
  9. package/dist/application/kernel-admission.d.ts.map +1 -1
  10. package/dist/application/kernel-resource-context.d.ts.map +1 -1
  11. package/dist/application/resource.d.ts +22 -0
  12. package/dist/application/resource.d.ts.map +1 -1
  13. package/dist/entrypoints/application/grammy.d.ts +2 -1
  14. package/dist/entrypoints/application/grammy.d.ts.map +1 -1
  15. package/dist/entrypoints/application/grammy.js +139 -5
  16. package/dist/entrypoints/application.d.ts +1 -1
  17. package/dist/entrypoints/application.d.ts.map +1 -1
  18. package/dist/entrypoints/application.js +1 -1
  19. package/dist/entrypoints/observability.d.ts +1 -0
  20. package/dist/entrypoints/observability.d.ts.map +1 -1
  21. package/dist/entrypoints/observability.js +46 -4
  22. package/dist/entrypoints/telegram.d.ts +4 -0
  23. package/dist/entrypoints/telegram.d.ts.map +1 -1
  24. package/dist/entrypoints/telegram.js +680 -88
  25. package/dist/entrypoints/testing.js +1 -1
  26. package/dist/{index-w906953s.js → index-5qn1kx34.js} +48 -0
  27. package/dist/observability/json-logger.d.ts +38 -0
  28. package/dist/observability/json-logger.d.ts.map +1 -0
  29. package/dist/telegram/bot-api.d.ts +50 -0
  30. package/dist/telegram/bot-api.d.ts.map +1 -0
  31. package/dist/telegram/broadcast-state.d.ts +67 -0
  32. package/dist/telegram/broadcast-state.d.ts.map +1 -0
  33. package/dist/telegram/broadcast.d.ts +93 -0
  34. package/dist/telegram/broadcast.d.ts.map +1 -0
  35. package/dist/telegram/local-files.d.ts +63 -0
  36. package/dist/telegram/local-files.d.ts.map +1 -0
  37. package/dist/telegram/operator-channel.d.ts +72 -0
  38. package/dist/telegram/operator-channel.d.ts.map +1 -0
  39. package/llms/application-grammy.txt +5 -0
  40. package/llms/application.2.txt +1 -0
  41. package/llms/application.txt +51 -13
  42. package/llms/observability.txt +5 -0
  43. package/llms/server.4.txt +3 -0
  44. package/llms/stitchkit.txt +5 -5
  45. package/llms/telegram.txt +159 -3
  46. package/llms/{upgrading-0.37.0-0.53.0.txt → upgrading-0.37.0-0.56.0.txt} +130 -3
  47. package/llms/{upgrading-0.55.0-0.68.0.txt → upgrading-0.57.0-0.69.0.txt} +44 -130
  48. package/llms/{upgrading-0.69.0-0.95.0.txt → upgrading-0.70.0-0.96.0.txt} +40 -44
  49. package/llms/upgrading.txt +3 -3
  50. package/llms-full.txt +265 -23
  51. package/llms.txt +10 -9
  52. package/package.json +1 -1
@@ -2,107 +2,179 @@ import {
2
2
  isRecord
3
3
  } from "../index-hffjpmq9.js";
4
4
 
5
- // src/telegram/init-data.ts
5
+ // src/telegram/bot-api.ts
6
+ var TELEGRAM_BOT_TOKEN_PATTERN = /\d{5,}:[A-Za-z0-9_-]{30,}/;
7
+ var DEFAULT_API_ROOT = "https://api.telegram.org";
8
+
9
+ class TelegramBotApiError extends Error {
10
+ method;
11
+ error_code;
12
+ description;
13
+ parameters;
14
+ constructor(method, answer) {
15
+ super(`Telegram ${method} failed: ${answer.error_code} ${answer.description}`);
16
+ this.name = "TelegramBotApiError";
17
+ this.method = method;
18
+ this.error_code = answer.error_code;
19
+ this.description = answer.description;
20
+ this.parameters = answer.parameters ?? {};
21
+ }
22
+ }
23
+ function numberField(value, key) {
24
+ if (!isRecord(value))
25
+ return;
26
+ const field = value[key];
27
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
28
+ }
29
+ async function callTelegramBotApi(call) {
30
+ const root = (call.apiRoot ?? DEFAULT_API_ROOT).replace(/\/+$/, "");
31
+ let response;
32
+ try {
33
+ response = await (call.fetch ?? fetch)(`${root}/bot${call.token}/${call.method}`, {
34
+ method: "POST",
35
+ headers: { "content-type": "application/json" },
36
+ body: JSON.stringify(call.params ?? {}),
37
+ ...call.signal && { signal: call.signal }
38
+ });
39
+ } catch (error) {
40
+ if (call.signal?.aborted)
41
+ throw call.signal.reason;
42
+ const kind = error instanceof Error ? error.name : "Error";
43
+ throw new Error(`Telegram ${call.method} request failed (${kind})`);
44
+ }
45
+ let answer;
46
+ try {
47
+ answer = await response.json();
48
+ } catch {
49
+ throw new TelegramBotApiError(call.method, {
50
+ error_code: response.status,
51
+ description: "response was not JSON"
52
+ });
53
+ }
54
+ if (isRecord(answer) && answer.ok === true)
55
+ return answer.result;
56
+ const parameters = isRecord(answer) ? answer.parameters : undefined;
57
+ const retryAfter = numberField(parameters, "retry_after");
58
+ const migrateTo = numberField(parameters, "migrate_to_chat_id");
59
+ throw new TelegramBotApiError(call.method, {
60
+ error_code: numberField(answer, "error_code") ?? response.status,
61
+ description: isRecord(answer) && typeof answer.description === "string" ? answer.description : "no description",
62
+ parameters: {
63
+ ...retryAfter !== undefined && { retry_after: retryAfter },
64
+ ...migrateTo !== undefined && { migrate_to_chat_id: migrateTo }
65
+ }
66
+ });
67
+ }
68
+ // src/telegram/broadcast-state.ts
69
+ import { appendFile, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
70
+ import { join } from "node:path";
6
71
  import { z } from "zod";
7
- var TelegramInitDataUserSchema = z.object({
8
- id: z.int(),
9
- is_bot: z.boolean().optional(),
10
- first_name: z.string(),
11
- last_name: z.string().optional(),
12
- username: z.string().optional(),
13
- language_code: z.string().optional(),
14
- is_premium: z.boolean().optional(),
15
- allows_write_to_pm: z.boolean().optional(),
16
- photo_url: z.string().optional()
17
- }).transform((user) => ({
18
- id: user.id,
19
- firstName: user.first_name,
20
- ...user.is_bot !== undefined && { isBot: user.is_bot },
21
- ...user.last_name !== undefined && { lastName: user.last_name },
22
- ...user.username !== undefined && { username: user.username },
23
- ...user.language_code !== undefined && { languageCode: user.language_code },
24
- ...user.is_premium !== undefined && { isPremium: user.is_premium },
25
- ...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
26
- ...user.photo_url !== undefined && { photoUrl: user.photo_url }
27
- }));
28
- var encoder = new TextEncoder;
29
- async function hmacSha256(key, message) {
30
- const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
31
- return crypto.subtle.sign("HMAC", imported, encoder.encode(message));
72
+ var TelegramBroadcastOutcomeSchema = z.enum([
73
+ "delivered",
74
+ "unreachable",
75
+ "failed",
76
+ "uncertain"
77
+ ]);
78
+ var ChatIdSchema = z.union([z.number().int(), z.string().min(1).max(64)]);
79
+ var RecipientsSchema = z.array(ChatIdSchema);
80
+ var JournalLineSchema = z.union([
81
+ z.object({ i: z.number().int().nonnegative(), s: z.enum(["sending", "released"]) }).strict(),
82
+ z.object({
83
+ i: z.number().int().nonnegative(),
84
+ o: TelegramBroadcastOutcomeSchema,
85
+ r: z.string().max(64).optional()
86
+ }).strict()
87
+ ]);
88
+ var NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
89
+ function assertBroadcastName(name) {
90
+ if (!NAME.test(name)) {
91
+ throw new TypeError("Telegram broadcast name must be 1–128 of [A-Za-z0-9._-] and start with a letter or digit");
92
+ }
32
93
  }
33
- function toHex(buffer) {
34
- return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
94
+ var absent = (error) => error instanceof Error && ("code" in error) && error.code === "ENOENT";
95
+ function broadcastFiles(directory, name) {
96
+ return {
97
+ recipients: join(directory, `${name}.recipients.json`),
98
+ journal: join(directory, `${name}.journal.ndjson`),
99
+ lock: join(directory, `${name}.lock`)
100
+ };
35
101
  }
36
- function digestsEqual(a, b) {
37
- if (a.length !== b.length)
38
- return false;
39
- let difference = 0;
40
- for (let index = 0;index < a.length; index += 1) {
41
- difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
102
+ async function readRecipients(files) {
103
+ try {
104
+ return RecipientsSchema.parse(JSON.parse(await readFile(files.recipients, "utf8")));
105
+ } catch (error) {
106
+ if (absent(error))
107
+ return null;
108
+ throw error;
42
109
  }
43
- return difference === 0;
44
110
  }
45
- function optionalUser(raw, key) {
46
- const encoded = raw[key];
47
- if (encoded === undefined)
48
- return;
49
- return TelegramInitDataUserSchema.parse(JSON.parse(encoded));
111
+ async function writeRecipients(files, recipients) {
112
+ const temporary = `${files.recipients}.${process.pid}.tmp`;
113
+ await writeFile(temporary, JSON.stringify(recipients));
114
+ await rename(temporary, files.recipients);
50
115
  }
51
- async function verifyTelegramInitData(options) {
52
- if (!options.botToken)
53
- throw new TypeError("botToken is required to verify initData");
54
- if (options.maxAgeSeconds !== undefined && (!Number.isFinite(options.maxAgeSeconds) || options.maxAgeSeconds < 0)) {
55
- throw new TypeError("maxAgeSeconds must be a non-negative finite number");
116
+ async function readProgress(files) {
117
+ const progress = new Map;
118
+ let text;
119
+ try {
120
+ text = await readFile(files.journal, "utf8");
121
+ } catch (error) {
122
+ if (absent(error))
123
+ return progress;
124
+ throw error;
56
125
  }
57
- const params = new URLSearchParams(options.initData);
58
- const hash = params.get("hash");
59
- if (!hash)
60
- return { valid: false, reason: "missing-hash" };
61
- const signed = {};
62
- for (const [key, value] of params) {
63
- if (key === "hash")
126
+ for (const raw of text.split(`
127
+ `)) {
128
+ if (raw.trim() === "")
64
129
  continue;
65
- signed[key] = value;
130
+ let line;
131
+ try {
132
+ line = JournalLineSchema.parse(JSON.parse(raw));
133
+ } catch {
134
+ continue;
135
+ }
136
+ if ("o" in line)
137
+ progress.set(line.i, line.o);
138
+ else if (line.s === "sending")
139
+ progress.set(line.i, "sending");
140
+ else
141
+ progress.delete(line.i);
66
142
  }
67
- const dataCheckString = Object.keys(signed).sort().map((key) => `${key}=${signed[key]}`).join(`
143
+ return progress;
144
+ }
145
+ function appendJournal(files, line) {
146
+ return appendFile(files.journal, `${JSON.stringify(line)}
68
147
  `);
69
- const secretKey = await hmacSha256(encoder.encode("WebAppData"), options.botToken);
70
- const expected = toHex(await hmacSha256(secretKey, dataCheckString));
71
- if (!digestsEqual(expected, hash.toLowerCase())) {
72
- return { valid: false, reason: "signature-mismatch" };
73
- }
74
- const authDateSeconds = Number(signed.auth_date);
75
- if (!Number.isSafeInteger(authDateSeconds) || authDateSeconds <= 0) {
76
- return { valid: false, reason: "malformed" };
77
- }
78
- const nowMs = options.now?.() ?? Date.now();
79
- const ageSeconds = Math.floor(nowMs / 1000) - authDateSeconds;
80
- if (options.maxAgeSeconds !== undefined && ageSeconds > options.maxAgeSeconds) {
81
- return { valid: false, reason: "expired", ageSeconds };
82
- }
83
- let user;
84
- let receiver;
148
+ }
149
+ function alive(pid) {
85
150
  try {
86
- user = optionalUser(signed, "user");
87
- receiver = optionalUser(signed, "receiver");
88
- } catch {
89
- return { valid: false, reason: "malformed" };
151
+ process.kill(pid, 0);
152
+ return true;
153
+ } catch (error) {
154
+ return error instanceof Error && "code" in error && error.code === "EPERM";
90
155
  }
91
- return {
92
- valid: true,
93
- data: {
94
- ...user !== undefined && { user },
95
- ...receiver !== undefined && { receiver },
96
- authDate: new Date(authDateSeconds * 1000),
97
- ageSeconds,
98
- ...signed.query_id !== undefined && { queryId: signed.query_id },
99
- ...signed.start_param !== undefined && { startParam: signed.start_param },
100
- ...signed.chat_type !== undefined && { chatType: signed.chat_type },
101
- ...signed.chat_instance !== undefined && { chatInstance: signed.chat_instance },
102
- raw: Object.freeze({ ...signed })
156
+ }
157
+ async function lockBroadcast(directory, files) {
158
+ await mkdir(directory, { recursive: true });
159
+ for (let attempt = 0;attempt < 2; attempt += 1) {
160
+ try {
161
+ const handle = await open(files.lock, "wx");
162
+ await handle.writeFile(String(process.pid));
163
+ await handle.close();
164
+ return () => rm(files.lock, { force: true });
165
+ } catch (error) {
166
+ if (!(error instanceof Error && ("code" in error) && error.code === "EEXIST"))
167
+ throw error;
168
+ const owner = Number.parseInt(await readFile(files.lock, "utf8").catch(() => ""), 10);
169
+ if (Number.isInteger(owner) && owner > 0 && alive(owner)) {
170
+ throw new Error("Telegram broadcast is already running in another process");
171
+ }
172
+ await rm(files.lock, { force: true });
103
173
  }
104
- };
174
+ }
175
+ throw new Error("Telegram broadcast lock could not be taken");
105
176
  }
177
+
106
178
  // src/telegram/send-failure.ts
107
179
  var BY_DESCRIPTION = [
108
180
  [/bot was blocked by the user|bot was kicked/i, "blocked-by-user"],
@@ -218,7 +290,527 @@ function classifyTelegramSendFailure(error) {
218
290
  evidence: "none"
219
291
  };
220
292
  }
293
+
294
+ // src/telegram/broadcast.ts
295
+ function pause(milliseconds) {
296
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
297
+ }
298
+ function tally(name, total, progress, outcome, halt) {
299
+ const counts = { delivered: 0, unreachable: 0, failed: 0, uncertain: 0 };
300
+ for (const state of progress.values())
301
+ if (state !== "sending")
302
+ counts[state] += 1;
303
+ return {
304
+ name,
305
+ outcome,
306
+ total,
307
+ ...counts,
308
+ pending: total - progress.size,
309
+ ...halt && { halt }
310
+ };
311
+ }
312
+ async function audience(config) {
313
+ const seen = new Set;
314
+ const list = [];
315
+ for await (const recipient of await config.recipients()) {
316
+ const key = `${typeof recipient}:${recipient}`;
317
+ if (seen.has(key))
318
+ continue;
319
+ seen.add(key);
320
+ list.push(recipient);
321
+ }
322
+ return list;
323
+ }
324
+ async function runTelegramBroadcast(config) {
325
+ assertBroadcastName(config.name);
326
+ const files = broadcastFiles(config.directory, config.name);
327
+ const intervalMs = 1000 / (config.ratePerSecond ?? 25);
328
+ const maxAttempts = config.maxAttempts ?? 5;
329
+ const sleep = config.sleep ?? pause;
330
+ const now = config.now ?? Date.now;
331
+ let recipients = [];
332
+ let progress = new Map;
333
+ const report = (outcome, halt) => tally(config.name, recipients.length, progress, outcome, halt);
334
+ if (config.dryRun) {
335
+ recipients = await readRecipients(files) ?? await audience(config);
336
+ progress = await readProgress(files);
337
+ return report("dry-run");
338
+ }
339
+ const unlock = await lockBroadcast(config.directory, files);
340
+ try {
341
+ const snapshot = await readRecipients(files);
342
+ if (snapshot)
343
+ recipients = snapshot;
344
+ else {
345
+ recipients = await audience(config);
346
+ await writeRecipients(files, recipients);
347
+ }
348
+ progress = await readProgress(files);
349
+ for (const [index, state] of progress) {
350
+ if (state !== "sending")
351
+ continue;
352
+ await appendJournal(files, { i: index, o: "uncertain" });
353
+ progress.set(index, "uncertain");
354
+ }
355
+ const emit = (value) => {
356
+ try {
357
+ config.onProgress?.(value);
358
+ } catch {}
359
+ return value;
360
+ };
361
+ emit(report("running"));
362
+ let lastProgressAt = now();
363
+ let lastSendAt = Number.NEGATIVE_INFINITY;
364
+ let halted;
365
+ const halt = (failure) => {
366
+ halted = failure;
367
+ return "halt";
368
+ };
369
+ const deliver = async (recipient) => {
370
+ for (let attempt = 1;; attempt += 1) {
371
+ const wait = lastSendAt + intervalMs - now();
372
+ if (wait > 0)
373
+ await sleep(wait);
374
+ lastSendAt = now();
375
+ try {
376
+ await config.send({ recipient, attempt });
377
+ return { outcome: "delivered" };
378
+ } catch (error) {
379
+ const failure = classifyTelegramSendFailure(error);
380
+ if (failure.recipientUnreachable)
381
+ return { outcome: "unreachable", reason: failure.reason };
382
+ if (failure.reason === "message-invalid")
383
+ return halt(failure);
384
+ const transport = failure.reason === "unknown" && failure.status === undefined;
385
+ if (!failure.retryable && !transport)
386
+ return { outcome: "failed", reason: failure.reason };
387
+ if (attempt >= maxAttempts) {
388
+ return transport ? halt(failure) : { outcome: "failed", reason: failure.reason };
389
+ }
390
+ await sleep(failure.retryAfterSeconds !== undefined ? failure.retryAfterSeconds * 1000 : Math.min(60000, 1000 * 2 ** (attempt - 1)));
391
+ }
392
+ }
393
+ };
394
+ for (let index = 0;index < recipients.length; index += 1) {
395
+ if (progress.has(index))
396
+ continue;
397
+ if (config.signal?.aborted)
398
+ return emit(report("stopped"));
399
+ const recipient = recipients[index];
400
+ if (recipient === undefined)
401
+ continue;
402
+ await appendJournal(files, { i: index, s: "sending" });
403
+ const result = await deliver(recipient);
404
+ if (result === "halt") {
405
+ await appendJournal(files, { i: index, s: "released" });
406
+ return emit(report("halted", halted));
407
+ }
408
+ await appendJournal(files, {
409
+ i: index,
410
+ o: result.outcome,
411
+ ...result.reason && { r: result.reason }
412
+ });
413
+ progress.set(index, result.outcome);
414
+ if (now() - lastProgressAt >= (config.progressEveryMs ?? 1e4)) {
415
+ lastProgressAt = now();
416
+ emit(report("running"));
417
+ }
418
+ }
419
+ return emit(report("finished"));
420
+ } finally {
421
+ await unlock();
422
+ }
423
+ }
424
+ function telegramBroadcastSender(config) {
425
+ const message = config.message;
426
+ return ({ recipient }) => callTelegramBotApi({
427
+ token: config.token,
428
+ ..."copyFrom" in message ? {
429
+ method: "copyMessage",
430
+ params: {
431
+ chat_id: recipient,
432
+ from_chat_id: message.copyFrom.chatId,
433
+ message_id: message.copyFrom.messageId
434
+ }
435
+ } : {
436
+ method: "sendMessage",
437
+ params: {
438
+ chat_id: recipient,
439
+ text: message.text,
440
+ ...message.parseMode && { parse_mode: message.parseMode }
441
+ }
442
+ },
443
+ ...config.apiRoot && { apiRoot: config.apiRoot },
444
+ ...config.fetch && { fetch: config.fetch }
445
+ });
446
+ }
447
+ // src/telegram/init-data.ts
448
+ import { z as z2 } from "zod";
449
+ var TelegramInitDataUserSchema = z2.object({
450
+ id: z2.int(),
451
+ is_bot: z2.boolean().optional(),
452
+ first_name: z2.string(),
453
+ last_name: z2.string().optional(),
454
+ username: z2.string().optional(),
455
+ language_code: z2.string().optional(),
456
+ is_premium: z2.boolean().optional(),
457
+ allows_write_to_pm: z2.boolean().optional(),
458
+ photo_url: z2.string().optional()
459
+ }).transform((user) => ({
460
+ id: user.id,
461
+ firstName: user.first_name,
462
+ ...user.is_bot !== undefined && { isBot: user.is_bot },
463
+ ...user.last_name !== undefined && { lastName: user.last_name },
464
+ ...user.username !== undefined && { username: user.username },
465
+ ...user.language_code !== undefined && { languageCode: user.language_code },
466
+ ...user.is_premium !== undefined && { isPremium: user.is_premium },
467
+ ...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
468
+ ...user.photo_url !== undefined && { photoUrl: user.photo_url }
469
+ }));
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
+ function optionalUser(raw, key) {
488
+ const encoded = raw[key];
489
+ if (encoded === undefined)
490
+ return;
491
+ return TelegramInitDataUserSchema.parse(JSON.parse(encoded));
492
+ }
493
+ async function verifyTelegramInitData(options) {
494
+ if (!options.botToken)
495
+ throw new TypeError("botToken is required to verify initData");
496
+ if (options.maxAgeSeconds !== undefined && (!Number.isFinite(options.maxAgeSeconds) || options.maxAgeSeconds < 0)) {
497
+ throw new TypeError("maxAgeSeconds must be a non-negative finite number");
498
+ }
499
+ const params = new URLSearchParams(options.initData);
500
+ const hash = params.get("hash");
501
+ if (!hash)
502
+ return { valid: false, reason: "missing-hash" };
503
+ const signed = {};
504
+ for (const [key, value] of params) {
505
+ if (key === "hash")
506
+ continue;
507
+ signed[key] = value;
508
+ }
509
+ const dataCheckString = Object.keys(signed).sort().map((key) => `${key}=${signed[key]}`).join(`
510
+ `);
511
+ const secretKey = await hmacSha256(encoder.encode("WebAppData"), options.botToken);
512
+ const expected = toHex(await hmacSha256(secretKey, dataCheckString));
513
+ if (!digestsEqual(expected, hash.toLowerCase())) {
514
+ return { valid: false, reason: "signature-mismatch" };
515
+ }
516
+ const authDateSeconds = Number(signed.auth_date);
517
+ if (!Number.isSafeInteger(authDateSeconds) || authDateSeconds <= 0) {
518
+ return { valid: false, reason: "malformed" };
519
+ }
520
+ const nowMs = options.now?.() ?? Date.now();
521
+ const ageSeconds = Math.floor(nowMs / 1000) - authDateSeconds;
522
+ if (options.maxAgeSeconds !== undefined && ageSeconds > options.maxAgeSeconds) {
523
+ return { valid: false, reason: "expired", ageSeconds };
524
+ }
525
+ let user;
526
+ let receiver;
527
+ try {
528
+ user = optionalUser(signed, "user");
529
+ receiver = optionalUser(signed, "receiver");
530
+ } catch {
531
+ return { valid: false, reason: "malformed" };
532
+ }
533
+ return {
534
+ valid: true,
535
+ data: {
536
+ ...user !== undefined && { user },
537
+ ...receiver !== undefined && { receiver },
538
+ authDate: new Date(authDateSeconds * 1000),
539
+ ageSeconds,
540
+ ...signed.query_id !== undefined && { queryId: signed.query_id },
541
+ ...signed.start_param !== undefined && { startParam: signed.start_param },
542
+ ...signed.chat_type !== undefined && { chatType: signed.chat_type },
543
+ ...signed.chat_instance !== undefined && { chatInstance: signed.chat_instance },
544
+ raw: Object.freeze({ ...signed })
545
+ }
546
+ };
547
+ }
548
+ // src/telegram/local-files.ts
549
+ import { constants } from "node:fs";
550
+ import { access, lstat, realpath, stat, unlink } from "node:fs/promises";
551
+ import { isAbsolute, relative, resolve, sep } from "node:path";
552
+
553
+ class TelegramLocalFileError extends Error {
554
+ reason;
555
+ constructor(reason) {
556
+ super(`Telegram local file refused: ${reason}`);
557
+ this.name = "TelegramLocalFileError";
558
+ this.reason = reason;
559
+ }
560
+ }
561
+ var absent2 = (error) => error instanceof Error && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
562
+ function inside(directory, candidate) {
563
+ const path = relative(directory, candidate);
564
+ return path !== "" && !path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path);
565
+ }
566
+ function createTelegramLocalFiles(config) {
567
+ const rootIsAbsolute = isAbsolute(config.root);
568
+ const botDirectory = resolve(config.root, config.token);
569
+ const realBotDirectory = async () => {
570
+ if (!rootIsAbsolute)
571
+ throw new TelegramLocalFileError("root-not-absolute");
572
+ try {
573
+ return await realpath(botDirectory);
574
+ } catch (error) {
575
+ if (absent2(error))
576
+ throw new TelegramLocalFileError("bot-directory-unavailable");
577
+ throw error;
578
+ }
579
+ };
580
+ const locate = async (filePath) => {
581
+ const directory = await realBotDirectory();
582
+ const lexical = isAbsolute(filePath) ? resolve(filePath) : resolve(botDirectory, filePath);
583
+ if (!inside(botDirectory, lexical) && !inside(directory, lexical)) {
584
+ throw new TelegramLocalFileError("outside-bot-directory");
585
+ }
586
+ let real;
587
+ try {
588
+ real = await realpath(lexical);
589
+ } catch (error) {
590
+ if (absent2(error))
591
+ throw new TelegramLocalFileError("missing");
592
+ throw error;
593
+ }
594
+ if (!inside(directory, real))
595
+ throw new TelegramLocalFileError("outside-bot-directory");
596
+ return real;
597
+ };
598
+ return {
599
+ async resolve(filePath) {
600
+ const path = await locate(filePath);
601
+ const info = await stat(path);
602
+ if (!info.isFile())
603
+ throw new TelegramLocalFileError("not-a-file");
604
+ return path;
605
+ },
606
+ async remove(filePath) {
607
+ let path;
608
+ try {
609
+ path = await locate(filePath);
610
+ } catch (error) {
611
+ if (error instanceof TelegramLocalFileError && error.reason === "missing")
612
+ return;
613
+ throw error;
614
+ }
615
+ if (!(await lstat(path)).isFile())
616
+ throw new TelegramLocalFileError("not-a-file");
617
+ try {
618
+ await unlink(path);
619
+ } catch (error) {
620
+ if (!absent2(error))
621
+ throw error;
622
+ }
623
+ },
624
+ async check() {
625
+ try {
626
+ const directory = await realBotDirectory();
627
+ await access(directory, constants.R_OK | constants.W_OK | constants.X_OK);
628
+ return { ready: true };
629
+ } catch (error) {
630
+ if (error instanceof TelegramLocalFileError)
631
+ return { ready: false, reason: error.reason };
632
+ return { ready: false, reason: "bot-directory-unavailable" };
633
+ }
634
+ }
635
+ };
636
+ }
637
+ // src/telegram/operator-channel.ts
638
+ var TEXT_LIMIT = 4096;
639
+ function timer(milliseconds, signal) {
640
+ return new Promise((resolve2, reject) => {
641
+ if (signal.aborted)
642
+ return reject(signal.reason);
643
+ const handle = setTimeout(() => {
644
+ signal.removeEventListener("abort", stop);
645
+ resolve2();
646
+ }, milliseconds);
647
+ const stop = () => {
648
+ clearTimeout(handle);
649
+ reject(signal.reason);
650
+ };
651
+ signal.addEventListener("abort", stop, { once: true });
652
+ });
653
+ }
654
+ function fitted(text) {
655
+ const points = Array.from(text);
656
+ return points.length <= TEXT_LIMIT ? text : `${points.slice(0, TEXT_LIMIT - 1).join("")}…`;
657
+ }
658
+ function createTelegramOperatorChannel(config) {
659
+ const maxQueued = config.maxQueued ?? 200;
660
+ const minIntervalMs = config.minIntervalMs ?? 3000;
661
+ const maxAttempts = config.maxAttempts ?? 3;
662
+ const sleep = config.sleep ?? timer;
663
+ const queue = [];
664
+ const idle = new Set;
665
+ let lifetime = new AbortController;
666
+ let running = false;
667
+ let closed = false;
668
+ let inFlight = 0;
669
+ const drop = (item, reason, failure) => {
670
+ try {
671
+ config.onDropped?.({
672
+ ...item.topic !== undefined && { topic: item.topic },
673
+ text: item.text,
674
+ reason,
675
+ ...failure && { failure }
676
+ });
677
+ } catch {}
678
+ };
679
+ const settleIdle = () => {
680
+ if (queue.length > 0 || inFlight > 0)
681
+ return;
682
+ for (const waiter of idle)
683
+ waiter();
684
+ idle.clear();
685
+ };
686
+ const attempt = async (item, signal) => {
687
+ item.attempts += 1;
688
+ const threadId = item.topic === undefined ? undefined : config.topics?.[item.topic];
689
+ try {
690
+ await config.send({
691
+ chatId: config.chatId,
692
+ ...threadId !== undefined && { threadId },
693
+ text: fitted(item.text)
694
+ }, signal);
695
+ return;
696
+ } catch (error) {
697
+ if (signal.aborted)
698
+ throw error;
699
+ const failure = classifyTelegramSendFailure(error);
700
+ if (!failure.retryable) {
701
+ drop(item, "refused", failure);
702
+ return;
703
+ }
704
+ if (item.attempts >= maxAttempts) {
705
+ drop(item, "attempts", failure);
706
+ return;
707
+ }
708
+ return Math.max(minIntervalMs, (failure.retryAfterSeconds ?? 0) * 1000);
709
+ }
710
+ };
711
+ const run = async (signal) => {
712
+ let current;
713
+ try {
714
+ for (let item = queue[0];item; item = queue[0]) {
715
+ current = item;
716
+ inFlight = 1;
717
+ queue.shift();
718
+ let retryIn = await attempt(item, signal);
719
+ while (retryIn !== undefined) {
720
+ await sleep(retryIn, signal);
721
+ retryIn = await attempt(item, signal);
722
+ }
723
+ current = undefined;
724
+ inFlight = 0;
725
+ settleIdle();
726
+ if (queue.length > 0)
727
+ await sleep(minIntervalMs, signal);
728
+ }
729
+ } catch {
730
+ if (current)
731
+ drop(current, "closed");
732
+ } finally {
733
+ inFlight = 0;
734
+ running = false;
735
+ settleIdle();
736
+ }
737
+ };
738
+ return {
739
+ post(text, topic) {
740
+ try {
741
+ const item = {
742
+ ...topic !== undefined && { topic },
743
+ text,
744
+ attempts: 0
745
+ };
746
+ if (closed) {
747
+ drop(item, "closed");
748
+ return;
749
+ }
750
+ queue.push(item);
751
+ while (queue.length > maxQueued) {
752
+ const oldest = queue.shift();
753
+ if (oldest)
754
+ drop(oldest, "overflow");
755
+ }
756
+ if (!running) {
757
+ running = true;
758
+ run(lifetime.signal);
759
+ }
760
+ } catch {}
761
+ },
762
+ get pending() {
763
+ return queue.length + inFlight;
764
+ },
765
+ drain(signal) {
766
+ if (queue.length === 0 && inFlight === 0)
767
+ return Promise.resolve();
768
+ return new Promise((resolve2) => {
769
+ const done = () => {
770
+ signal?.removeEventListener("abort", done);
771
+ idle.delete(done);
772
+ resolve2();
773
+ };
774
+ idle.add(done);
775
+ signal?.addEventListener("abort", done, { once: true });
776
+ });
777
+ },
778
+ close() {
779
+ closed = true;
780
+ lifetime.abort(new DOMException("Operator channel closed", "AbortError"));
781
+ lifetime = new AbortController;
782
+ for (const item of queue.splice(0))
783
+ drop(item, "closed");
784
+ settleIdle();
785
+ }
786
+ };
787
+ }
788
+ function telegramOperatorSender(config) {
789
+ return (message, signal) => callTelegramBotApi({
790
+ token: config.token,
791
+ method: "sendMessage",
792
+ params: {
793
+ chat_id: message.chatId,
794
+ text: message.text,
795
+ ...message.threadId !== undefined && { message_thread_id: message.threadId },
796
+ ...config.parseMode && { parse_mode: config.parseMode },
797
+ link_preview_options: { is_disabled: true }
798
+ },
799
+ signal,
800
+ ...config.apiRoot && { apiRoot: config.apiRoot },
801
+ ...config.fetch && { fetch: config.fetch }
802
+ });
803
+ }
221
804
  export {
222
805
  verifyTelegramInitData,
223
- classifyTelegramSendFailure
806
+ telegramOperatorSender,
807
+ telegramBroadcastSender,
808
+ runTelegramBroadcast,
809
+ createTelegramOperatorChannel,
810
+ createTelegramLocalFiles,
811
+ classifyTelegramSendFailure,
812
+ callTelegramBotApi,
813
+ TelegramLocalFileError,
814
+ TelegramBotApiError,
815
+ TELEGRAM_BOT_TOKEN_PATTERN
224
816
  };