@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.1

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 (295) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/adapters/clock.d.ts +3 -0
  5. package/dist/adapters/clock.js +4 -0
  6. package/dist/adapters/console-log.d.ts +13 -0
  7. package/dist/adapters/console-log.js +41 -0
  8. package/dist/adapters/d1-db.d.ts +5 -0
  9. package/dist/adapters/d1-db.js +37 -0
  10. package/dist/adapters/ids.d.ts +5 -0
  11. package/dist/adapters/ids.js +25 -0
  12. package/dist/adapters/index.d.ts +7 -0
  13. package/dist/adapters/keyring.d.ts +50 -0
  14. package/dist/adapters/keyring.js +174 -0
  15. package/dist/adapters/metrics.d.ts +28 -0
  16. package/dist/adapters/metrics.js +48 -0
  17. package/dist/adapters/r2-files.d.ts +20 -0
  18. package/dist/adapters/r2-files.js +53 -0
  19. package/dist/bin/cli.d.ts +1 -0
  20. package/dist/bin/cli.js +45 -0
  21. package/dist/client/broadcasts.d.ts +35 -0
  22. package/dist/client/commands.d.ts +23 -0
  23. package/dist/client/endpoint.d.ts +34 -0
  24. package/dist/client/events.d.ts +33 -0
  25. package/dist/client/index.d.ts +11 -0
  26. package/dist/client/index.js +3 -0
  27. package/dist/client/schedules.d.ts +42 -0
  28. package/dist/client/schema.d.ts +181 -0
  29. package/dist/client/send.d.ts +15 -0
  30. package/dist/client/templates.d.ts +47 -0
  31. package/dist/contracts/bots.d.ts +129 -0
  32. package/dist/contracts/bots.js +70 -0
  33. package/dist/contracts/broadcasts.d.ts +116 -0
  34. package/dist/contracts/broadcasts.js +63 -0
  35. package/dist/contracts/chats.d.ts +97 -0
  36. package/dist/contracts/chats.js +66 -0
  37. package/dist/contracts/commands.d.ts +52 -0
  38. package/dist/contracts/commands.js +53 -0
  39. package/dist/contracts/common.d.ts +38 -0
  40. package/dist/contracts/common.js +41 -0
  41. package/dist/contracts/deliveries.d.ts +103 -0
  42. package/dist/contracts/deliveries.js +74 -0
  43. package/dist/contracts/destinations.d.ts +24 -0
  44. package/dist/contracts/destinations.js +28 -0
  45. package/dist/contracts/events.d.ts +165 -0
  46. package/dist/contracts/events.js +98 -0
  47. package/dist/contracts/features.d.ts +20 -0
  48. package/dist/contracts/features.js +21 -0
  49. package/dist/contracts/health.d.ts +55 -0
  50. package/dist/contracts/health.js +58 -0
  51. package/dist/contracts/index.d.ts +22 -0
  52. package/dist/contracts/index.js +22 -0
  53. package/dist/contracts/labels.d.ts +38 -0
  54. package/dist/contracts/labels.js +52 -0
  55. package/dist/contracts/links.d.ts +51 -0
  56. package/dist/contracts/links.js +50 -0
  57. package/dist/contracts/meta.d.ts +132 -0
  58. package/dist/contracts/meta.js +85 -0
  59. package/dist/contracts/permissions.d.ts +24 -0
  60. package/dist/contracts/permissions.js +27 -0
  61. package/dist/contracts/problems.d.ts +106 -0
  62. package/dist/contracts/problems.js +101 -0
  63. package/dist/contracts/recipients.d.ts +36 -0
  64. package/dist/contracts/recipients.js +22 -0
  65. package/dist/contracts/schedules.d.ts +276 -0
  66. package/dist/contracts/schedules.js +143 -0
  67. package/dist/contracts/send.d.ts +126 -0
  68. package/dist/contracts/send.js +69 -0
  69. package/dist/contracts/subscriptions.d.ts +38 -0
  70. package/dist/contracts/subscriptions.js +31 -0
  71. package/dist/contracts/templates.d.ts +358 -0
  72. package/dist/contracts/templates.js +174 -0
  73. package/dist/contracts/users.d.ts +15 -0
  74. package/dist/contracts/users.js +19 -0
  75. package/dist/core/guards.d.ts +22 -0
  76. package/dist/core/guards.js +47 -0
  77. package/dist/core/html/escape.d.ts +2 -0
  78. package/dist/core/html/escape.js +36 -0
  79. package/dist/core/html/tokenize.js +65 -0
  80. package/dist/core/index.d.ts +18 -0
  81. package/dist/core/pacing.d.ts +1 -0
  82. package/dist/core/pacing.js +86 -0
  83. package/dist/core/random.d.ts +2 -0
  84. package/dist/core/random.js +38 -0
  85. package/dist/core/recipients.d.ts +1 -0
  86. package/dist/core/recipients.js +86 -0
  87. package/dist/core/schedule/describe.d.ts +3 -0
  88. package/dist/core/schedule/describe.js +74 -0
  89. package/dist/core/schedule/index.d.ts +4 -0
  90. package/dist/core/schedule/policy.d.ts +1 -0
  91. package/dist/core/schedule/policy.js +37 -0
  92. package/dist/core/schedule/rule.d.ts +25 -0
  93. package/dist/core/schedule/rule.js +142 -0
  94. package/dist/core/schedule/tz.d.ts +2 -0
  95. package/dist/core/schedule/tz.js +59 -0
  96. package/dist/core/split/plain.d.ts +2 -0
  97. package/dist/core/split/plain.js +23 -0
  98. package/dist/core/split/split.d.ts +15 -0
  99. package/dist/core/split/split.js +158 -0
  100. package/dist/core/template/ast.d.ts +58 -0
  101. package/dist/core/template/ast.js +5 -0
  102. package/dist/core/template/expr.js +108 -0
  103. package/dist/core/template/filters.d.ts +20 -0
  104. package/dist/core/template/filters.js +127 -0
  105. package/dist/core/template/format.d.ts +17 -0
  106. package/dist/core/template/format.js +88 -0
  107. package/dist/core/template/index.d.ts +20 -0
  108. package/dist/core/template/index.js +29 -0
  109. package/dist/core/template/parse.d.ts +8 -0
  110. package/dist/core/template/parse.js +369 -0
  111. package/dist/core/template/render.d.ts +17 -0
  112. package/dist/core/template/render.js +118 -0
  113. package/dist/core/template/shape.d.ts +28 -0
  114. package/dist/core/template/shape.js +151 -0
  115. package/dist/core/template/tags.js +105 -0
  116. package/dist/core/template/validate.d.ts +10 -0
  117. package/dist/core/template/validate.js +144 -0
  118. package/dist/core/template/variables.d.ts +15 -0
  119. package/dist/core/template/variables.js +41 -0
  120. package/dist/do/index.d.ts +3 -0
  121. package/dist/do/index.js +3 -0
  122. package/dist/do/telegram-bot-do.d.ts +45 -0
  123. package/dist/do/telegram-bot-do.js +219 -0
  124. package/dist/engine/actions/callback.js +256 -0
  125. package/dist/engine/actions/tokens.d.ts +2 -0
  126. package/dist/engine/actions/tokens.js +51 -0
  127. package/dist/engine/bot-api.js +20 -0
  128. package/dist/engine/bot-features.js +39 -0
  129. package/dist/engine/chat-state.js +66 -0
  130. package/dist/engine/content/audience.js +54 -0
  131. package/dist/engine/content/buttons.d.ts +1 -0
  132. package/dist/engine/content/buttons.js +113 -0
  133. package/dist/engine/content/fanout.js +284 -0
  134. package/dist/engine/content/intersect.d.ts +6 -0
  135. package/dist/engine/content/intersect.js +26 -0
  136. package/dist/engine/content/providers.d.ts +6 -0
  137. package/dist/engine/content/providers.js +142 -0
  138. package/dist/engine/content/recipients.js +128 -0
  139. package/dist/engine/content/render.js +138 -0
  140. package/dist/engine/content/reply.js +156 -0
  141. package/dist/engine/content/report-files.js +78 -0
  142. package/dist/engine/content/template-rows.js +74 -0
  143. package/dist/engine/dialogs/dialog.d.ts +2 -0
  144. package/dist/engine/dialogs/dialog.js +375 -0
  145. package/dist/engine/dispatch/builtins.js +198 -0
  146. package/dist/engine/dispatch/commands.js +194 -0
  147. package/dist/engine/dispatch/context.d.ts +2 -0
  148. package/dist/engine/dispatch/context.js +194 -0
  149. package/dist/engine/dispatch/dispatcher.js +66 -0
  150. package/dist/engine/dispatch/handlers.d.ts +1 -0
  151. package/dist/engine/dispatch/handlers.js +103 -0
  152. package/dist/engine/dispatch/lifecycle.js +85 -0
  153. package/dist/engine/dispatch/link-chat.js +31 -0
  154. package/dist/engine/dispatch/linking.js +273 -0
  155. package/dist/engine/errors.d.ts +60 -0
  156. package/dist/engine/errors.js +72 -0
  157. package/dist/engine/events/filters.d.ts +5 -0
  158. package/dist/engine/events/filters.js +82 -0
  159. package/dist/engine/events/kick.js +30 -0
  160. package/dist/engine/events/payload.d.ts +3 -0
  161. package/dist/engine/events/payload.js +16 -0
  162. package/dist/engine/events/tick.js +208 -0
  163. package/dist/engine/i18n.d.ts +12 -0
  164. package/dist/engine/i18n.js +55 -0
  165. package/dist/engine/inbound/files.js +121 -0
  166. package/dist/engine/inbound/handler.js +46 -0
  167. package/dist/engine/jobs.d.ts +32 -0
  168. package/dist/engine/jobs.js +85 -0
  169. package/dist/engine/link-codes.js +46 -0
  170. package/dist/engine/metrics.js +12 -0
  171. package/dist/engine/options.d.ts +369 -0
  172. package/dist/engine/options.js +81 -0
  173. package/dist/engine/outbox/dead-letter.js +78 -0
  174. package/dist/engine/outbox/do-registry.d.ts +10 -0
  175. package/dist/engine/outbox/do-registry.js +10 -0
  176. package/dist/engine/outbox/do-transport.d.ts +33 -0
  177. package/dist/engine/outbox/do-transport.js +73 -0
  178. package/dist/engine/outbox/drain.d.ts +13 -0
  179. package/dist/engine/outbox/drain.js +381 -0
  180. package/dist/engine/outbox/enqueue.d.ts +1 -0
  181. package/dist/engine/outbox/enqueue.js +63 -0
  182. package/dist/engine/outbox/select.js +24 -0
  183. package/dist/engine/outbox/send.js +362 -0
  184. package/dist/engine/outbox/transport.d.ts +35 -0
  185. package/dist/engine/outbox/transport.js +175 -0
  186. package/dist/engine/outbox/wake.js +28 -0
  187. package/dist/engine/registry.d.ts +502 -0
  188. package/dist/engine/registry.js +258 -0
  189. package/dist/engine/rows.d.ts +25 -0
  190. package/dist/engine/rows.js +131 -0
  191. package/dist/engine/runtime.d.ts +95 -0
  192. package/dist/engine/runtime.js +149 -0
  193. package/dist/engine/schedules/fire.js +208 -0
  194. package/dist/engine/schedules/resume.js +75 -0
  195. package/dist/engine/schedules/rows.js +69 -0
  196. package/dist/engine/schedules/tick.js +30 -0
  197. package/dist/engine/schedules/wake.js +50 -0
  198. package/dist/engine/state.d.ts +11 -0
  199. package/dist/engine/state.js +78 -0
  200. package/dist/locales/en.js +339 -0
  201. package/dist/locales/en.json +341 -0
  202. package/dist/locales/mr.js +339 -0
  203. package/dist/locales/mr.json +341 -0
  204. package/dist/ports/db.d.ts +22 -0
  205. package/dist/ports/index.d.ts +4 -0
  206. package/dist/ports/ports.d.ts +257 -0
  207. package/dist/ports/sql.d.ts +39 -0
  208. package/dist/ports/sql.js +76 -0
  209. package/dist/renderers/csv.d.ts +14 -0
  210. package/dist/renderers/csv.js +51 -0
  211. package/dist/renderers/html.d.ts +44 -0
  212. package/dist/renderers/html.js +81 -0
  213. package/dist/renderers/index.d.ts +5 -0
  214. package/dist/renderers/index.js +4 -0
  215. package/dist/renderers/table.d.ts +30 -0
  216. package/dist/renderers/table.js +114 -0
  217. package/dist/routes/admin.js +146 -0
  218. package/dist/routes/app.d.ts +3111 -0
  219. package/dist/routes/app.js +9 -0
  220. package/dist/routes/broadcasts.js +37 -0
  221. package/dist/routes/commands.js +35 -0
  222. package/dist/routes/content.js +12 -0
  223. package/dist/routes/events.js +36 -0
  224. package/dist/routes/http.d.ts +30 -0
  225. package/dist/routes/http.js +223 -0
  226. package/dist/routes/me.js +59 -0
  227. package/dist/routes/schedules.js +62 -0
  228. package/dist/routes/send.js +24 -0
  229. package/dist/routes/templates.js +70 -0
  230. package/dist/routes/webhook.js +147 -0
  231. package/dist/schema/columns.js +307 -0
  232. package/dist/schema/ddl.d.ts +7 -0
  233. package/dist/schema/ddl.js +45 -0
  234. package/dist/schema/index.d.ts +4255 -0
  235. package/dist/schema/index.js +44 -0
  236. package/dist/schema/reference.sql +359 -0
  237. package/dist/schema/tables.d.ts +4243 -0
  238. package/dist/schema/tables.js +327 -0
  239. package/dist/schema/version.d.ts +5 -0
  240. package/dist/schema/version.js +7 -0
  241. package/dist/server/index.d.ts +55 -0
  242. package/dist/server/index.js +46 -0
  243. package/dist/server/plugin.d.ts +81 -0
  244. package/dist/server/plugin.js +247 -0
  245. package/dist/server/version.d.ts +2 -0
  246. package/dist/server/version.js +4 -0
  247. package/dist/services/ai-tool.d.ts +26 -0
  248. package/dist/services/ai-tool.js +148 -0
  249. package/dist/services/bots.js +300 -0
  250. package/dist/services/broadcasts.js +390 -0
  251. package/dist/services/chats.js +83 -0
  252. package/dist/services/cleanup.js +83 -0
  253. package/dist/services/commands.js +140 -0
  254. package/dist/services/deliveries.js +53 -0
  255. package/dist/services/destinations.js +91 -0
  256. package/dist/services/event-rules.js +229 -0
  257. package/dist/services/events.js +20 -0
  258. package/dist/services/idempotency.js +64 -0
  259. package/dist/services/links.js +200 -0
  260. package/dist/services/menu.js +95 -0
  261. package/dist/services/ops.js +90 -0
  262. package/dist/services/rekey.js +63 -0
  263. package/dist/services/reports.js +87 -0
  264. package/dist/services/schedules.js +214 -0
  265. package/dist/services/send.d.ts +14 -0
  266. package/dist/services/send.js +421 -0
  267. package/dist/services/subscriptions.js +28 -0
  268. package/dist/services/templates.js +259 -0
  269. package/dist/telegram/classify.d.ts +30 -0
  270. package/dist/telegram/classify.js +164 -0
  271. package/dist/telegram/client.d.ts +73 -0
  272. package/dist/telegram/client.js +135 -0
  273. package/dist/telegram/errors.d.ts +37 -0
  274. package/dist/telegram/errors.js +62 -0
  275. package/dist/telegram/inbound.d.ts +40 -0
  276. package/dist/telegram/inbound.js +72 -0
  277. package/dist/telegram/index.d.ts +8 -0
  278. package/dist/telegram/multipart.d.ts +5 -0
  279. package/dist/telegram/multipart.js +47 -0
  280. package/dist/telegram/types.d.ts +271 -0
  281. package/dist/telegram/update.d.ts +833 -0
  282. package/dist/telegram/update.js +129 -0
  283. package/dist/testing/fake-html.d.ts +9 -0
  284. package/dist/testing/fake-html.js +83 -0
  285. package/dist/testing/fake-telegram.d.ts +206 -0
  286. package/dist/testing/fake-telegram.js +856 -0
  287. package/dist/testing/index.d.ts +7 -0
  288. package/dist/testing/index.js +7 -0
  289. package/dist/testing/reference-sql.d.ts +4 -0
  290. package/dist/testing/reference-sql.js +8 -0
  291. package/dist/testing/test-plugin.d.ts +126 -0
  292. package/dist/testing/test-plugin.js +182 -0
  293. package/dist/testing/updates.d.ts +100 -0
  294. package/dist/testing/updates.js +212 -0
  295. package/package.json +113 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@prajwalshete/telegram-plugin` (working name before the first release: `cf-telegram-plugin`). The format follows [Keep a Changelog](https://keepachangelog.com/),
4
+ and the package uses [semantic versioning](https://semver.org/) (0.x: minor versions may break).
5
+
6
+ Schema changes are listed under **Schema**. Upgrade by applying the new `reference.sql` statements (or
7
+ regenerating your Drizzle migration); the plugin answers `schema_outdated` until the tables match.
8
+
9
+ ## 0.1.1 (2026-10-28)
10
+
11
+ No code changes: first release published from GitHub Actions with npm Trusted Publishing.
12
+
13
+ ## 0.1.0 (2026-10-28)
14
+
15
+ First release.
16
+
17
+ ### Added
18
+
19
+ - `createTelegramPlugin()` with routes (admin, me, webhook, dev poll), jobs (`telegram:tick`, `telegram:cleanup`,
20
+ `telegram:rekey`), `emit`, `preview`, `enqueue`, `kick`, `queue`, `meta`, `aiToolSpec`.
21
+ - `define{Provider,Command,Event,Action,Dialog,Audience,FileSource}` and `definitions<Actor, Env>()`.
22
+ - `TelegramPluginError` (`code`, `status`, `detail`, `extensions`, `toProblem()`), the only error type the
23
+ programmatic API throws (unexpected failures: `internal_error` with `cause`); `isTelegramPluginError`,
24
+ `toPluginError`. `ports.errors(problem, error)` receives the full problem body including extension members.
25
+ - Problem codes `unauthenticated` (401, signed out) and `internal_error` (500), with en/mr labels.
26
+ - Per-invocation env for ports: `ports.files` / `ports.metrics` accept `(env) => port`, renderers get
27
+ `render(input, env)`, file sources get `resolve({ actor, ref, env })`.
28
+ - Per-bot feature switches (`PATCH /admin/bots/:id { features }`, off only, validated against the app's features).
29
+ - Custom template filters (`options.filters`, `defineFilter`) in rendering, validation, preview and `/admin/meta`.
30
+ - `GET /admin/users?q=` (user picker over `ports.users.search`); meta `filters` and `userSearch`.
31
+ - Default log is `consoleLog` at `info` (token redaction); `ports.log` also takes a level or `"silent"`.
32
+ - Bots (encrypted tokens, webhook or polling), linking, chats, forum topics, destinations, subscriptions.
33
+ - Commands, templates (versions, preview), schedules (daily/weekly/monthly/cron/once, any IANA zone), reports
34
+ (CSV built in, PDF via Browser Rendering or a host renderer), events, broadcasts, send intents.
35
+ - Actions (confirm, single use, live permission checks), dialogs (text, choice, photo, document steps), inbound
36
+ files, AI send bridge with per-audience guards.
37
+ - Outbox with pacing, retries and Telegram error handling; transports `cron`, `queue` (+ DLQ) and `do`
38
+ (`TelegramBotDO`); Analytics Engine metrics adapter; delivery log and health.
39
+ - Subpaths `server`, `do`, `renderers`, `schema` (+ `reference.sql`), `contracts`, `client`, `locales`, `testing`
40
+ (`FakeTelegram`, `createTestPlugin`). CLI `cf-telegram-plugin keyring new|rotate`.
41
+
42
+ ### Behaviour notes
43
+
44
+ - Finishing an action on a message also invalidates the other action buttons of that message.
45
+ - A single-use action's claim and the SQL the action returns commit in one batch. Actions that write on their own
46
+ inside `run` are at-least-once (make those writes idempotent).
47
+ - Deleting or pausing a schedule moves or clears the Durable Object scheduler wake.
48
+
49
+ - Pre-release review fixes (docs/REVIEW-0.1.0.md):
50
+ - **Security:** record sends keep a field only when both the sender's and the audience's provider runs returned
51
+ it with a non-null value (null redaction no longer leaks). Previews and renders for another audience
52
+ (`preview()`, `/admin/send/preview`, `/admin/templates/preview`, `/admin/broadcasts/:id/preview`,
53
+ `/admin/reports/render`) show the caller ∩ audience view and need the provider permission.
54
+ - **Security:** in groups, host commands, `/report`, inbound files and AI free text need a linked sender
55
+ (`groups.requireLinkedSender`, default true; per command `groupMembers: "any"`). Group `/start` unmute is
56
+ guarded like `/stop`. An admin invite code for another user parks a pending link the invited user confirms
57
+ (`/me/links/pending…`; `LinkResult.requiresConfirmation`).
58
+ - **Security:** `Idempotency-Key` is per caller/route/body, replays only JSON after the permission check, never
59
+ stores link codes or files, and in-flight keys expire after 60 s. `/admin/send` dedupe keys are per caller.
60
+ Encrypted bot columns use per-column AAD (`tg_bot.token:<id>`, `tg_bot.webhook:<id>`).
61
+ - **Security:** file sends by ref need `FilesPort.canRead`; `r2Files` denies by default (`r2Files(b, { canRead })`).
62
+ - Queue mode: a failed `queue.send` un-records the update and answers 500 so Telegram retries. `queue()` handles
63
+ the plugin's messages in mixed batches and returns `true` only for an all-plugin batch.
64
+ - Reliability: stuck schedule runs are resumed by the tick (same slot; `failed` / `run_stuck` after 3 tries or
65
+ 1 h); the drain commits sends every 5 sends / 1 s; events retry transient provider failures; provider data wins
66
+ over payload fields; no orphan action tokens on deduped sends.
67
+ - `/report <key>` finds any key (the list is capped at 30 with a hint); `/help` lists only runnable commands;
68
+ callback answers are cut on grapheme boundaries. Schema check is cached per database.
69
+ - Review follow-ups:
70
+ - A used admin invite tells the invited user in their already-linked chat on that bot (en/mr
71
+ `bot.link.pending_notice`) and calls the new hook `onPendingLink(pending, userId)` (show an in-app notice).
72
+ - `Idempotency-Key`: the in-flight entry is a lease with a fencing token, renewed every 20 s while the request
73
+ runs, so a request longer than 60 s is never taken over by a retry (409 instead), and a request that lost its
74
+ lease cannot overwrite or delete the new holder's entry.
75
+ - Queue mode: when `queue.send` fails **and** un-recording the update fails too, the update is processed in
76
+ `waitUntil` (logged `tg.webhook_unrecord_failed`, metric outcome `queue_fallback`) instead of being lost.
77
+ - Command replies use the orphan action-token guard (a re-processed update leaves no extra tokens).
78
+ - `HealthDto.schedules`: `stuckRuns` (`running` > 10 min) and `runStuckFailed24h`.
79
+ - **Breaking (pre-release):** command `defaultRoles: []` now means **no** role (omit it or use `["*"]` = `ANY_ROLE`
80
+ for any role; per-bot overrides alike). Every host context (`ProviderContext`, `CommandContext`, `ActionContext`,
81
+ `DialogFinishContext`, dialog `choices`, audience `resolve`) gets `env`. `./server` no longer exports the raw
82
+ services and engine stages (they check no permissions); `plugin.runtime()` is internal. The client declares
83
+ `/admin/reports/render` as a `BinaryEndpoint`. Non-plugin render errors in events are `render_failed`.
84
+ `engines.node` is `>=20` (CLI only; the library runs on Workers).
85
+
86
+ ### Schema
87
+
88
+ - Schema version 2 (19 `tg_*` tables). Table names are fixed; `telegramTables()` takes no options.
89
+ - v2 adds `CREATE UNIQUE INDEX IF NOT EXISTS tg_bot_tg_bot_uq ON tg_bot (tenant, tg_bot_id) WHERE deleted_at IS NULL`
90
+ (one live row per Telegram bot and tenant). Soft-delete duplicates before applying it; until then routes answer
91
+ `503 schema_outdated` with `missing: ["index:tg_bot_tg_bot_uq"]`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prajwal Shete
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,110 @@
1
- # Temporary Holding Version
1
+ # telegram-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Telegram bots for apps on **Cloudflare Workers + Hono + D1 (+ R2)**. Logic only, no UI: you get an HTTP API,
4
+ Zod contracts, permission names and en/mr labels, and your app draws its own screens.
5
+ - **Bots and people:** connect bots (tokens encrypted with a keyring), link app users with a one-time `/start`
6
+ code, register groups and forum topics, named destinations.
7
+ - **Content:** host-defined commands, templates (safe Telegram HTML, Mustache subset), schedules in any IANA zone,
8
+ CSV/PDF reports, event alerts emitted inside your own D1 batch, broadcasts, subscriptions and quiet hours.
9
+ - **Interactive:** action buttons (confirm, single use, re-checked permissions), multi-step dialogs with photo and
10
+ document steps, inbound files.
11
+ - **AI bridge:** a "send to Telegram" preview → confirm → commit flow with guards (allowed destinations, money and
12
+ link checks per audience), plus a JSON Schema tool spec.
13
+ - **Delivery:** a D1 outbox with pacing, retries, 429/migration/blocked handling. Transports: cron tick, Queues,
14
+ or one Durable Object per bot. Delivery log, health, metrics.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ pnpm add @prajwalshete/telegram-plugin hono zod # drizzle-orm is an optional peer (only for @prajwalshete/telegram-plugin/schema)
20
+ npx @prajwalshete/telegram-plugin keyring new # → TELEGRAM_KEYRING in .dev.vars / wrangler secret put
21
+ cp node_modules/@prajwalshete/telegram-plugin/dist/schema/reference.sql migrations/0001_telegram.sql
22
+ ```
23
+
24
+ Peers: `hono` ^4.13, `zod` ^4.6, `drizzle-orm` ^1.0.0-rc.5 (optional). Runtime dependency: `croner`. Node ≥ 20 for the CLI (the library runs on the Workers runtime).
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import { createTelegramPlugin } from "@prajwalshete/telegram-plugin/server";
30
+ import { Hono } from "hono";
31
+
32
+ interface Env {
33
+ DB: D1Database;
34
+ TELEGRAM_KEYRING: string;
35
+ PUBLIC_BASE_URL?: string;
36
+ }
37
+ interface Actor {
38
+ userId: string;
39
+ role: string;
40
+ }
41
+
42
+ const telegram = createTelegramPlugin<Env, Actor>({
43
+ env: (env) => ({ db: env.DB, keyring: env.TELEGRAM_KEYRING, publicBaseUrl: env.PUBLIC_BASE_URL }),
44
+ ports: {
45
+ actor: {
46
+ fromRequest: async (c) => yourSession(c), // who is calling the admin API (null = signed out)
47
+ forUser: (userId) => yourUserById(userId), // a linked Telegram user's live app actor
48
+ forAudience: () => null,
49
+ describe: (a) => ({ userId: a.userId, role: a.role }),
50
+ },
51
+ authorize: (actor, permission) => can(actor.role, permission), // "telegram.bots.manage", your own perms…
52
+ },
53
+ features: { commands: true, templates: true, schedules: true },
54
+ });
55
+
56
+ const app = new Hono<{ Bindings: Env }>().route("/api/telegram", telegram.routes);
57
+
58
+ export default {
59
+ fetch: app.fetch,
60
+ scheduled: async (c, env, ctx) => {
61
+ for (const job of telegram.jobs.filter((j) => j.cron === c.cron))
62
+ ctx.waitUntil(job.run({ env, now: c.scheduledTime, cron: c.cron }));
63
+ },
64
+ } satisfies ExportedHandler<Env>;
65
+ ```
66
+
67
+ `yourSession`, `yourUserById` and `can` are your app's own code. A complete, tested app is in
68
+ [`examples/reference-host`](https://github.com/PrajwalShete/telegram-plugin/tree/main/examples/reference-host).
69
+
70
+ ## Subpaths
71
+
72
+ | Import | Runs in | Contents |
73
+ | ---------------------------------------------------- | -------------------- | ---------------------------------------------------------- |
74
+ | `@prajwalshete/telegram-plugin/server` | worker | `createTelegramPlugin`, `define*` helpers, ports, adapters |
75
+ | `@prajwalshete/telegram-plugin/do` | worker | `TelegramBotDO` (optional per-bot Durable Object) |
76
+ | `@prajwalshete/telegram-plugin/renderers` | worker | CSV and PDF report renderers |
77
+ | `@prajwalshete/telegram-plugin/schema` | worker / drizzle-kit | Drizzle tables (`tg_*`), `referenceSql()` |
78
+ | `@prajwalshete/telegram-plugin/schema/reference.sql` | — | Plain SQL schema for hosts without Drizzle |
79
+ | `@prajwalshete/telegram-plugin/contracts` | browser + worker | Zod DTOs, feature keys, permissions, problem codes |
80
+ | `@prajwalshete/telegram-plugin/client` | browser | Typed admin API client types (Hono `hc`) |
81
+ | `@prajwalshete/telegram-plugin/locales/{en,mr}.json` | any | Labels for every enum + bot strings |
82
+ | `@prajwalshete/telegram-plugin/testing` | tests | `FakeTelegram`, `createTestPlugin`, update builders |
83
+
84
+ ## Errors and logs
85
+
86
+ Admin routes answer RFC 9457 `application/problem+json` with a stable `code` (`unauthenticated` 401, `forbidden`
87
+ 403, `validation_failed` with `issues[]`, `template_invalid` with `errors[]`, `send_text_rejected` with `reasons[]`, …),
88
+ or throw your own error type when you set `ports.errors(problem, error)`. The programmatic API (`emit`, `preview`,
89
+ `enqueue`, `kick`, `aiToolSpec`) throws only `TelegramPluginError` (`code`, `status`, `extensions`, `toProblem()`);
90
+ check with `isTelegramPluginError`. Logs go to the built-in `consoleLog` (JSON lines, tokens redacted) at `info`
91
+ unless you pass `ports.log` (a `LogPort` or `"debug" | "info" | "warn" | "error" | "silent"`).
92
+
93
+ ## Data protection
94
+
95
+ - Every provider runs as the recipient's audience. Record sends keep a field only when both the sender and the
96
+ audience got it with a non-null value; previews and renders for another audience show the caller ∩ audience view.
97
+ - In groups, members must link their own Telegram before commands, `/report`, files or AI free text run as the
98
+ group's audience (`groups.requireLinkedSender`, default on). An admin invite for another user waits for that
99
+ user's confirmation in the app.
100
+ - `./server` exports only entry points that check permissions (routes and plugin methods) plus pure helpers.
101
+
102
+ ## Documentation
103
+
104
+ The [integration guide](https://github.com/PrajwalShete/telegram-plugin/tree/main/docs/guide) covers install,
105
+ schema, Worker wiring, ports, permissions, providers/commands/events/actions/dialogs, features, building a settings
106
+ UI, the AI bridge, local development, going live, operations, troubleshooting and a full reference.
107
+
108
+ ## License
109
+
110
+ MIT. See [CHANGELOG.md](CHANGELOG.md) for changes.
@@ -0,0 +1,3 @@
1
+ import { ClockPort } from "../ports/ports.js";
2
+ /** Default clock: `Date.now()` (epoch ms UTC); no `sleep`, so the runtime waits with `setTimeout`. */
3
+ export declare const systemClock: ClockPort;
@@ -0,0 +1,4 @@
1
+ // src/adapters/clock.ts
2
+ /** Default clock: `Date.now()` (epoch ms UTC); no `sleep`, so the runtime waits with `setTimeout`. */
3
+ const systemClock = { now: () => Date.now() };
4
+ export { systemClock };
@@ -0,0 +1,13 @@
1
+ import { LogFields, LogPort } from "../ports/ports.js";
2
+ declare const LEVELS: readonly ["debug", "info", "warn", "error"];
3
+ type Level = (typeof LEVELS)[number];
4
+ /** Strips anything shaped like a bot token or a Bot API URL. A safety net; callers must not log them at all. */
5
+ export declare function redactSecrets(text: string): string;
6
+ /** JSON-lines logger on `console` (Workers Logs picks it up). */
7
+ export declare function consoleLog(opts?: {
8
+ level?: Level;
9
+ base?: LogFields;
10
+ }): LogPort;
11
+ /** Logger that drops everything. */
12
+ export declare const silentLog: LogPort;
13
+ export {};
@@ -0,0 +1,41 @@
1
+ // src/adapters/console-log.ts
2
+ const LEVELS = [
3
+ "debug",
4
+ "info",
5
+ "warn",
6
+ "error"
7
+ ];
8
+ /** Strips anything shaped like a bot token or a Bot API URL. A safety net; callers must not log them at all. */
9
+ function redactSecrets(text) {
10
+ return text.replace(/api\.telegram\.org\/(file\/)?bot[^\s"'/]*/gi, "api.telegram.org/$1bot<redacted>").replace(/\b\d{5,20}:[A-Za-z0-9_-]{30,}\b/g, "<redacted-token>");
11
+ }
12
+ /** JSON-lines logger on `console` (Workers Logs picks it up). */
13
+ function consoleLog(opts = {}) {
14
+ const min = LEVELS.indexOf(opts.level ?? "info");
15
+ const write = (level, evt, fields) => {
16
+ if (LEVELS.indexOf(level) < min) return;
17
+ const line = redactSecrets(JSON.stringify({
18
+ level,
19
+ evt,
20
+ ...opts.base,
21
+ ...fields
22
+ }));
23
+ if (level === "error") console.error(line);
24
+ else if (level === "warn") console.warn(line);
25
+ else console.log(line);
26
+ };
27
+ return {
28
+ debug: (evt, fields) => write("debug", evt, fields),
29
+ info: (evt, fields) => write("info", evt, fields),
30
+ warn: (evt, fields) => write("warn", evt, fields),
31
+ error: (evt, fields) => write("error", evt, fields)
32
+ };
33
+ }
34
+ /** Logger that drops everything. */
35
+ const silentLog = {
36
+ debug() {},
37
+ info() {},
38
+ warn() {},
39
+ error() {}
40
+ };
41
+ export { consoleLog, redactSecrets, silentLog };
@@ -0,0 +1,5 @@
1
+ import { DbPort } from "../ports/db.js";
2
+ /** D1 allows at most 100 bound parameters per statement. */
3
+ export declare const D1_MAX_PARAMS = 100;
4
+ /** The default `DbPort` over a D1 binding (or a D1 session). */
5
+ export declare function d1Db(db: D1Database | D1DatabaseSession): DbPort;
@@ -0,0 +1,37 @@
1
+ // src/adapters/d1-db.ts
2
+ /** D1 allows at most 100 bound parameters per statement. */
3
+ const D1_MAX_PARAMS = 100;
4
+ function prepare(db, q) {
5
+ if (q.params.length > 100) throw new RangeError(`statement has ${q.params.length} params; D1 allows 100 (chunk the insert)`);
6
+ const stmt = db.prepare(q.text);
7
+ return q.params.length > 0 ? stmt.bind(...q.params) : stmt;
8
+ }
9
+ function runResult(meta) {
10
+ return {
11
+ changes: meta.changes ?? 0,
12
+ lastRowId: meta.last_row_id ?? null
13
+ };
14
+ }
15
+ /** The default `DbPort` over a D1 binding (or a D1 session). */
16
+ function d1Db(db) {
17
+ const d = db;
18
+ return {
19
+ async all(q) {
20
+ return (await prepare(d, q).all()).results;
21
+ },
22
+ async first(q) {
23
+ return await prepare(d, q).first() ?? null;
24
+ },
25
+ async run(q) {
26
+ return runResult((await prepare(d, q).run()).meta);
27
+ },
28
+ async batch(qs) {
29
+ if (qs.length === 0) return [];
30
+ return (await d.batch(qs.map((q) => prepare(d, q)))).map((r) => ({
31
+ rows: r.results ?? [],
32
+ changes: r.meta.changes ?? 0
33
+ }));
34
+ }
35
+ };
36
+ }
37
+ export { D1_MAX_PARAMS, d1Db };
@@ -0,0 +1,5 @@
1
+ import { ClockPort, IdsPort } from "../ports/ports.js";
2
+ /** RFC 9562 UUIDv7: 48-bit ms timestamp, version 7, variant 10, 74 random bits. */
3
+ export declare function uuidv7(now?: number): string;
4
+ /** Default ids port (UUIDv7 from the given clock). */
5
+ export declare function uuidv7Ids(clock?: ClockPort): IdsPort;
@@ -0,0 +1,25 @@
1
+ // src/adapters/ids.ts
2
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
3
+ /** RFC 9562 UUIDv7: 48-bit ms timestamp, version 7, variant 10, 74 random bits. */
4
+ function uuidv7(now = Date.now()) {
5
+ const b = /* @__PURE__ */ new Uint8Array(16);
6
+ crypto.getRandomValues(b);
7
+ let ts = Math.max(0, Math.floor(now));
8
+ for (let i = 5; i >= 0; i--) {
9
+ b[i] = ts % 256;
10
+ ts = Math.floor(ts / 256);
11
+ }
12
+ b[6] = (b[6] ?? 0) & 15 | 112;
13
+ b[8] = (b[8] ?? 0) & 63 | 128;
14
+ let s = "";
15
+ for (let i = 0; i < 16; i++) {
16
+ if (i === 4 || i === 6 || i === 8 || i === 10) s += "-";
17
+ s += HEX[b[i] ?? 0];
18
+ }
19
+ return s;
20
+ }
21
+ /** Default ids port (UUIDv7 from the given clock). */
22
+ function uuidv7Ids(clock) {
23
+ return { uuid: () => uuidv7(clock ? clock.now() : Date.now()) };
24
+ }
25
+ export { uuidv7, uuidv7Ids };
@@ -0,0 +1,7 @@
1
+ import { systemClock } from "./clock.js";
2
+ import { consoleLog, redactSecrets, silentLog } from "./console-log.js";
3
+ import { D1_MAX_PARAMS, d1Db } from "./d1-db.js";
4
+ import { uuidv7, uuidv7Ids } from "./ids.js";
5
+ import { AnalyticsEngineLike, AnalyticsEngineMetricsOptions, analyticsEngineMetrics, noopMetrics } from "./metrics.js";
6
+ import { R2FilesOptions, r2Files } from "./r2-files.js";
7
+ export { type AnalyticsEngineLike, type AnalyticsEngineMetricsOptions, D1_MAX_PARAMS, type R2FilesOptions, analyticsEngineMetrics, consoleLog, d1Db, noopMetrics, r2Files, redactSecrets, silentLog, systemClock, uuidv7, uuidv7Ids };
@@ -0,0 +1,50 @@
1
+ import { KeyringPort } from "../ports/ports.js";
2
+ /** Shape of the `TELEGRAM_KEYRING` secret. */
3
+ export interface KeyringSecret {
4
+ /** Key id used for new ciphertexts; must be in `keys`. */
5
+ active: string;
6
+ /** Master keys by id (`[A-Za-z0-9_-]{1,32}`), base64 or base64url, at least 32 bytes each. */
7
+ keys: Record<string, string>;
8
+ }
9
+ /** The default `KeyringPort` (`webCryptoKeyring`), plus rekey helpers. */
10
+ export interface WebCryptoKeyring extends KeyringPort {
11
+ /** True when the blob was written with a key other than the active one (the rekey job re-encrypts it). */
12
+ needsRekey(ciphertext: string): boolean;
13
+ /** All key ids in the secret. */
14
+ readonly kids: readonly string[];
15
+ }
16
+ /** Thrown for an invalid keyring secret, an unknown key id, or a ciphertext that fails to decrypt. */
17
+ export declare class KeyringError extends Error {
18
+ /** Always `"KeyringError"`. */
19
+ readonly name = "KeyringError";
20
+ }
21
+ /** Unpadded base64url encoding of bytes. */
22
+ export declare function bytesToBase64Url(bytes: Uint8Array): string;
23
+ /** Accepts base64 and base64url, with or without padding. Throws on other characters. */
24
+ export declare function base64ToBytes(text: string): Uint8Array;
25
+ /** Validates the secret JSON (string or parsed object). Error messages never include key material. */
26
+ export declare function parseKeyringSecret(input: string | KeyringSecret): {
27
+ active: string;
28
+ keys: Map<string, Uint8Array>;
29
+ };
30
+ /** Key id of a `v1.<kid>.…` blob, or null when the shape is wrong. */
31
+ export declare function kidOfCiphertext(ciphertext: string): string | null;
32
+ /**
33
+ * The default keyring: HKDF-SHA-256 per key id → AES-256-GCM, format `v1.<kid>.<iv>.<ct>`. Throws `KeyringError`
34
+ * on an invalid secret (bad JSON, short keys, `active` missing from `keys`).
35
+ */
36
+ export declare function webCryptoKeyring(secret: string | KeyringSecret): WebCryptoKeyring;
37
+ /** Random bytes as base64url (for webhook secrets and link codes). */
38
+ export declare function randomBase64Url(bytes?: number): string;
39
+ /**
40
+ * A fresh keyring secret JSON. Pass the current secret to add a new active key and keep the old ones (rotation).
41
+ */
42
+ export declare function generateKeyringSecret(opts?: {
43
+ kid?: string;
44
+ existing?: string | KeyringSecret;
45
+ }): string;
46
+ /**
47
+ * Constant-time string comparison (webhook secrets). Both sides are hashed first, so different lengths take the
48
+ * same time and reveal nothing. Uses `crypto.subtle.timingSafeEqual` in workerd, a XOR loop elsewhere.
49
+ */
50
+ export declare function timingSafeEqualStrings(a: string, b: string): Promise<boolean>;
@@ -0,0 +1,174 @@
1
+ // src/adapters/keyring.ts
2
+ /** Thrown for an invalid keyring secret, an unknown key id, or a ciphertext that fails to decrypt. */
3
+ var KeyringError = class extends Error {
4
+ /** Always `"KeyringError"`. */
5
+ name = "KeyringError";
6
+ };
7
+ const VERSION = "v1";
8
+ const KID_RE = /^[A-Za-z0-9_-]{1,32}$/;
9
+ const HKDF_SALT = new TextEncoder().encode("cf-telegram-plugin/keyring/v1");
10
+ const MIN_KEY_BYTES = 32;
11
+ const IV_BYTES = 12;
12
+ /** Unpadded base64url encoding of bytes. */
13
+ function bytesToBase64Url(bytes) {
14
+ let s = "";
15
+ for (const b of bytes) s += String.fromCharCode(b);
16
+ return btoa(s).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
17
+ }
18
+ /** Accepts base64 and base64url, with or without padding. Throws on other characters. */
19
+ function base64ToBytes(text) {
20
+ const clean = text.trim().replaceAll("-", "+").replaceAll("_", "/");
21
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(clean)) throw new KeyringError("invalid base64");
22
+ const bin = atob(clean.padEnd(Math.ceil(clean.length / 4) * 4, "="));
23
+ const out = new Uint8Array(bin.length);
24
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
25
+ return out;
26
+ }
27
+ /** Validates the secret JSON (string or parsed object). Error messages never include key material. */
28
+ function parseKeyringSecret(input) {
29
+ let raw = input;
30
+ if (typeof input === "string") try {
31
+ raw = JSON.parse(input);
32
+ } catch {
33
+ throw new KeyringError("keyring secret is not valid JSON");
34
+ }
35
+ if (typeof raw !== "object" || raw === null) throw new KeyringError("keyring secret must be an object");
36
+ const { active, keys } = raw;
37
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) throw new KeyringError("keyring secret needs \"keys\": { kid: base64 }");
38
+ const map = /* @__PURE__ */ new Map();
39
+ for (const [kid, value] of Object.entries(keys)) {
40
+ if (!KID_RE.test(kid)) throw new KeyringError(`key id "${kid.slice(0, 32)}" must match [A-Za-z0-9_-]{1,32}`);
41
+ if (typeof value !== "string") throw new KeyringError(`key "${kid}" must be a base64 string`);
42
+ let bytes;
43
+ try {
44
+ bytes = base64ToBytes(value);
45
+ } catch {
46
+ throw new KeyringError(`key "${kid}" is not valid base64`);
47
+ }
48
+ if (bytes.length < MIN_KEY_BYTES) throw new KeyringError(`key "${kid}" must be at least ${MIN_KEY_BYTES} bytes`);
49
+ map.set(kid, bytes);
50
+ }
51
+ if (map.size === 0) throw new KeyringError("keyring secret has no keys");
52
+ if (typeof active !== "string" || !map.has(active)) throw new KeyringError("keyring secret \"active\" must name one of its keys");
53
+ return {
54
+ active,
55
+ keys: map
56
+ };
57
+ }
58
+ /** Key id of a `v1.<kid>.…` blob, or null when the shape is wrong. */
59
+ function kidOfCiphertext(ciphertext) {
60
+ const parts = ciphertext.split(".");
61
+ if (parts.length !== 4 || parts[0] !== VERSION) return null;
62
+ const kid = parts[1] ?? "";
63
+ return KID_RE.test(kid) ? kid : null;
64
+ }
65
+ /**
66
+ * The default keyring: HKDF-SHA-256 per key id → AES-256-GCM, format `v1.<kid>.<iv>.<ct>`. Throws `KeyringError`
67
+ * on an invalid secret (bad JSON, short keys, `active` missing from `keys`).
68
+ */
69
+ function webCryptoKeyring(secret) {
70
+ const { active, keys } = parseKeyringSecret(secret);
71
+ const derived = /* @__PURE__ */ new Map();
72
+ const enc = new TextEncoder();
73
+ function keyFor(kid) {
74
+ let key = derived.get(kid);
75
+ if (!key) {
76
+ const master = keys.get(kid);
77
+ if (!master) throw new KeyringError(`unknown key id "${kid}"`);
78
+ key = crypto.subtle.importKey("raw", master.slice(), "HKDF", false, ["deriveKey"]).then((ikm) => crypto.subtle.deriveKey({
79
+ name: "HKDF",
80
+ hash: "SHA-256",
81
+ salt: HKDF_SALT,
82
+ info: enc.encode(`aes-256-gcm:${kid}`)
83
+ }, ikm, {
84
+ name: "AES-GCM",
85
+ length: 256
86
+ }, false, ["encrypt", "decrypt"]));
87
+ derived.set(kid, key);
88
+ }
89
+ return key;
90
+ }
91
+ return {
92
+ kids: [...keys.keys()],
93
+ activeKid: () => active,
94
+ kidOf: kidOfCiphertext,
95
+ needsRekey: (ciphertext) => kidOfCiphertext(ciphertext) !== active,
96
+ async encrypt(plaintext, aad) {
97
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
98
+ const ct = await crypto.subtle.encrypt({
99
+ name: "AES-GCM",
100
+ iv,
101
+ additionalData: enc.encode(aad)
102
+ }, await keyFor(active), enc.encode(plaintext));
103
+ return `${VERSION}.${active}.${bytesToBase64Url(iv)}.${bytesToBase64Url(new Uint8Array(ct))}`;
104
+ },
105
+ async decrypt(ciphertext, aad) {
106
+ const kid = kidOfCiphertext(ciphertext);
107
+ if (!kid) throw new KeyringError("malformed ciphertext");
108
+ if (!keys.has(kid)) throw new KeyringError(`ciphertext uses key "${kid}", which is not in the keyring`);
109
+ const [, , ivText = "", ctText = ""] = ciphertext.split(".");
110
+ let iv;
111
+ let ct;
112
+ try {
113
+ iv = base64ToBytes(ivText);
114
+ ct = base64ToBytes(ctText);
115
+ } catch {
116
+ throw new KeyringError("malformed ciphertext");
117
+ }
118
+ if (iv.length !== IV_BYTES || ct.length < 16) throw new KeyringError("malformed ciphertext");
119
+ try {
120
+ const pt = await crypto.subtle.decrypt({
121
+ name: "AES-GCM",
122
+ iv: iv.slice(),
123
+ additionalData: enc.encode(aad)
124
+ }, await keyFor(kid), ct.slice());
125
+ return new TextDecoder().decode(pt);
126
+ } catch {
127
+ throw new KeyringError("decryption failed (wrong key, wrong AAD or tampered ciphertext)");
128
+ }
129
+ }
130
+ };
131
+ }
132
+ /** Random bytes as base64url (for webhook secrets and link codes). */
133
+ function randomBase64Url(bytes = 32) {
134
+ return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
135
+ }
136
+ /**
137
+ * A fresh keyring secret JSON. Pass the current secret to add a new active key and keep the old ones (rotation).
138
+ */
139
+ function generateKeyringSecret(opts = {}) {
140
+ const keys = {};
141
+ if (opts.existing !== void 0) {
142
+ const parsed = parseKeyringSecret(opts.existing);
143
+ for (const [kid, bytes] of parsed.keys) keys[kid] = bytesToBase64Url(bytes);
144
+ }
145
+ let kid = opts.kid;
146
+ if (kid === void 0) {
147
+ let n = Object.keys(keys).length + 1;
148
+ while (`k${n}` in keys) n++;
149
+ kid = `k${n}`;
150
+ }
151
+ if (!KID_RE.test(kid)) throw new KeyringError("key id must match [A-Za-z0-9_-]{1,32}");
152
+ if (kid in keys) throw new KeyringError(`key id "${kid}" already exists`);
153
+ keys[kid] = randomBase64Url(32);
154
+ return JSON.stringify({
155
+ active: kid,
156
+ keys
157
+ });
158
+ }
159
+ /**
160
+ * Constant-time string comparison (webhook secrets). Both sides are hashed first, so different lengths take the
161
+ * same time and reveal nothing. Uses `crypto.subtle.timingSafeEqual` in workerd, a XOR loop elsewhere.
162
+ */
163
+ async function timingSafeEqualStrings(a, b) {
164
+ const enc = new TextEncoder();
165
+ const [ha, hb] = await Promise.all([crypto.subtle.digest("SHA-256", enc.encode(a)), crypto.subtle.digest("SHA-256", enc.encode(b))]);
166
+ const subtle = crypto.subtle;
167
+ if (typeof subtle.timingSafeEqual === "function") return subtle.timingSafeEqual(ha, hb);
168
+ const x = new Uint8Array(ha);
169
+ const y = new Uint8Array(hb);
170
+ let diff = 0;
171
+ for (let i = 0; i < x.length; i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
172
+ return diff === 0;
173
+ }
174
+ export { KeyringError, base64ToBytes, bytesToBase64Url, generateKeyringSecret, kidOfCiphertext, parseKeyringSecret, randomBase64Url, timingSafeEqualStrings, webCryptoKeyring };
@@ -0,0 +1,28 @@
1
+ import { MetricsPort } from "../ports/ports.js";
2
+ /** Discards everything (the default when `ports.metrics` is not set). */
3
+ export declare const noopMetrics: MetricsPort;
4
+ /** The part of an Analytics Engine dataset binding the adapter uses. */
5
+ export interface AnalyticsEngineLike {
6
+ /** Writes one data point (fire-and-forget). */
7
+ writeDataPoint(point: {
8
+ indexes?: string[];
9
+ blobs?: string[];
10
+ doubles?: number[];
11
+ }): void;
12
+ }
13
+ /** Options of `analyticsEngineMetrics`. */
14
+ export interface AnalyticsEngineMetricsOptions {
15
+ /** Added to every data point's tags (for example `{ app: "my-app" }`). */
16
+ tags?: Record<string, string>;
17
+ }
18
+ /**
19
+ * Writes one data point per call. Layout (query with SQL over the dataset):
20
+ * - `index1` = metric name (the sampling key)
21
+ * - `blob1` = name, `blob2` = `count` | `timing`, `blob3` = botId, `blob4` = outcome, `blob5` = code,
22
+ * `blob6` = other tags as sorted `k=v;k=v`
23
+ * - `double1` = value (count n, or ms)
24
+ *
25
+ * Example: `SELECT blob4 AS outcome, SUM(_sample_interval * double1) AS n FROM tg_metrics
26
+ * WHERE blob1 = 'tg.send' AND timestamp > NOW() - INTERVAL '1' DAY GROUP BY outcome`.
27
+ */
28
+ export declare function analyticsEngineMetrics(dataset: AnalyticsEngineLike, opts?: AnalyticsEngineMetricsOptions): MetricsPort;