stitchkit 0.100.3 → 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 (41) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/entrypoints/application.js +2 -1
  3. package/dist/entrypoints/observability.js +2 -1
  4. package/dist/entrypoints/telegram/screens.js +1 -1
  5. package/dist/entrypoints/telegram.d.ts +8 -4
  6. package/dist/entrypoints/telegram.d.ts.map +1 -1
  7. package/dist/entrypoints/telegram.js +277 -49
  8. package/dist/entrypoints/tools.js +2 -1
  9. package/dist/{index-6k1svmjh.js → index-e05cjgtf.js} +16 -13
  10. package/dist/{index-kzz6rq0w.js → index-e6c2spsd.js} +56 -18
  11. package/dist/index-sznqf8fx.js +20 -0
  12. package/dist/internal/secret-shapes.d.ts +21 -0
  13. package/dist/internal/secret-shapes.d.ts.map +1 -0
  14. package/dist/observability/sanitize.d.ts.map +1 -1
  15. package/dist/telegram/bot-api.d.ts +7 -4
  16. package/dist/telegram/bot-api.d.ts.map +1 -1
  17. package/dist/telegram/init-data.d.ts +3 -44
  18. package/dist/telegram/init-data.d.ts.map +1 -1
  19. package/dist/telegram/operator-channel.d.ts +1 -18
  20. package/dist/telegram/operator-channel.d.ts.map +1 -1
  21. package/dist/telegram/operator-dedupe.d.ts +44 -0
  22. package/dist/telegram/operator-dedupe.d.ts.map +1 -0
  23. package/dist/telegram/send-failure.d.ts +6 -0
  24. package/dist/telegram/send-failure.d.ts.map +1 -1
  25. package/dist/telegram/update-intake.d.ts +5 -1
  26. package/dist/telegram/update-intake.d.ts.map +1 -1
  27. package/dist/telegram/update-store-check.d.ts +22 -0
  28. package/dist/telegram/update-store-check.d.ts.map +1 -0
  29. package/dist/telegram/update-store-postgres.d.ts +33 -0
  30. package/dist/telegram/update-store-postgres.d.ts.map +1 -0
  31. package/dist/telegram/user.d.ts +51 -0
  32. package/dist/telegram/user.d.ts.map +1 -0
  33. package/llms/observability.txt +13 -1
  34. package/llms/server.4.txt +16 -0
  35. package/llms/stitchkit.txt +3 -3
  36. package/llms/telegram.txt +91 -13
  37. package/llms/{upgrading-0.74.0-0.100.0.txt → upgrading-0.74.0-0.101.0.txt} +39 -3
  38. package/llms/upgrading.txt +1 -1
  39. package/llms-full.txt +159 -17
  40. package/llms.txt +4 -4
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -15,6 +15,73 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [0.101.0] — 2026-09-29
19
+
20
+ ### ⚠️ Breaking changes
21
+
22
+ - **`stitchkit/telegram`** — `TelegramInitDataUser` is `TelegramUser`. The same
23
+ camelCase record now also comes out of `parseTelegramUser(ctx.from)`, so its
24
+ name says any Telegram user. `import type { TelegramInitDataUser }` →
25
+ `import type { TelegramUser }`.
26
+ - **`stitchkit/telegram`** — `TELEGRAM_BOT_TOKEN_PATTERN` matches the secret
27
+ half of a token only, after `<bot id>:`, so masking with it keeps the bot's id
28
+ (`123456:[redacted]`) — the only thing in a masked line that says which bot
29
+ failed. Masking no longer needs it at all: `redact` masks bot tokens by shape.
30
+ `sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN]` → nothing;
31
+ `text.replace(new RegExp(TELEGRAM_BOT_TOKEN_PATTERN.source, 'g'), …)` →
32
+ `redactTelegramBotToken(text)`. See ADR 0214 and the 0.101.0 migration in
33
+ `docs/guide/upgrading.md`.
34
+
35
+ **Who must act:** code that names the type `TelegramInitDataUser` (rename the
36
+ import), and code that uses `TELEGRAM_BOT_TOKEN_PATTERN` to find or cut a whole
37
+ token. Code that only passes the pattern to a logger keeps working — and can
38
+ drop it, since tokens are masked by shape.
39
+
40
+ ### Added
41
+
42
+ - `stitchkit/telegram` — `postgresTelegramUpdateStore`: the webhook update store
43
+ in Postgres over one `query(text, parameters)` the application already has —
44
+ Bun `sql.unsafe`, Prisma `$queryRawUnsafe`, `pg` `pool.query` — each rule one
45
+ conditional statement with explicit casts, so a claim is atomic across
46
+ processes and a driver returning `bigint` as a string or a `bigint` reads the
47
+ same. `postgresTelegramUpdateStoreSchema(table)` is the table for a migration;
48
+ `createTable: false` leaves the schema to it.
49
+ - `stitchkit/telegram` — `checkTelegramUpdateStore(make)`: every rule of
50
+ `TelegramUpdateStore` on a fresh store, eight claims raced against each other,
51
+ resolving with the violations. A store of one's own over an ORM is checked by
52
+ the rules the shipped memory, SQLite and Postgres stores pass.
53
+ - `stitchkit/telegram` — `createTelegramOperatorDedupe`: the operator channel's
54
+ dedupe window alone, for a bot that sends alerts itself — `(text, topic?)` is
55
+ `{ send: true, text }` with the held-back counts, or `{ send: false, reason }`.
56
+ - `stitchkit/telegram` — `parseTelegramUser`: grammY's `ctx.from`, a Bot API
57
+ `User` or a Mini App `user` as the one `TelegramUser` initData returns.
58
+ - `stitchkit/telegram` — `TELEGRAM_SEND_FAILURE_REASONS`, every reason as a
59
+ list; the reasons' traits are one table, so a new reason without its traits
60
+ does not compile.
61
+ - `stitchkit/telegram` — `redactTelegramBotToken(text)`.
62
+
63
+ ### Changed
64
+
65
+ - `stitchkit/observability` — `redact`, and so every logger and audit sink,
66
+ masks secrets by shape in every string: the secret half of a bot token, the
67
+ password in `scheme://user:password@host`, and the value of a query parameter
68
+ named as a secret (`token`, `access_token`, `api-key`, `key`, `sig`, …). A URL
69
+ in an error message is the usual way a token reaches a journal, and no key
70
+ name marks it.
71
+ - `stitchkit/observability` — an error keeps its own fields: a
72
+ `TelegramBotApiError`'s `error_code` and `parameters`, a provider's `status`,
73
+ written beside `name`, `message`, `stack` and `cause` and masked by the same
74
+ rules. A field holding a class instance other than an error — grammY's
75
+ `BotError.ctx` — is written as its class name, so one error cannot push its
76
+ line past the bound and lose the stack.
77
+
78
+ ### Fixed
79
+
80
+ - `stitchkit/telegram` — the update intake unwraps grammY's `BotError`: a
81
+ `retry`, an `onFailure` and the error stored with the update saw the wrapper
82
+ ("Error in middleware: …") instead of what the middleware threw, so a retry
83
+ policy could not recognise the bot's own terminal error and retried it.
84
+
18
85
  ## [0.100.3] — 2026-09-29
19
86
 
20
87
  ### Added
@@ -73,9 +73,10 @@ import {
73
73
  ApplicationAdmissionError2,
74
74
  createApplication2
75
75
  } from "../index-w575evhq.js";
76
+ import"../index-sznqf8fx.js";
76
77
  import {
77
78
  measureSize2
78
- } from "../index-kzz6rq0w.js";
79
+ } from "../index-e6c2spsd.js";
79
80
  import"../index-rfkh44ss.js";
80
81
  import {
81
82
  diff,
@@ -19,12 +19,13 @@ import {
19
19
  childSpan2,
20
20
  resolvePropagationContext2
21
21
  } from "../index-hby4qhax.js";
22
+ import"../index-sznqf8fx.js";
22
23
  import {
23
24
  redact2,
24
25
  truncatePreview2,
25
26
  sanitizePayload2,
26
27
  measureSize2
27
- } from "../index-kzz6rq0w.js";
28
+ } from "../index-e6c2spsd.js";
28
29
  import {
29
30
  runWithRequestContext2,
30
31
  getRequestContext2,
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../index-m38ej7e5.js";
10
10
  import {
11
11
  classifyTelegramEditRefusal
12
- } from "../../index-6k1svmjh.js";
12
+ } from "../../index-e05cjgtf.js";
13
13
  import {
14
14
  checkTelegramHtml2
15
15
  } from "../../index-rz3z13e9.js";
@@ -1,11 +1,15 @@
1
- export { callTelegramBotApi, TELEGRAM_BOT_TOKEN_PATTERN, type TelegramBotApiCall, TelegramBotApiError, } from '../telegram/bot-api.js';
1
+ export { callTelegramBotApi, redactTelegramBotToken, TELEGRAM_BOT_TOKEN_PATTERN, type TelegramBotApiCall, TelegramBotApiError, } from '../telegram/bot-api.js';
2
2
  export { runTelegramBroadcast, type TelegramBroadcastConfig, type TelegramBroadcastMessage, type TelegramBroadcastOutcome, type TelegramBroadcastRecipient, type TelegramBroadcastReport, type TelegramBroadcastRunOutcome, type TelegramBroadcastSend, type TelegramBroadcastSenderConfig, telegramBroadcastSender, } from '../telegram/broadcast.js';
3
- export { type TelegramInitData, type TelegramInitDataRefusal, type TelegramInitDataUser, type TelegramInitDataVerification, type VerifyTelegramInitDataOptions, verifyTelegramInitData, } from '../telegram/init-data.js';
3
+ export { type TelegramInitData, type TelegramInitDataRefusal, type TelegramInitDataVerification, type VerifyTelegramInitDataOptions, verifyTelegramInitData, } from '../telegram/init-data.js';
4
4
  export { createTelegramLocalFiles, TelegramLocalFileError, type TelegramLocalFileRefusal, type TelegramLocalFiles, type TelegramLocalFilesCheck, type TelegramLocalFilesConfig, } from '../telegram/local-files.js';
5
- export { createTelegramOperatorChannel, type TelegramChatId, type TelegramOperatorChannel, type TelegramOperatorChannelConfig, type TelegramOperatorDedupe, type TelegramOperatorDrop, type TelegramOperatorMessage, type TelegramOperatorSenderConfig, telegramOperatorSender, } from '../telegram/operator-channel.js';
6
- export { classifyTelegramSendFailure, type TelegramSendFailure, type TelegramSendFailureReason, } from '../telegram/send-failure.js';
5
+ export { createTelegramOperatorChannel, type TelegramChatId, type TelegramOperatorChannel, type TelegramOperatorChannelConfig, type TelegramOperatorDrop, type TelegramOperatorMessage, type TelegramOperatorSenderConfig, telegramOperatorSender, } from '../telegram/operator-channel.js';
6
+ export { createTelegramOperatorDedupe, type TelegramOperatorDedupe, type TelegramOperatorDedupeVerdict, } from '../telegram/operator-dedupe.js';
7
+ export { classifyTelegramSendFailure, TELEGRAM_SEND_FAILURE_REASONS, type TelegramSendFailure, type TelegramSendFailureReason, } from '../telegram/send-failure.js';
7
8
  export { createTelegramUpdateIntake, type TelegramUpdateEnvelope, type TelegramUpdateFailure, type TelegramUpdateIntake, type TelegramUpdateIntakeConfig, type TelegramUpdateStoreStep, } from '../telegram/update-intake.js';
8
9
  export { memoryTelegramUpdateStore, type StoredTelegramUpdate, type TelegramUpdateClaimOptions, type TelegramUpdateDueQuery, type TelegramUpdateSettlement, type TelegramUpdateState, type TelegramUpdateStore, } from '../telegram/update-store.js';
10
+ export { checkTelegramUpdateStore } from '../telegram/update-store-check.js';
11
+ export { type PostgresTelegramUpdateStoreConfig, postgresTelegramUpdateStore, postgresTelegramUpdateStoreSchema, type TelegramPostgresQuery, } from '../telegram/update-store-postgres.js';
9
12
  export { type SqliteTelegramUpdateStoreConfig, sqliteTelegramUpdateStore, type TelegramSqliteDatabase, } from '../telegram/update-store-sqlite.js';
13
+ export { parseTelegramUser, type TelegramUser } from '../telegram/user.js';
10
14
  export { type ClaimTelegramWebhookConfig, checkTelegramWebhook, claimTelegramWebhook, type ReceiveTelegramWebhookOptions, receiveTelegramWebhook, TELEGRAM_WEBHOOK_NONE, type TelegramUpdateAcceptance, type TelegramWebhookClaim, TelegramWebhookClaimError, type TelegramWebhookConfig, type TelegramWebhookRefusal, type TelegramWebhookState, telegramWebhookUrl, } from '../telegram/webhook.js';
11
15
  //# sourceMappingURL=telegram.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/entrypoints/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,0BAA0B,EAC1B,KAAK,kBAAkB,EACvB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,0BAA0B,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,yBAAyB,EACzB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,+BAA+B,EACpC,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,KAAK,0BAA0B,EAC/B,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,6BAA6B,EAClC,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/entrypoints/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC1B,KAAK,kBAAkB,EACvB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,oBAAoB,EACpB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAClC,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,sBAAsB,GACvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,4BAA4B,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,GACnC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,2BAA2B,EAC3B,6BAA6B,EAC7B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,0BAA0B,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,GAC7B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,yBAAyB,EACzB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EACL,KAAK,iCAAiC,EACtC,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,qBAAqB,GAC3B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,KAAK,+BAA+B,EACpC,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EACL,KAAK,0BAA0B,EAC/B,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,6BAA6B,EAClC,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC"}
@@ -1,6 +1,10 @@
1
1
  import {
2
+ BOT_TOKEN_SECRET
3
+ } from "../index-sznqf8fx.js";
4
+ import {
5
+ TELEGRAM_SEND_FAILURE_REASONS2,
2
6
  classifyTelegramSendFailure2
3
- } from "../index-6k1svmjh.js";
7
+ } from "../index-e05cjgtf.js";
4
8
  import {
5
9
  TELEGRAM_TEXT_LIMIT2,
6
10
  truncateTelegramHtml2
@@ -11,7 +15,10 @@ import {
11
15
  } from "../index-9jfdm5tg.js";
12
16
 
13
17
  // src/telegram/bot-api.ts
14
- 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
+ }
15
22
  var DEFAULT_API_ROOT = "https://api.telegram.org";
16
23
 
17
24
  class TelegramBotApiError extends Error {
@@ -336,9 +343,6 @@ function telegramBroadcastSender(config) {
336
343
  ...config.fetch && { fetch: config.fetch }
337
344
  });
338
345
  }
339
- // src/telegram/init-data.ts
340
- import { z as z2 } from "zod";
341
-
342
346
  // src/telegram/crypto.ts
343
347
  var encoder = new TextEncoder;
344
348
  async function hmacSha256(key, message) {
@@ -358,9 +362,9 @@ function digestsEqual(a, b) {
358
362
  return difference === 0;
359
363
  }
360
364
 
361
- // src/telegram/init-data.ts
362
- var encoder2 = new TextEncoder;
363
- var TelegramInitDataUserSchema = z2.object({
365
+ // src/telegram/user.ts
366
+ import { z as z2 } from "zod";
367
+ var TelegramUserSchema = z2.object({
364
368
  id: z2.int(),
365
369
  is_bot: z2.boolean().optional(),
366
370
  first_name: z2.string(),
@@ -381,11 +385,21 @@ var TelegramInitDataUserSchema = z2.object({
381
385
  ...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
382
386
  ...user.photo_url !== undefined && { photoUrl: user.photo_url }
383
387
  }));
388
+ function parseTelegramUser(value) {
389
+ const parsed = TelegramUserSchema.safeParse(value);
390
+ return parsed.success ? parsed.data : undefined;
391
+ }
392
+
393
+ // src/telegram/init-data.ts
394
+ var encoder2 = new TextEncoder;
384
395
  function optionalUser(raw, key) {
385
396
  const encoded = raw[key];
386
397
  if (encoded === undefined)
387
398
  return;
388
- 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;
389
403
  }
390
404
  async function verifyTelegramInitData(options) {
391
405
  if (!options.botToken)
@@ -531,42 +545,12 @@ function createTelegramLocalFiles(config) {
531
545
  }
532
546
  };
533
547
  }
534
- // src/telegram/operator-channel.ts
535
- var MASK = "[redacted]";
536
- function masked(text, patterns) {
537
- let output = text;
538
- for (const pattern of patterns) {
539
- output = output.replace(new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`), MASK);
540
- }
541
- return output;
542
- }
543
- function literal(value) {
544
- return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
545
- }
546
- function timer(milliseconds, signal) {
547
- return new Promise((resolve, reject) => {
548
- if (signal.aborted)
549
- return reject(signal.reason);
550
- const handle = setTimeout(() => {
551
- signal.removeEventListener("abort", stop);
552
- resolve();
553
- }, milliseconds);
554
- const stop = () => {
555
- clearTimeout(handle);
556
- reject(signal.reason);
557
- };
558
- signal.addEventListener("abort", stop, { once: true });
559
- });
560
- }
561
- function fitted(text) {
562
- const points = Array.from(text);
563
- return points.length <= TELEGRAM_TEXT_LIMIT2 ? text : `${points.slice(0, TELEGRAM_TEXT_LIMIT2 - 1).join("")}…`;
564
- }
548
+ // src/telegram/operator-dedupe.ts
565
549
  function defaultFingerprint(text, topic) {
566
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, "#");
567
551
  return `${topic ?? ""}\x00${blanked.slice(0, 300)}`;
568
552
  }
569
- function dedupeWindow(options) {
553
+ function createTelegramOperatorDedupe(options = {}) {
570
554
  const windowMs = options.windowMs ?? 600000;
571
555
  const maxPerWindow = options.maxPerWindow ?? 20;
572
556
  const fingerprint = options.fingerprint ?? defaultFingerprint;
@@ -610,6 +594,38 @@ function dedupeWindow(options) {
610
594
  `) };
611
595
  };
612
596
  }
597
+
598
+ // src/telegram/operator-channel.ts
599
+ var MASK = "[redacted]";
600
+ function masked(text, patterns) {
601
+ let output = text;
602
+ for (const pattern of patterns) {
603
+ output = output.replace(new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`), MASK);
604
+ }
605
+ return output;
606
+ }
607
+ function literal(value) {
608
+ return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
609
+ }
610
+ function timer(milliseconds, signal) {
611
+ return new Promise((resolve, reject) => {
612
+ if (signal.aborted)
613
+ return reject(signal.reason);
614
+ const handle = setTimeout(() => {
615
+ signal.removeEventListener("abort", stop);
616
+ resolve();
617
+ }, milliseconds);
618
+ const stop = () => {
619
+ clearTimeout(handle);
620
+ reject(signal.reason);
621
+ };
622
+ signal.addEventListener("abort", stop, { once: true });
623
+ });
624
+ }
625
+ function fitted(text) {
626
+ const points = Array.from(text);
627
+ return points.length <= TELEGRAM_TEXT_LIMIT2 ? text : `${points.slice(0, TELEGRAM_TEXT_LIMIT2 - 1).join("")}…`;
628
+ }
613
629
  function createTelegramOperatorChannel(config) {
614
630
  const maxQueued = config.maxQueued ?? 200;
615
631
  const minIntervalMs = config.minIntervalMs ?? 3000;
@@ -620,7 +636,7 @@ function createTelegramOperatorChannel(config) {
620
636
  ...config.sensitivePatterns ?? [],
621
637
  ...[...config.sensitiveValues ?? []].filter((value) => value !== "").sort((left, right) => right.length - left.length).map(literal)
622
638
  ];
623
- const admit = config.dedupe ? dedupeWindow(config.dedupe) : undefined;
639
+ const admit = config.dedupe ? createTelegramOperatorDedupe(config.dedupe) : undefined;
624
640
  const queue = [];
625
641
  const idle = new Set;
626
642
  let lifetime = new AbortController;
@@ -809,6 +825,9 @@ function parsed(body) {
809
825
  return;
810
826
  }
811
827
  }
828
+ function handlerError(error) {
829
+ return error instanceof Error && error.name === "BotError" && "ctx" in error && "error" in error ? error.error : error;
830
+ }
812
831
  function positive(name, value) {
813
832
  if (!Number.isSafeInteger(value) || value < 1) {
814
833
  throw new RangeError(`[stitchkit] telegram update intake: ${name} is a positive integer`);
@@ -862,7 +881,8 @@ function createTelegramUpdateIntake(config) {
862
881
  try {
863
882
  await config.handle(transportResult(update));
864
883
  settlement = { state: "completed", at: now() };
865
- } catch (error) {
884
+ } catch (thrown) {
885
+ const error = handlerError(thrown);
866
886
  const delay = number >= maxAttempts ? false : retry(error, number);
867
887
  const message = error instanceof Error ? error.message : String(error);
868
888
  settlement = delay === false ? { state: "abandoned", at: now(), error: message } : { state: "failed", at: now(), retryAt: now() + delay, error: message };
@@ -1022,9 +1042,210 @@ function memoryTelegramUpdateStore() {
1022
1042
  }
1023
1043
  };
1024
1044
  }
1025
- // src/telegram/update-store-sqlite.ts
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
1026
1139
  var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
1027
- var CLAIMABLE = `(state = 'pending' OR (state = 'failed' AND due_at <= ?1)
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)
1028
1249
  OR (state = 'processing' AND due_at < ?1))`;
1029
1250
  function rowOf(row) {
1030
1251
  if (typeof row !== "object" || row === null)
@@ -1035,7 +1256,7 @@ function rowOf(row) {
1035
1256
  }
1036
1257
  function sqliteTelegramUpdateStore(config) {
1037
1258
  const table = config.table ?? "telegram_updates";
1038
- if (!TABLE_NAME.test(table)) {
1259
+ if (!TABLE_NAME2.test(table)) {
1039
1260
  throw new TypeError("[stitchkit] telegram update store: a table name is [A-Za-z_][A-Za-z0-9_]*");
1040
1261
  }
1041
1262
  const database = config.database;
@@ -1053,14 +1274,14 @@ function sqliteTelegramUpdateStore(config) {
1053
1274
  const insert = database.prepare(`INSERT OR IGNORE INTO ${table} (update_id, body, state, attempts, due_at, received_at)
1054
1275
  VALUES (?1, ?2, 'pending', 0, ?3, ?3)`);
1055
1276
  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`);
1277
+ WHERE update_id = ?3 AND attempts < ?4 AND ${CLAIMABLE2} RETURNING attempts`);
1057
1278
  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}`);
1279
+ WHERE update_id = ?2 AND attempts >= ?3 AND ${CLAIMABLE2}`);
1059
1280
  const renew = database.prepare(`UPDATE ${table} SET due_at = ?1 WHERE update_id = ?2 AND attempts = ?3 AND state = 'processing'`);
1060
1281
  const settle = database.prepare(`UPDATE ${table} SET state = ?1, due_at = COALESCE(?2, due_at), settled_at = ?3, error = ?4
1061
1282
  WHERE update_id = ?5 AND attempts = ?6 AND state = 'processing'`);
1062
1283
  const due = database.prepare(`SELECT update_id, body FROM ${table}
1063
- WHERE (state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE})
1284
+ WHERE (state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE2})
1064
1285
  ORDER BY update_id LIMIT ?3`);
1065
1286
  const prune = database.prepare(`DELETE FROM ${table} WHERE state IN ('completed', 'abandoned') AND settled_at < ?1`);
1066
1287
  return {
@@ -1199,19 +1420,26 @@ async function receiveTelegramWebhook(request, options) {
1199
1420
  }
1200
1421
  export {
1201
1422
  TELEGRAM_BOT_TOKEN_PATTERN,
1423
+ TELEGRAM_SEND_FAILURE_REASONS2 as TELEGRAM_SEND_FAILURE_REASONS,
1202
1424
  TELEGRAM_WEBHOOK_NONE,
1203
1425
  TelegramBotApiError,
1204
1426
  TelegramLocalFileError,
1205
1427
  TelegramWebhookClaimError,
1206
1428
  callTelegramBotApi,
1429
+ checkTelegramUpdateStore,
1207
1430
  checkTelegramWebhook,
1208
1431
  claimTelegramWebhook,
1209
1432
  classifyTelegramSendFailure2 as classifyTelegramSendFailure,
1210
1433
  createTelegramLocalFiles,
1211
1434
  createTelegramOperatorChannel,
1435
+ createTelegramOperatorDedupe,
1212
1436
  createTelegramUpdateIntake,
1213
1437
  memoryTelegramUpdateStore,
1438
+ parseTelegramUser,
1439
+ postgresTelegramUpdateStore,
1440
+ postgresTelegramUpdateStoreSchema,
1214
1441
  receiveTelegramWebhook,
1442
+ redactTelegramBotToken,
1215
1443
  runTelegramBroadcast,
1216
1444
  sqliteTelegramUpdateStore,
1217
1445
  telegramBroadcastSender,
@@ -57,9 +57,10 @@ import {
57
57
  defineContract2
58
58
  } from "../index-ck71tbcf.js";
59
59
  import"../index-1bx1xv1k.js";
60
+ import"../index-sznqf8fx.js";
60
61
  import {
61
62
  redact2
62
- } from "../index-kzz6rq0w.js";
63
+ } from "../index-e6c2spsd.js";
63
64
  import {
64
65
  AsyncOperationCancelResultSchema2,
65
66
  createAsyncOperationSnapshotSchema2,
@@ -14,16 +14,20 @@ var BY_DESCRIPTION = [
14
14
  "message-invalid"
15
15
  ]
16
16
  ];
17
- var RETRYABLE = new Set([
18
- "rate-limited",
19
- "server-error"
20
- ]);
21
- var UNREACHABLE = new Set([
22
- "blocked-by-user",
23
- "user-deactivated",
24
- "chat-not-found",
25
- "not-started"
26
- ]);
17
+ var TRAITS = {
18
+ "blocked-by-user": { retryable: false, recipientUnreachable: true },
19
+ "user-deactivated": { retryable: false, recipientUnreachable: true },
20
+ "chat-not-found": { retryable: false, recipientUnreachable: true },
21
+ "not-started": { retryable: false, recipientUnreachable: true },
22
+ "rate-limited": { retryable: true, recipientUnreachable: false },
23
+ "message-invalid": { retryable: false, recipientUnreachable: false },
24
+ "server-error": { retryable: true, recipientUnreachable: false },
25
+ unknown: { retryable: false, recipientUnreachable: false }
26
+ };
27
+ function isSendFailureReason(value) {
28
+ return Object.hasOwn(TRAITS, value);
29
+ }
30
+ var TELEGRAM_SEND_FAILURE_REASONS2 = Object.keys(TRAITS).filter(isSendFailureReason);
27
31
  function numberAt(value, keys) {
28
32
  if (!isRecord(value))
29
33
  return;
@@ -85,8 +89,7 @@ function classifyTelegramSendFailure2(error) {
85
89
  return {
86
90
  reason,
87
91
  ...status !== undefined && { status },
88
- retryable: RETRYABLE.has(reason),
89
- recipientUnreachable: UNREACHABLE.has(reason),
92
+ ...TRAITS[reason],
90
93
  evidence: "description"
91
94
  };
92
95
  }
@@ -138,4 +141,4 @@ function classifyTelegramEditRefusal(error) {
138
141
  return "other";
139
142
  }
140
143
 
141
- export { classifyTelegramSendFailure2, classifyTelegramEditRefusal };
144
+ export { TELEGRAM_SEND_FAILURE_REASONS2, classifyTelegramSendFailure2, classifyTelegramEditRefusal };