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
@@ -79,12 +79,12 @@ own, recorded as an ADR.
79
79
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
80
80
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
81
81
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
82
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 45 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
82
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 46 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
83
83
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
84
84
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
85
85
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
86
86
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
87
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 45 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
87
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 46 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
88
88
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
89
89
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
90
90
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -94,7 +94,7 @@ own, recorded as an ADR.
94
94
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
95
95
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
96
96
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
97
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 45 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
97
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 46 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
98
98
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
99
99
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
100
100
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
package/llms/telegram.txt CHANGED
@@ -42,16 +42,21 @@ output, in pino's shape — numeric `level` (`debug` 20, `info` 30, `warn` 40,
42
42
  `error` 50), `time` in epoch milliseconds, `msg` — so `pino-pretty` reads it on
43
43
  a terminal and a supervisor matching `"level":50` sees errors. Unlike pino, an
44
44
  `Error` under *any* key is written with its name, message, stack and cause; an
45
- error logged as `{ error }` does not become `"error":{}`.
45
+ error logged as `{ error }` does not become `"error":{}`, and its own fields —
46
+ a `TelegramBotApiError`'s `error_code` and `parameters.retry_after` — are on the
47
+ line beside them.
48
+
49
+ A bot token is masked wherever it appears — a request URL in an error message,
50
+ a local Bot API file path — with no configuration: `123456:[redacted]` keeps the
51
+ bot's id, which is public and is what tells two bots apart in one journal. So is
52
+ the password in an address and a query parameter named as a secret (`?token=`).
46
53
 
47
54
  ```ts
48
55
  import { createJsonLogger } from 'stitchkit/observability'
49
- import { TELEGRAM_BOT_TOKEN_PATTERN } from 'stitchkit/telegram'
50
56
 
51
57
  export const logger = createJsonLogger({
52
58
  level: env.LOG_LEVEL,
53
59
  fields: { service: 'my-bot' },
54
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
55
60
  })
56
61
 
57
62
  logger.error('Application startup failed', { error })
@@ -67,11 +72,13 @@ query values inside any URL variable (`DATABASE_URL`), and each URL-encoded:
67
72
  import { createJsonLogger, secretValuesFromEnv } from 'stitchkit/observability'
68
73
 
69
74
  export const logger = createJsonLogger({
70
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
71
75
  sensitiveValues: secretValuesFromEnv(process.env),
72
76
  })
73
77
  ```
74
78
 
79
+ A string of your own — a message for a person, a report — masks bot tokens with
80
+ `redactTelegramBotToken(text)` from `stitchkit/telegram`.
81
+
75
82
  A key added to the environment is masked from its first line; a URL's user name
76
83
  is left alone, since it names a role that appears in ordinary text.
77
84
 
@@ -128,7 +135,21 @@ const operators = createTelegramOperatorChannel<'errors'>({
128
135
  })
129
136
  ```
130
137
 
131
- Held-back messages reach `onDropped` as `repeated` or `over-budget`. What to post stays the bot's. On the way down, drain it within the grace period
138
+ Held-back messages reach `onDropped` as `repeated` or `over-budget`.
139
+
140
+ A bot that already sends its alerts itself — its own bot instance, its own retry
141
+ — takes the window alone instead of switching the channel off with parameters:
142
+
143
+ ```ts
144
+ import { createTelegramOperatorDedupe } from 'stitchkit/telegram'
145
+
146
+ const admit = createTelegramOperatorDedupe<'errors'>({ windowMs: 10 * 60_000 })
147
+
148
+ const verdict = admit(text, 'errors')
149
+ if (verdict.send) await sendAlert(verdict.text) // the text carries the held-back counts
150
+ ```
151
+
152
+ What to post stays the bot's. On the way down, drain it within the grace period
132
153
  and close it: `drain: (context) => operators.drain(context.signal)`,
133
154
  `close: () => operators.close()` in a resource.
134
155
 
@@ -249,6 +270,14 @@ itself be a secret — and says what to pass: set the takeover for one start and
249
270
  remove it. `checkTelegramWebhook` on a timer notices a webhook that moved
250
271
  elsewhere, which this process cannot see any other way.
251
272
 
273
+ **Moving to it from a claim of your own.** A bot that tagged its webhook itself
274
+ holds an address this one does not recognise, so the first claim is refused —
275
+ `other-secret` when the address is the same, `owned-elsewhere` otherwise — and
276
+ names its own host. Start once with `takeoverFrom` set to that host, then remove
277
+ it. The tag is derived from the secret with a fixed context, so it is the same
278
+ across stitchkit releases: that takeover is needed once per bot, not per
279
+ upgrade. A secret rotation is a new owner, and takes a takeover too.
280
+
252
281
  **The update is recorded before Telegram is answered.** A handler that runs
253
282
  longer than Telegram waits — a large file from a local Bot API server — gets the
254
283
  same update again while it still works, and a process that answers and then
@@ -263,10 +292,55 @@ fix (a refused message, a blocked user) is abandoned; `maxAttempts` (5) ends
263
292
  the rest. Delivery is at least once: a handler that finished but could not be
264
293
  recorded as finished runs again.
265
294
 
295
+ With grammY the handler is `bot.handleUpdate`, which wraps a middleware's error
296
+ in a `BotError`; the intake unwraps it, so `retry`, `onFailure` and the error
297
+ stored with the update are what the middleware threw — a `retry` that returns
298
+ `false` for the bot's own terminal error sees that error.
299
+
266
300
  The store is an interface of six atomic steps (`TelegramUpdateStore`), so the
267
- bot's own database can hold it; `sqliteTelegramUpdateStore` takes a
268
- `bun:sqlite` or `node:sqlite` handle as it is, and `memoryTelegramUpdateStore`
269
- is for tests.
301
+ bot's own database holds it:
302
+
303
+ | Store | Takes |
304
+ |---|---|
305
+ | `sqliteTelegramUpdateStore` | a `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync` as it is |
306
+ | `postgresTelegramUpdateStore` | one `query(text, parameters)` over the client the bot already holds |
307
+ | `memoryTelegramUpdateStore` | nothing — tests, or a bot that accepts losing unhandled updates to a restart |
308
+
309
+ ```ts
310
+ import { postgresTelegramUpdateStore } from 'stitchkit/telegram'
311
+
312
+ // Bun
313
+ postgresTelegramUpdateStore({ query: (text, parameters) => sql.unsafe(text, [...parameters]) })
314
+ // Prisma
315
+ postgresTelegramUpdateStore({
316
+ query: (text, parameters) => prisma.$queryRawUnsafe(text, ...parameters),
317
+ createTable: false, // the migration below owns the table
318
+ })
319
+ // pg
320
+ postgresTelegramUpdateStore({
321
+ query: (text, parameters) => pool.query(text, [...parameters]).then((result) => result.rows),
322
+ })
323
+ ```
324
+
325
+ Each rule is one conditional statement, so a claim is atomic across every
326
+ process sharing the table. By default the table is created on first use; where
327
+ migrations own the schema, `postgresTelegramUpdateStoreSchema(table)` is the
328
+ statement to put in one.
329
+
330
+ A store of your own — over an ORM, another database — is put through the same
331
+ rules the shipped ones are, concurrent claims included:
332
+
333
+ ```ts
334
+ import { checkTelegramUpdateStore } from 'stitchkit/telegram'
335
+
336
+ test('our update store keeps the rules', async () => {
337
+ expect(await checkTelegramUpdateStore(() => ourStore(freshTable()))).toEqual([])
338
+ })
339
+ ```
340
+
341
+ A store that reads and then writes in two statements passes every test with one
342
+ process and runs one update twice in production; the check races eight claims
343
+ against each other to catch it.
270
344
 
271
345
  ## Files from a local Bot API server
272
346
 
@@ -313,23 +387,24 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
313
387
  | `verifyTelegramInitData` | function | verify a Mini App `initData` signature against the bot token in constant time, before reading anything out of it |
314
388
  | `VerifyTelegramInitDataOptions` | _type_ | raw `initData`, bot token, optional `maxAgeSeconds` bound and injected clock |
315
389
  | `TelegramInitDataVerification` | _type_ | `{ valid: true, data }` or a refusal carrying its reason |
316
- | `TelegramInitData` | _type_ | verified `user`/`receiver`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
317
- | `TelegramInitDataUser` | _type_ | camelCase user record inferred from Telegram's signed `user` payload |
390
+ | `TelegramInitData` | _type_ | verified `user`/`receiver` as `TelegramUser`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
391
+ | `parseTelegramUser` / `TelegramUser` | function / _type_ | Telegram's `snake_case` user — grammY's `ctx.from`, a Bot API `User`, a Mini App `user` — as the one camelCase record initData returns too; `undefined` when it is not a user |
318
392
  | `TelegramInitDataRefusal` | _type_ | `missing-hash` / `signature-mismatch` / `malformed` / `expired` — an expired string is not a forged one |
319
393
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
320
394
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
321
- | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
395
+ | `TelegramSendFailureReason` / `TELEGRAM_SEND_FAILURE_REASONS` | _type_ / constant | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown`, and the same as a list; a `Record<TelegramSendFailureReason, …>` over them fails to compile when a reason is added |
322
396
  | `runTelegramBroadcast` | function | a resumable broadcast by `name` under a state `directory`: audience written once, an append-only journal, pacing at `ratePerSecond` (25), 429 waits `retry_after`, unreachable recipients never addressed again, a `message-invalid` or unreachable Telegram halts without charging the recipient, a send in flight at a crash is `uncertain` and not repeated, `dryRun`, one runner per name |
323
397
  | `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
324
398
  | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
325
399
  | `createTelegramOperatorChannel` | function | the operator chat, apart from the journal: `post(text, topic?)` returns at once and never throws; paced sends into forum topics, 429 honoured, oldest dropped on overflow, `drain(signal)` and `close()` for the way down |
326
400
  | `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` and `sensitiveValues` (a bot token is always masked), `dedupe`; a drop is `overflow` / `refused` / `attempts` / `closed` / `repeated` / `over-budget` with its classification |
401
+ | `createTelegramOperatorDedupe` / `TelegramOperatorDedupeVerdict` | function / _type_ | the channel's dedupe window alone, for a sender with its own bot and retry: `(text, topic?)` → `{ send: true, text }` with the held-back counts, or `{ send: false, reason: 'repeated' \| 'over-budget' }` |
327
402
  | `TelegramOperatorDedupe` | _type_ | `windowMs` (10 min): one message per `fingerprint` per window, the next one sent carries `repeatedLine(count)`; `maxPerWindow` (20) across fingerprints, the overflow counted onto the next one sent with `overBudgetLine(count)`; `now` |
328
403
  | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode`; with `'HTML'` the markup is cleaned and cut by what it shows, so the channel's cut never leaves a tag open |
329
404
  | `createTelegramLocalFiles` | function | files the local Bot API server wrote under `<root>/<token>/`: `resolve(file_path)` and `remove(file_path)` only inside the bot's directory, judged on real paths so a link is judged by where it leads; `check()` for readiness. Recommended root variable: `BOT_API_FILES_ROOT` |
330
405
  | `TelegramLocalFilesConfig` / `TelegramLocalFiles` / `TelegramLocalFilesCheck` / `TelegramLocalFileError` / `TelegramLocalFileRefusal` | _type_ / class | `root`, `token`; refusals `root-not-absolute` / `bot-directory-unavailable` / `outside-bot-directory` / `missing` / `not-a-file`, never carrying the path or the token |
331
406
  | `callTelegramBotApi` / `TelegramBotApiCall` / `TelegramBotApiError` | function / _type_ / class | one Bot API method over `fetch`; a refusal keeps `error_code`, `description` and `parameters` for the classifier, and no error names the URL |
332
- | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
407
+ | `TELEGRAM_BOT_TOKEN_PATTERN` / `redactTelegramBotToken` | constant / function | the secret half of a bot token — the bot's id before the colon is not matched; `redact` and every logger on it mask it already, `redactTelegramBotToken(text)` masks every token in a string to `123456:[redacted]` |
333
408
  | `claimTelegramWebhook` | function | set the webhook only where it already points here, or by naming the host it is taken from; confirm Telegram holds it; pending updates kept unless `dropPendingUpdates` |
334
409
  | `ClaimTelegramWebhookConfig` / `TelegramWebhookConfig` / `TelegramWebhookClaim` | _type_ | `token`, `url`, `secret`, `apiRoot`, `fetch`, `signal`; `takeoverFrom` (a host, or `'none'`), `allowedUpdates`, `maxConnections`, `dropPendingUpdates`; the claim's tagged `url`, `takenFrom`, `pendingUpdates` |
335
410
  | `TelegramWebhookClaimError` / `TelegramWebhookRefusal` / `TELEGRAM_WEBHOOK_NONE` | class / _type_ / constant | `owned-elsewhere` / `other-secret` / `not-confirmed` with `ownerHost` — a host, never a path; `'none'` names no webhook |
@@ -337,9 +412,12 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
337
412
  | `checkTelegramWebhook` / `TelegramWebhookState` | function / _type_ | whether the webhook still points here: `owned`, `ownerHost`, `pendingUpdates`, Telegram's `lastError` |
338
413
  | `receiveTelegramWebhook` / `ReceiveTelegramWebhookOptions` / `TelegramUpdateAcceptance` | function / _type_ | one webhook `Request`: 403 without the secret (compared in constant time), 405 for another method, 400 for no update, 200 once `accept` recorded it |
339
414
  | `createTelegramUpdateIntake` | function | webhook updates recorded before Telegram is answered and handled after: chat order kept, other chats alongside up to `maxConcurrent`, a renewed lease per attempt, a sweep that takes over what a restart or a dead process left, retries, `maxAttempts`, finished updates remembered `retainMs` to refuse repeats; at least once |
340
- | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle`, `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
415
+ | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle` (a grammY `BotError` is unwrapped: `retry`, `onFailure` and the stored error see what the middleware threw), `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
341
416
  | `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it |
342
417
  | `memoryTelegramUpdateStore` | function | the store in this process's memory — tests, or a bot that accepts losing unhandled updates to a restart |
343
418
  | `sqliteTelegramUpdateStore` / `SqliteTelegramUpdateStoreConfig` / `TelegramSqliteDatabase` | function / _type_ | the store in SQLite over the handle the application opened — `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync`; `table` (default `telegram_updates`) created when missing |
419
+ | `postgresTelegramUpdateStore` / `PostgresTelegramUpdateStoreConfig` / `TelegramPostgresQuery` | function / _type_ | the store in Postgres over the client the application holds, as one `query(text, parameters)` returning rows — Bun `sql.unsafe`, Prisma `$queryRawUnsafe`, `pg` `pool.query`; each rule one conditional statement; `table`, `createTable` (default `true`) |
420
+ | `postgresTelegramUpdateStoreSchema` | function | the table and its index as a statement for a migration, safe to run twice |
421
+ | `checkTelegramUpdateStore` | function | put any store through every rule, each on a fresh store from `make`, concurrent claims included; resolves with the violations, empty when it keeps them |
344
422
 
345
423
  ---
@@ -1,14 +1,50 @@
1
- # Upgrading 0.74.0 → 0.100.0 — stitchkit docs slice
1
+ # Upgrading 0.74.0 → 0.101.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.74.0 → 0.100.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.74.0 → 0.101.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.74.0 → 0.100.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.74.0 → 0.101.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.101.0
13
+
14
+ ### `TelegramInitDataUser` is `TelegramUser`
15
+
16
+ **Who must act:** code that names the type `TelegramInitDataUser` from
17
+ `stitchkit/telegram`. The record is the same; its name now says it is any
18
+ Telegram user, since `parseTelegramUser(ctx.from)` returns it for a Bot API user
19
+ too.
20
+
21
+ ```ts
22
+ // Before
23
+ import type { TelegramInitDataUser } from 'stitchkit/telegram'
24
+ function ensureAccount(user: TelegramInitDataUser) {}
25
+ // After
26
+ import type { TelegramUser } from 'stitchkit/telegram'
27
+ function ensureAccount(user: TelegramUser) {}
28
+ ```
29
+
30
+ ### `TELEGRAM_BOT_TOKEN_PATTERN` matches the secret half only
31
+
32
+ **Who must act:** code that uses `TELEGRAM_BOT_TOKEN_PATTERN` to find or cut a
33
+ whole token. It now matches what follows `<bot id>:`, so a masking replace keeps
34
+ the bot's id — `123456:[redacted]`. Masking needs nothing any more: `redact` and
35
+ every logger on it mask bot tokens by shape; remove the pattern from
36
+ `sensitiveUrlPatterns`, and use `redactTelegramBotToken(text)` for a string of
37
+ your own.
38
+
39
+ ```ts
40
+ // Before
41
+ createJsonLogger({ sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN] })
42
+ text.replace(new RegExp(TELEGRAM_BOT_TOKEN_PATTERN.source, 'g'), '[redacted]')
43
+ // After
44
+ createJsonLogger({})
45
+ redactTelegramBotToken(text)
46
+ ```
47
+
12
48
  ## Released migration: 0.100.0
13
49
 
14
50
  ### watch capacity and session/live/delivery adoption
@@ -11,7 +11,7 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.74.0 → 0.100.0: `llms/upgrading-0.74.0-0.100.0.txt`
14
+ - 0.74.0 → 0.101.0: `llms/upgrading-0.74.0-0.101.0.txt`
15
15
  - 0.61.0 → 0.73.0: `llms/upgrading-0.61.0-0.73.0.txt`
16
16
  - 0.44.0 → 0.60.0: `llms/upgrading-0.44.0-0.60.0.txt`
17
17
  - 0.37.0 → 0.37.0: `llms/upgrading-0.37.0.txt`
package/llms-full.txt CHANGED
@@ -70,12 +70,12 @@ own, recorded as an ADR.
70
70
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
71
71
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
72
72
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
73
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 45 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
73
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 46 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
74
74
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
75
75
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
76
76
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
77
77
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
78
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 45 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
78
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 46 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
79
79
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
80
80
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
81
81
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -85,7 +85,7 @@ own, recorded as an ADR.
85
85
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
86
86
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
87
87
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
88
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 45 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
88
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 46 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
89
89
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
90
90
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
91
91
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -10799,6 +10799,11 @@ Three things it does that a hand-written check usually does not:
10799
10799
  - `raw` keeps every signed pair, so a field Telegram adds after this release
10800
10800
  does not need a release to reach.
10801
10801
 
10802
+ The user arrives `camelCase` — `firstName`, `languageCode` — as `TelegramUser`.
10803
+ A bot meets the same person as grammY's `ctx.from`, in Telegram's `snake_case`;
10804
+ `parseTelegramUser(ctx.from)` gives the same `TelegramUser`, so one
10805
+ `ensureAccount(user)` serves the Mini App and the chat.
10806
+
10802
10807
  `maxAgeSeconds` is optional and there is no default: only the application knows
10803
10808
  how long its own session is worth. Omitting it means the string never expires,
10804
10809
  which is a decision rather than an oversight.
@@ -10828,6 +10833,17 @@ and our payload is wrong, which is the case a list of substrings quietly counts
10828
10833
  against the user. An unrecognised refusal leaves the recipient reachable: losing
10829
10834
  a working subscriber forever costs more than one wasted send.
10830
10835
 
10836
+ `TELEGRAM_SEND_FAILURE_REASONS` lists every reason. A table keyed by
10837
+ `TelegramSendFailureReason` — error codes, the text a person reads — fails to
10838
+ compile when a release adds one, instead of falling through to a default:
10839
+
10840
+ ```ts
10841
+ const codes: Record<TelegramSendFailureReason, string> = {
10842
+ 'blocked-by-user': 'E_BLOCKED',
10843
+ /* … every reason … */
10844
+ }
10845
+ ```
10846
+
10831
10847
  The rest of what a bot is assembled from — its resources, journal, operator
10832
10848
  channel, broadcasts and local Bot API files — is in [Telegram bots](telegram.md).
10833
10849
 
@@ -11894,6 +11910,18 @@ A payload goes into an audit row only after `sanitizePayload`:
11894
11910
  `tokenizer`) survive. This applies wherever a key exists — an object field and
11895
11911
  a `Map` entry alike, at any depth. A `Set` member has no key, so nothing there
11896
11912
  is masked by name;
11913
+ - **secrets are masked by their shape** in every string, whatever key holds
11914
+ it — the secret half of a Telegram bot token (`123456:[redacted]`: the bot's
11915
+ id stays, it tells two bots apart), the password in
11916
+ `postgresql://user:password@host` (user, host and path stay), and the value
11917
+ of a query parameter named as a secret (`?access_token=`, `?api-key=`,
11918
+ `?key=`, `?sig=`). A URL in an error message or a request log line is the
11919
+ usual way a token reaches a journal;
11920
+ - **an error keeps what it says about itself** — its own fields (`error_code`,
11921
+ `parameters`, a provider's `status`) are written beside `name`, `message`,
11922
+ `stack` and `cause`, masked by the same rules; a field holding a class
11923
+ instance other than an error (grammY's `BotError.ctx`) is named, not written,
11924
+ so one error cannot push its line past the bound and lose the stack;
11897
11925
  - **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
11898
11926
  never the bytes;
11899
11927
  - the result is **capped** — anything over the byte limit becomes a preview.
@@ -13277,16 +13305,21 @@ output, in pino's shape — numeric `level` (`debug` 20, `info` 30, `warn` 40,
13277
13305
  `error` 50), `time` in epoch milliseconds, `msg` — so `pino-pretty` reads it on
13278
13306
  a terminal and a supervisor matching `"level":50` sees errors. Unlike pino, an
13279
13307
  `Error` under *any* key is written with its name, message, stack and cause; an
13280
- error logged as `{ error }` does not become `"error":{}`.
13308
+ error logged as `{ error }` does not become `"error":{}`, and its own fields —
13309
+ a `TelegramBotApiError`'s `error_code` and `parameters.retry_after` — are on the
13310
+ line beside them.
13311
+
13312
+ A bot token is masked wherever it appears — a request URL in an error message,
13313
+ a local Bot API file path — with no configuration: `123456:[redacted]` keeps the
13314
+ bot's id, which is public and is what tells two bots apart in one journal. So is
13315
+ the password in an address and a query parameter named as a secret (`?token=`).
13281
13316
 
13282
13317
  ```ts
13283
13318
  import { createJsonLogger } from 'stitchkit/observability'
13284
- import { TELEGRAM_BOT_TOKEN_PATTERN } from 'stitchkit/telegram'
13285
13319
 
13286
13320
  export const logger = createJsonLogger({
13287
13321
  level: env.LOG_LEVEL,
13288
13322
  fields: { service: 'my-bot' },
13289
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
13290
13323
  })
13291
13324
 
13292
13325
  logger.error('Application startup failed', { error })
@@ -13302,11 +13335,13 @@ query values inside any URL variable (`DATABASE_URL`), and each URL-encoded:
13302
13335
  import { createJsonLogger, secretValuesFromEnv } from 'stitchkit/observability'
13303
13336
 
13304
13337
  export const logger = createJsonLogger({
13305
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
13306
13338
  sensitiveValues: secretValuesFromEnv(process.env),
13307
13339
  })
13308
13340
  ```
13309
13341
 
13342
+ A string of your own — a message for a person, a report — masks bot tokens with
13343
+ `redactTelegramBotToken(text)` from `stitchkit/telegram`.
13344
+
13310
13345
  A key added to the environment is masked from its first line; a URL's user name
13311
13346
  is left alone, since it names a role that appears in ordinary text.
13312
13347
 
@@ -13363,7 +13398,21 @@ const operators = createTelegramOperatorChannel<'errors'>({
13363
13398
  })
13364
13399
  ```
13365
13400
 
13366
- Held-back messages reach `onDropped` as `repeated` or `over-budget`. What to post stays the bot's. On the way down, drain it within the grace period
13401
+ Held-back messages reach `onDropped` as `repeated` or `over-budget`.
13402
+
13403
+ A bot that already sends its alerts itself — its own bot instance, its own retry
13404
+ — takes the window alone instead of switching the channel off with parameters:
13405
+
13406
+ ```ts
13407
+ import { createTelegramOperatorDedupe } from 'stitchkit/telegram'
13408
+
13409
+ const admit = createTelegramOperatorDedupe<'errors'>({ windowMs: 10 * 60_000 })
13410
+
13411
+ const verdict = admit(text, 'errors')
13412
+ if (verdict.send) await sendAlert(verdict.text) // the text carries the held-back counts
13413
+ ```
13414
+
13415
+ What to post stays the bot's. On the way down, drain it within the grace period
13367
13416
  and close it: `drain: (context) => operators.drain(context.signal)`,
13368
13417
  `close: () => operators.close()` in a resource.
13369
13418
 
@@ -13484,6 +13533,14 @@ itself be a secret — and says what to pass: set the takeover for one start and
13484
13533
  remove it. `checkTelegramWebhook` on a timer notices a webhook that moved
13485
13534
  elsewhere, which this process cannot see any other way.
13486
13535
 
13536
+ **Moving to it from a claim of your own.** A bot that tagged its webhook itself
13537
+ holds an address this one does not recognise, so the first claim is refused —
13538
+ `other-secret` when the address is the same, `owned-elsewhere` otherwise — and
13539
+ names its own host. Start once with `takeoverFrom` set to that host, then remove
13540
+ it. The tag is derived from the secret with a fixed context, so it is the same
13541
+ across stitchkit releases: that takeover is needed once per bot, not per
13542
+ upgrade. A secret rotation is a new owner, and takes a takeover too.
13543
+
13487
13544
  **The update is recorded before Telegram is answered.** A handler that runs
13488
13545
  longer than Telegram waits — a large file from a local Bot API server — gets the
13489
13546
  same update again while it still works, and a process that answers and then
@@ -13498,10 +13555,55 @@ fix (a refused message, a blocked user) is abandoned; `maxAttempts` (5) ends
13498
13555
  the rest. Delivery is at least once: a handler that finished but could not be
13499
13556
  recorded as finished runs again.
13500
13557
 
13558
+ With grammY the handler is `bot.handleUpdate`, which wraps a middleware's error
13559
+ in a `BotError`; the intake unwraps it, so `retry`, `onFailure` and the error
13560
+ stored with the update are what the middleware threw — a `retry` that returns
13561
+ `false` for the bot's own terminal error sees that error.
13562
+
13501
13563
  The store is an interface of six atomic steps (`TelegramUpdateStore`), so the
13502
- bot's own database can hold it; `sqliteTelegramUpdateStore` takes a
13503
- `bun:sqlite` or `node:sqlite` handle as it is, and `memoryTelegramUpdateStore`
13504
- is for tests.
13564
+ bot's own database holds it:
13565
+
13566
+ | Store | Takes |
13567
+ |---|---|
13568
+ | `sqliteTelegramUpdateStore` | a `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync` as it is |
13569
+ | `postgresTelegramUpdateStore` | one `query(text, parameters)` over the client the bot already holds |
13570
+ | `memoryTelegramUpdateStore` | nothing — tests, or a bot that accepts losing unhandled updates to a restart |
13571
+
13572
+ ```ts
13573
+ import { postgresTelegramUpdateStore } from 'stitchkit/telegram'
13574
+
13575
+ // Bun
13576
+ postgresTelegramUpdateStore({ query: (text, parameters) => sql.unsafe(text, [...parameters]) })
13577
+ // Prisma
13578
+ postgresTelegramUpdateStore({
13579
+ query: (text, parameters) => prisma.$queryRawUnsafe(text, ...parameters),
13580
+ createTable: false, // the migration below owns the table
13581
+ })
13582
+ // pg
13583
+ postgresTelegramUpdateStore({
13584
+ query: (text, parameters) => pool.query(text, [...parameters]).then((result) => result.rows),
13585
+ })
13586
+ ```
13587
+
13588
+ Each rule is one conditional statement, so a claim is atomic across every
13589
+ process sharing the table. By default the table is created on first use; where
13590
+ migrations own the schema, `postgresTelegramUpdateStoreSchema(table)` is the
13591
+ statement to put in one.
13592
+
13593
+ A store of your own — over an ORM, another database — is put through the same
13594
+ rules the shipped ones are, concurrent claims included:
13595
+
13596
+ ```ts
13597
+ import { checkTelegramUpdateStore } from 'stitchkit/telegram'
13598
+
13599
+ test('our update store keeps the rules', async () => {
13600
+ expect(await checkTelegramUpdateStore(() => ourStore(freshTable()))).toEqual([])
13601
+ })
13602
+ ```
13603
+
13604
+ A store that reads and then writes in two statements passes every test with one
13605
+ process and runs one update twice in production; the check races eight claims
13606
+ against each other to catch it.
13505
13607
 
13506
13608
  ## Files from a local Bot API server
13507
13609
 
@@ -13950,6 +14052,42 @@ analytics policy belong to the application.
13950
14052
 
13951
14053
  # Upgrading stitchkit
13952
14054
 
14055
+ ## Released migration: 0.101.0
14056
+
14057
+ ### `TelegramInitDataUser` is `TelegramUser`
14058
+
14059
+ **Who must act:** code that names the type `TelegramInitDataUser` from
14060
+ `stitchkit/telegram`. The record is the same; its name now says it is any
14061
+ Telegram user, since `parseTelegramUser(ctx.from)` returns it for a Bot API user
14062
+ too.
14063
+
14064
+ ```ts
14065
+ // Before
14066
+ import type { TelegramInitDataUser } from 'stitchkit/telegram'
14067
+ function ensureAccount(user: TelegramInitDataUser) {}
14068
+ // After
14069
+ import type { TelegramUser } from 'stitchkit/telegram'
14070
+ function ensureAccount(user: TelegramUser) {}
14071
+ ```
14072
+
14073
+ ### `TELEGRAM_BOT_TOKEN_PATTERN` matches the secret half only
14074
+
14075
+ **Who must act:** code that uses `TELEGRAM_BOT_TOKEN_PATTERN` to find or cut a
14076
+ whole token. It now matches what follows `<bot id>:`, so a masking replace keeps
14077
+ the bot's id — `123456:[redacted]`. Masking needs nothing any more: `redact` and
14078
+ every logger on it mask bot tokens by shape; remove the pattern from
14079
+ `sensitiveUrlPatterns`, and use `redactTelegramBotToken(text)` for a string of
14080
+ your own.
14081
+
14082
+ ```ts
14083
+ // Before
14084
+ createJsonLogger({ sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN] })
14085
+ text.replace(new RegExp(TELEGRAM_BOT_TOKEN_PATTERN.source, 'g'), '[redacted]')
14086
+ // After
14087
+ createJsonLogger({})
14088
+ redactTelegramBotToken(text)
14089
+ ```
14090
+
13953
14091
  ## Released migration: 0.100.0
13954
14092
 
13955
14093
  ### watch capacity and session/live/delivery adoption
@@ -19328,7 +19466,7 @@ audit event. See the [Observability guide](../guide/observability.md).
19328
19466
  | Export | Kind | Summary |
19329
19467
  |--------|------|---------|
19330
19468
  | `sanitizePayload` | function | redact secrets and cap size — [guide](../guide/observability.md#sanitisation) |
19331
- | `redact` | function | mask secret-named keys, drop binary blobs |
19469
+ | `redact` | function | mask secret-named keys and secrets recognised by shape in every string — the secret half of a bot token, a password in an address, a query parameter named as a secret; drop binary blobs; an error keeps its own fields (`error_code`, `parameters`), a class instance on one is named, not written |
19332
19470
  | `truncatePreview` | function | cap a value by serialised size |
19333
19471
  | `measureSize` | function | item count + byte size of a result |
19334
19472
  | `JsonValue` | _type_ | a JSON-serialisable value |
@@ -19758,23 +19896,24 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
19758
19896
  | `verifyTelegramInitData` | function | verify a Mini App `initData` signature against the bot token in constant time, before reading anything out of it |
19759
19897
  | `VerifyTelegramInitDataOptions` | _type_ | raw `initData`, bot token, optional `maxAgeSeconds` bound and injected clock |
19760
19898
  | `TelegramInitDataVerification` | _type_ | `{ valid: true, data }` or a refusal carrying its reason |
19761
- | `TelegramInitData` | _type_ | verified `user`/`receiver`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
19762
- | `TelegramInitDataUser` | _type_ | camelCase user record inferred from Telegram's signed `user` payload |
19899
+ | `TelegramInitData` | _type_ | verified `user`/`receiver` as `TelegramUser`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
19900
+ | `parseTelegramUser` / `TelegramUser` | function / _type_ | Telegram's `snake_case` user — grammY's `ctx.from`, a Bot API `User`, a Mini App `user` — as the one camelCase record initData returns too; `undefined` when it is not a user |
19763
19901
  | `TelegramInitDataRefusal` | _type_ | `missing-hash` / `signature-mismatch` / `malformed` / `expired` — an expired string is not a forged one |
19764
19902
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
19765
19903
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
19766
- | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
19904
+ | `TelegramSendFailureReason` / `TELEGRAM_SEND_FAILURE_REASONS` | _type_ / constant | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown`, and the same as a list; a `Record<TelegramSendFailureReason, …>` over them fails to compile when a reason is added |
19767
19905
  | `runTelegramBroadcast` | function | a resumable broadcast by `name` under a state `directory`: audience written once, an append-only journal, pacing at `ratePerSecond` (25), 429 waits `retry_after`, unreachable recipients never addressed again, a `message-invalid` or unreachable Telegram halts without charging the recipient, a send in flight at a crash is `uncertain` and not repeated, `dryRun`, one runner per name |
19768
19906
  | `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
19769
19907
  | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
19770
19908
  | `createTelegramOperatorChannel` | function | the operator chat, apart from the journal: `post(text, topic?)` returns at once and never throws; paced sends into forum topics, 429 honoured, oldest dropped on overflow, `drain(signal)` and `close()` for the way down |
19771
19909
  | `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` and `sensitiveValues` (a bot token is always masked), `dedupe`; a drop is `overflow` / `refused` / `attempts` / `closed` / `repeated` / `over-budget` with its classification |
19910
+ | `createTelegramOperatorDedupe` / `TelegramOperatorDedupeVerdict` | function / _type_ | the channel's dedupe window alone, for a sender with its own bot and retry: `(text, topic?)` → `{ send: true, text }` with the held-back counts, or `{ send: false, reason: 'repeated' \| 'over-budget' }` |
19772
19911
  | `TelegramOperatorDedupe` | _type_ | `windowMs` (10 min): one message per `fingerprint` per window, the next one sent carries `repeatedLine(count)`; `maxPerWindow` (20) across fingerprints, the overflow counted onto the next one sent with `overBudgetLine(count)`; `now` |
19773
19912
  | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode`; with `'HTML'` the markup is cleaned and cut by what it shows, so the channel's cut never leaves a tag open |
19774
19913
  | `createTelegramLocalFiles` | function | files the local Bot API server wrote under `<root>/<token>/`: `resolve(file_path)` and `remove(file_path)` only inside the bot's directory, judged on real paths so a link is judged by where it leads; `check()` for readiness. Recommended root variable: `BOT_API_FILES_ROOT` |
19775
19914
  | `TelegramLocalFilesConfig` / `TelegramLocalFiles` / `TelegramLocalFilesCheck` / `TelegramLocalFileError` / `TelegramLocalFileRefusal` | _type_ / class | `root`, `token`; refusals `root-not-absolute` / `bot-directory-unavailable` / `outside-bot-directory` / `missing` / `not-a-file`, never carrying the path or the token |
19776
19915
  | `callTelegramBotApi` / `TelegramBotApiCall` / `TelegramBotApiError` | function / _type_ / class | one Bot API method over `fetch`; a refusal keeps `error_code`, `description` and `parameters` for the classifier, and no error names the URL |
19777
- | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
19916
+ | `TELEGRAM_BOT_TOKEN_PATTERN` / `redactTelegramBotToken` | constant / function | the secret half of a bot token — the bot's id before the colon is not matched; `redact` and every logger on it mask it already, `redactTelegramBotToken(text)` masks every token in a string to `123456:[redacted]` |
19778
19917
  | `claimTelegramWebhook` | function | set the webhook only where it already points here, or by naming the host it is taken from; confirm Telegram holds it; pending updates kept unless `dropPendingUpdates` |
19779
19918
  | `ClaimTelegramWebhookConfig` / `TelegramWebhookConfig` / `TelegramWebhookClaim` | _type_ | `token`, `url`, `secret`, `apiRoot`, `fetch`, `signal`; `takeoverFrom` (a host, or `'none'`), `allowedUpdates`, `maxConnections`, `dropPendingUpdates`; the claim's tagged `url`, `takenFrom`, `pendingUpdates` |
19780
19919
  | `TelegramWebhookClaimError` / `TelegramWebhookRefusal` / `TELEGRAM_WEBHOOK_NONE` | class / _type_ / constant | `owned-elsewhere` / `other-secret` / `not-confirmed` with `ownerHost` — a host, never a path; `'none'` names no webhook |
@@ -19782,10 +19921,13 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
19782
19921
  | `checkTelegramWebhook` / `TelegramWebhookState` | function / _type_ | whether the webhook still points here: `owned`, `ownerHost`, `pendingUpdates`, Telegram's `lastError` |
19783
19922
  | `receiveTelegramWebhook` / `ReceiveTelegramWebhookOptions` / `TelegramUpdateAcceptance` | function / _type_ | one webhook `Request`: 403 without the secret (compared in constant time), 405 for another method, 400 for no update, 200 once `accept` recorded it |
19784
19923
  | `createTelegramUpdateIntake` | function | webhook updates recorded before Telegram is answered and handled after: chat order kept, other chats alongside up to `maxConcurrent`, a renewed lease per attempt, a sweep that takes over what a restart or a dead process left, retries, `maxAttempts`, finished updates remembered `retainMs` to refuse repeats; at least once |
19785
- | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle`, `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
19924
+ | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle` (a grammY `BotError` is unwrapped: `retry`, `onFailure` and the stored error see what the middleware threw), `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
19786
19925
  | `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it |
19787
19926
  | `memoryTelegramUpdateStore` | function | the store in this process's memory — tests, or a bot that accepts losing unhandled updates to a restart |
19788
19927
  | `sqliteTelegramUpdateStore` / `SqliteTelegramUpdateStoreConfig` / `TelegramSqliteDatabase` | function / _type_ | the store in SQLite over the handle the application opened — `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync`; `table` (default `telegram_updates`) created when missing |
19928
+ | `postgresTelegramUpdateStore` / `PostgresTelegramUpdateStoreConfig` / `TelegramPostgresQuery` | function / _type_ | the store in Postgres over the client the application holds, as one `query(text, parameters)` returning rows — Bun `sql.unsafe`, Prisma `$queryRawUnsafe`, `pg` `pool.query`; each rule one conditional statement; `table`, `createTable` (default `true`) |
19929
+ | `postgresTelegramUpdateStoreSchema` | function | the table and its index as a statement for a migration, safe to run twice |
19930
+ | `checkTelegramUpdateStore` | function | put any store through every rule, each on a fresh store from `make`, concurrent claims included; resolves with the violations, empty when it keeps them |
19789
19931
 
19790
19932
  ---
19791
19933
 
package/llms.txt CHANGED
@@ -21,8 +21,8 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
21
21
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
22
22
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
23
23
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 9.3 KB
24
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 37.5 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
25
- - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 43.7 KB
24
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 38.1 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
25
+ - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 44.8 KB
26
26
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 36.8 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 46.7 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 34.3 KB
27
27
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.2 KB
28
28
  - **stitchkit/agent-runtime/harness**: [llms/agent-runtime-harness.txt](llms/agent-runtime-harness.txt) 4.1 KB
@@ -40,7 +40,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
40
40
  - **stitchkit/testing**: [llms/testing.txt](llms/testing.txt) 30.8 KB
41
41
  - **stitchkit/agent-runtime/sandbox**: [llms/agent-runtime-sandbox.txt](llms/agent-runtime-sandbox.txt) 8.1 KB
42
42
  - **stitchkit/files**: [llms/files.txt](llms/files.txt) 2.9 KB
43
- - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 22.2 KB
43
+ - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 26.9 KB
44
44
  - **stitchkit/telegram/html**: [llms/telegram-html.txt](llms/telegram-html.txt) 2.7 KB
45
45
  - **stitchkit/telegram/screens**: [llms/telegram-screens.txt](llms/telegram-screens.txt) 21.2 KB
46
46
  - **stitchkit/tracking**: [llms/tracking.txt](llms/tracking.txt) 16.8 KB
@@ -55,7 +55,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
55
55
 
56
56
  ## Upgrading slices
57
57
  - **Upgrading — how to move across versions**: [llms/upgrading.txt](llms/upgrading.txt) 10.3 KB
58
- - **Upgrading 0.74.0 → 0.100.0**: [llms/upgrading-0.74.0-0.100.0.txt](llms/upgrading-0.74.0-0.100.0.txt) 45.1 KB
58
+ - **Upgrading 0.74.0 → 0.101.0**: [llms/upgrading-0.74.0-0.101.0.txt](llms/upgrading-0.74.0-0.101.0.txt) 46.3 KB
59
59
  - **Upgrading 0.61.0 → 0.73.0**: [llms/upgrading-0.61.0-0.73.0.txt](llms/upgrading-0.61.0-0.73.0.txt) 41.2 KB
60
60
  - **Upgrading 0.44.0 → 0.60.0**: [llms/upgrading-0.44.0-0.60.0.txt](llms/upgrading-0.44.0-0.60.0.txt) 43.9 KB
61
61
  - **Upgrading 0.37.0 → 0.37.0**: [llms/upgrading-0.37.0.txt](llms/upgrading-0.37.0.txt) 6.2 KB