@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
@@ -0,0 +1,105 @@
1
+ // src/core/template/tags.ts
2
+ const ALLOWED_TAGS = [
3
+ "b",
4
+ "strong",
5
+ "i",
6
+ "em",
7
+ "u",
8
+ "ins",
9
+ "s",
10
+ "strike",
11
+ "del",
12
+ "code",
13
+ "pre",
14
+ "a",
15
+ "blockquote",
16
+ "tg-spoiler",
17
+ "span",
18
+ "tg-emoji",
19
+ "tg-time"
20
+ ];
21
+ const TAG_SET = new Set(ALLOWED_TAGS);
22
+ function isAllowedTag(name) {
23
+ return TAG_SET.has(name);
24
+ }
25
+ /** Schemes allowed in `<a href>`. */
26
+ const SAFE_URL_RE = /^(?:https?:\/\/|tg:\/\/)/i;
27
+ const TIME_FORMAT_RE = /^(?:r|w?[dD]?[tT]?)$/;
28
+ const LANGUAGE_RE = /^language-[A-Za-z0-9_+#.-]{1,32}$/;
29
+ /** Literal text of an attribute, or null when it contains a variable. */
30
+ function literalValue(attr) {
31
+ if (attr.value === null) return "";
32
+ let out = "";
33
+ for (const p of attr.value) {
34
+ if (p.type !== "text") return null;
35
+ out += p.value;
36
+ }
37
+ return out;
38
+ }
39
+ const RULES = {
40
+ a: { href: {
41
+ required: true,
42
+ vars: true,
43
+ check: (v) => SAFE_URL_RE.test(v) ? null : "href must start with https://, http:// or tg://"
44
+ } },
45
+ blockquote: { expandable: { boolean: true } },
46
+ span: { class: {
47
+ required: true,
48
+ check: (v) => v === "tg-spoiler" ? null : "span needs class=\"tg-spoiler\""
49
+ } },
50
+ code: { class: { check: (v) => LANGUAGE_RE.test(v) ? null : "code class must be \"language-…\"" } },
51
+ "tg-emoji": { "emoji-id": {
52
+ required: true,
53
+ vars: true,
54
+ check: (v) => /^\d+$/.test(v) ? null : "emoji-id must be digits"
55
+ } },
56
+ "tg-time": {
57
+ unix: {
58
+ required: true,
59
+ vars: true,
60
+ check: (v) => /^\d+$/.test(v) ? null : "unix must be digits"
61
+ },
62
+ format: { check: (v) => TIME_FORMAT_RE.test(v) ? null : "format must match r|w?[dD]?[tT]?" }
63
+ }
64
+ };
65
+ /** Checks one element's attributes. Returns error messages (empty when valid). */
66
+ function checkAttrs(tag, attrs) {
67
+ const rules = RULES[tag] ?? {};
68
+ const errors = [];
69
+ const seen = /* @__PURE__ */ new Set();
70
+ for (const attr of attrs) {
71
+ const rule = rules[attr.name];
72
+ if (!rule) {
73
+ errors.push(`attribute "${attr.name}" is not allowed on <${tag}>`);
74
+ continue;
75
+ }
76
+ if (seen.has(attr.name)) errors.push(`duplicate attribute "${attr.name}"`);
77
+ seen.add(attr.name);
78
+ if (rule.boolean) {
79
+ if (attr.value !== null) errors.push(`"${attr.name}" takes no value`);
80
+ continue;
81
+ }
82
+ if (attr.value === null) {
83
+ errors.push(`"${attr.name}" needs a value`);
84
+ continue;
85
+ }
86
+ const literal = literalValue(attr);
87
+ if (literal === null) {
88
+ if (!rule.vars) errors.push(`"${attr.name}" cannot contain variables`);
89
+ const first = attr.value[0];
90
+ if (tag === "a" && first?.type === "text" && !SAFE_URL_RE.test(first.value)) errors.push("href must start with https://, http:// or tg://");
91
+ continue;
92
+ }
93
+ const msg = rule.check?.(literal);
94
+ if (msg) errors.push(msg);
95
+ }
96
+ for (const [name, rule] of Object.entries(rules)) if (rule.required && !seen.has(name)) errors.push(`<${tag}> needs "${name}"`);
97
+ return errors;
98
+ }
99
+ /** Values checked again after rendering (variables are allowed in these). */
100
+ function renderedAttrOk(tag, attr, value) {
101
+ if (tag === "a" && attr === "href") return SAFE_URL_RE.test(value);
102
+ if (tag === "tg-emoji" && attr === "emoji-id" || tag === "tg-time" && attr === "unix") return /^\d+$/.test(value);
103
+ return true;
104
+ }
105
+ export { ALLOWED_TAGS, SAFE_URL_RE, checkAttrs, isAllowedTag, literalValue, renderedAttrOk };
@@ -0,0 +1,10 @@
1
+ import { CoreTemplateError, TemplateAst } from "./ast.js";
2
+ import { FilterRegistry } from "./filters.js";
3
+ /** Options for {@link validateTemplate}. */
4
+ export interface ValidateOptions {
5
+ /** A Zod schema or a `Shape`. Without it only filters are checked. */
6
+ variables?: unknown;
7
+ filters?: FilterRegistry | undefined;
8
+ }
9
+ /** Checks a parsed template against its variables (unknown paths, type misuse) and filters (unknown names, arg counts). */
10
+ export declare function validateTemplate(ast: TemplateAst, opts?: ValidateOptions): CoreTemplateError[];
@@ -0,0 +1,144 @@
1
+ import { pathText } from "./ast.js";
2
+ import { resolveFilter } from "./filters.js";
3
+ import { zodToShape } from "./shape.js";
4
+ // src/core/template/validate.ts
5
+ const UNKNOWN = {
6
+ kind: "unknown",
7
+ optional: false
8
+ };
9
+ function isShape(v) {
10
+ return v !== null && typeof v === "object" && "kind" in v && "optional" in v && !("_zod" in v);
11
+ }
12
+ function resolve(path, stack) {
13
+ const top = stack[stack.length - 1] ?? UNKNOWN;
14
+ if (path.length === 0) return { shape: top };
15
+ const [head, ...rest] = path;
16
+ let shape;
17
+ for (let k = stack.length - 1; k >= 0 && head !== void 0; k--) {
18
+ const s = stack[k];
19
+ if (!s) continue;
20
+ if (s.kind === "object" && Object.hasOwn(s.fields, head)) {
21
+ shape = s.fields[head];
22
+ break;
23
+ }
24
+ if (s.kind === "record" || s.kind === "unknown") {
25
+ shape = UNKNOWN;
26
+ break;
27
+ }
28
+ }
29
+ if (!shape) return { error: `unknown variable "${head}"` };
30
+ let at = head ?? "";
31
+ for (const key of rest) {
32
+ if (shape.kind === "object") {
33
+ const next = Object.hasOwn(shape.fields, key) ? shape.fields[key] : void 0;
34
+ if (!next) return { error: `unknown variable "${at}.${key}"` };
35
+ shape = next;
36
+ } else if (shape.kind === "array" && key === "length") shape = {
37
+ kind: "number",
38
+ optional: false
39
+ };
40
+ else if (shape.kind === "record" || shape.kind === "unknown") shape = UNKNOWN;
41
+ else return { error: `"${at}" is a ${shape.kind} and has no field "${key}"` };
42
+ at += `.${key}`;
43
+ }
44
+ return { shape };
45
+ }
46
+ /** Checks a parsed template against its variables (unknown paths, type misuse) and filters (unknown names, arg counts). */
47
+ function validateTemplate(ast, opts = {}) {
48
+ const errors = [];
49
+ const root = opts.variables === void 0 ? UNKNOWN : isShape(opts.variables) ? opts.variables : zodToShape(opts.variables);
50
+ const checkVar = (node, stack) => {
51
+ const r = resolve(node.path, stack);
52
+ if ("error" in r) errors.push({
53
+ code: "variable_unknown",
54
+ message: r.error,
55
+ line: node.line,
56
+ column: node.column
57
+ });
58
+ else if (node.filters.length === 0 && r.shape.kind === "object") errors.push({
59
+ code: "variable_type",
60
+ message: `"${pathText(node.path)}" is an object; use {{#${pathText(node.path)}}}…{{/${pathText(node.path)}}}`,
61
+ line: node.line,
62
+ column: node.column
63
+ });
64
+ for (const f of node.filters) {
65
+ const def = resolveFilter(f.name, opts.filters);
66
+ if (!def) {
67
+ errors.push({
68
+ code: "filter_unknown",
69
+ message: `unknown filter "${f.name}"`,
70
+ line: f.line,
71
+ column: f.column
72
+ });
73
+ continue;
74
+ }
75
+ const min = def.minArgs ?? 0;
76
+ const max = def.maxArgs ?? Number.POSITIVE_INFINITY;
77
+ if (f.args.length < min || f.args.length > max) {
78
+ const want = min === max ? `${min}` : max === Number.POSITIVE_INFINITY ? `at least ${min}` : `${min}–${max}`;
79
+ errors.push({
80
+ code: "filter_args",
81
+ message: `filter "${f.name}" takes ${want} argument(s), got ${f.args.length}`,
82
+ line: f.line,
83
+ column: f.column
84
+ });
85
+ }
86
+ }
87
+ };
88
+ const walk = (list, stack) => {
89
+ for (const node of list) switch (node.type) {
90
+ case "text": break;
91
+ case "var":
92
+ checkVar(node, stack);
93
+ break;
94
+ case "element":
95
+ for (const a of node.attrs) for (const p of a.value ?? []) if (p.type === "var") checkVar(p, stack);
96
+ walk(node.children, stack);
97
+ break;
98
+ case "inverted": {
99
+ const r = resolve(node.path, stack);
100
+ if ("error" in r) errors.push({
101
+ code: "variable_unknown",
102
+ message: r.error,
103
+ line: node.line,
104
+ column: node.column
105
+ });
106
+ walk(node.children, stack);
107
+ break;
108
+ }
109
+ case "section": {
110
+ const r = resolve(node.path, stack);
111
+ if ("error" in r) {
112
+ errors.push({
113
+ code: "variable_unknown",
114
+ message: r.error,
115
+ line: node.line,
116
+ column: node.column
117
+ });
118
+ walk(node.children, [...stack, UNKNOWN]);
119
+ break;
120
+ }
121
+ const s = r.shape;
122
+ if (s.kind === "array") walk(node.children, [...stack, s.item]);
123
+ else if (s.kind === "boolean") walk(node.children, stack);
124
+ else if (s.kind === "object" || s.kind === "record" || s.kind === "unknown" || s.optional) walk(node.children, [...stack, {
125
+ ...s,
126
+ optional: false
127
+ }]);
128
+ else {
129
+ errors.push({
130
+ code: "section_invalid",
131
+ message: `{{#${pathText(node.path)}}} needs a list, an object, a boolean or an optional value (it is a ${s.kind})`,
132
+ line: node.line,
133
+ column: node.column
134
+ });
135
+ walk(node.children, [...stack, s]);
136
+ }
137
+ break;
138
+ }
139
+ }
140
+ };
141
+ walk(ast.children, [root]);
142
+ return errors;
143
+ }
144
+ export { validateTemplate };
@@ -0,0 +1,15 @@
1
+ import { TemplateAst } from "./ast.js";
2
+ /** One variable use in a template (1-based `line` / `column`). */
3
+ export interface VariableRef {
4
+ /** Path as written (`name`, `project.stage`, `.`). */
5
+ path: string;
6
+ kind: "var" | "section" | "inverted";
7
+ /** Enclosing section paths, outermost first (`["projects"]` for `{{name}}` inside `{{#projects}}`). */
8
+ scope: string[];
9
+ /** Filter names applied, in order. */
10
+ filters: string[];
11
+ line: number;
12
+ column: number;
13
+ }
14
+ /** Every variable and section a template reads, in source order. */
15
+ export declare function listVariables(ast: TemplateAst): VariableRef[];
@@ -0,0 +1,41 @@
1
+ import { pathText } from "./ast.js";
2
+ // src/core/template/variables.ts
3
+ /** Every variable and section a template reads, in source order. */
4
+ function listVariables(ast) {
5
+ const out = [];
6
+ const walk = (list, scope) => {
7
+ for (const node of list) if (node.type === "var") out.push({
8
+ path: pathText(node.path),
9
+ kind: "var",
10
+ scope,
11
+ filters: node.filters.map((f) => f.name),
12
+ line: node.line,
13
+ column: node.column
14
+ });
15
+ else if (node.type === "section" || node.type === "inverted") {
16
+ const path = pathText(node.path);
17
+ out.push({
18
+ path,
19
+ kind: node.type,
20
+ scope,
21
+ filters: [],
22
+ line: node.line,
23
+ column: node.column
24
+ });
25
+ walk(node.children, node.type === "section" ? [...scope, path] : scope);
26
+ } else if (node.type === "element") {
27
+ for (const a of node.attrs) for (const p of a.value ?? []) if (p.type === "var") out.push({
28
+ path: pathText(p.path),
29
+ kind: "var",
30
+ scope,
31
+ filters: p.filters.map((f) => f.name),
32
+ line: p.line,
33
+ column: p.column
34
+ });
35
+ walk(node.children, scope);
36
+ }
37
+ };
38
+ walk(ast.children, []);
39
+ return out;
40
+ }
41
+ export { listVariables };
@@ -0,0 +1,3 @@
1
+ import { BotDoHost, registerTelegramPlugin } from "../engine/outbox/do-registry.js";
2
+ import { BotObjectState, TelegramBotDO, createTelegramBotDO, doLease } from "./telegram-bot-do.js";
3
+ export { type BotDoHost, type BotObjectState, TelegramBotDO, createTelegramBotDO, doLease, registerTelegramPlugin };
@@ -0,0 +1,3 @@
1
+ import { registerTelegramPlugin } from "../engine/outbox/do-registry.js";
2
+ import { TelegramBotDO, createTelegramBotDO, doLease } from "./telegram-bot-do.js";
3
+ export { TelegramBotDO, createTelegramBotDO, doLease, registerTelegramPlugin };
@@ -0,0 +1,45 @@
1
+ import { Runtime } from "../engine/runtime.js";
2
+ import { BotDoHost } from "../engine/outbox/do-registry.js";
3
+ import { DoKickBody, DoWakeBody } from "../engine/outbox/do-transport.js";
4
+ import { DrainLease } from "../engine/outbox/drain.js";
5
+ /** What a {@link TelegramBotDO} persists in its storage: which bot it serves and its next alarm reasons. */
6
+ export interface BotObjectState {
7
+ tenant: string;
8
+ botId: string;
9
+ /** Next due delivery (epoch ms, plugin clock), null = nothing queued. */
10
+ dueAt: number | null;
11
+ /** Next scheduler wake, null = none. */
12
+ wakeAt: number | null;
13
+ }
14
+ /** The object is the lease: it only re-reads the bot row now and then to notice a pause, 401 or removal. */
15
+ export declare function doLease<A>(rt: Runtime<A>, botId: string): DrainLease;
16
+ /** Per-bot Durable Object: strict paced sender and per-bot scheduler. Export it from the Worker and bind it as `env.botDO`. */
17
+ export declare class TelegramBotDO<E = unknown> implements DurableObject {
18
+ #private;
19
+ protected readonly ctx: DurableObjectState;
20
+ protected readonly env: E;
21
+ constructor(ctx: DurableObjectState, env: E);
22
+ /** The plugin that owns this object. Default: the one `createTelegramPlugin()` registered for the tenant. */
23
+ protected plugin(tenant: string): BotDoHost;
24
+ /** Wall-clock alarm time for a delay. Override only in tests (to drive alarms by hand). */
25
+ protected alarmTime(delayMs: number): number;
26
+ /** Scheduler role: runs the per-bot wake stages; returns the next wake time. */
27
+ protected onWake(rt: Runtime<unknown>, botId: string, now: number): Promise<number | null>;
28
+ /** RPC over fetch: `POST /kick`, `POST /wake` (`{ tenant, botId, at, replace? }`), `GET /state`. Bad input → 400 JSON. */
29
+ fetch(request: Request): Promise<Response>;
30
+ /** Alarm handler: runs drain/wake cycles. */
31
+ alarm(): Promise<void>;
32
+ /** A drain is wanted now: arm the alarm at once (or loop again if a drain is running). */
33
+ kick(body: DoKickBody): Promise<void>;
34
+ /** Scheduler role: wake at `at` (earlier wakes win), or exactly at `at` / never (`replace`). */
35
+ scheduleWake(body: DoWakeBody): Promise<void>;
36
+ /** Runs drain cycles until nothing more was asked for. Concurrent callers share the running cycle. */
37
+ run(): Promise<void>;
38
+ /** The current state (tests and debugging). */
39
+ state(): Promise<BotObjectState | null>;
40
+ }
41
+ /**
42
+ * A TelegramBotDO bound to one plugin instance (explicit wiring, or several plugins per tenant):
43
+ * `export const TelegramBotDO = createTelegramBotDO(telegram);`
44
+ */
45
+ export declare function createTelegramBotDO<E>(plugin: BotDoHost): typeof TelegramBotDO<E>;
@@ -0,0 +1,219 @@
1
+ import { sql } from "../ports/sql.js";
2
+ import { TelegramConfigError } from "../engine/errors.js";
3
+ import { registeredTelegramPlugin } from "../engine/outbox/do-registry.js";
4
+ import { drainBot } from "../engine/outbox/drain.js";
5
+ import { runWake } from "../engine/outbox/wake.js";
6
+ // src/do/telegram-bot-do.ts
7
+ /** Longest the object waits in-loop for the next due row; anything later is an alarm. */
8
+ const DO_IDLE_WAIT_MS = 1500;
9
+ /** How often the DO lease re-reads the bot row (status, pause) during a drain. */
10
+ const BOT_REFRESH_MS = 5e3;
11
+ const STATE_KEY = "tg:state";
12
+ const minOf = (a, b) => a === null ? b : b === null ? a : Math.min(a, b);
13
+ /** The object is the lease: it only re-reads the bot row now and then to notice a pause, 401 or removal. */
14
+ function doLease(rt, botId) {
15
+ let bot = null;
16
+ let readAt = Number.NEGATIVE_INFINITY;
17
+ const read = async (now) => {
18
+ readAt = now;
19
+ bot = await rt.db.first(sql`SELECT * FROM tg_bot WHERE tenant = ${rt.tenant} AND id = ${botId} AND deleted_at IS NULL
20
+ AND status IN ('active', 'webhook_error') AND (paused_until IS NULL OR paused_until < ${now})`);
21
+ return bot;
22
+ };
23
+ return {
24
+ acquire: read,
25
+ renew: async (now) => now - readAt >= BOT_REFRESH_MS ? read(now) : bot,
26
+ release: async () => {}
27
+ };
28
+ }
29
+ function bodyOf(v) {
30
+ if (typeof v !== "object" || v === null) return null;
31
+ const o = v;
32
+ if (typeof o.tenant !== "string" || typeof o.botId !== "string" || o.botId === "") return null;
33
+ return {
34
+ tenant: o.tenant,
35
+ botId: o.botId
36
+ };
37
+ }
38
+ const json = (v, status = 200) => new Response(JSON.stringify(v), {
39
+ status,
40
+ headers: { "content-type": "application/json" }
41
+ });
42
+ /** Per-bot Durable Object: strict paced sender and per-bot scheduler. Export it from the Worker and bind it as `env.botDO`. */
43
+ var TelegramBotDO = class {
44
+ ctx;
45
+ env;
46
+ #state;
47
+ #running = null;
48
+ #again = false;
49
+ constructor(ctx, env) {
50
+ this.ctx = ctx;
51
+ this.env = env;
52
+ }
53
+ /** The plugin that owns this object. Default: the one `createTelegramPlugin()` registered for the tenant. */
54
+ plugin(tenant) {
55
+ const host = registeredTelegramPlugin(tenant);
56
+ if (!host) throw new TelegramConfigError("TelegramBotDO: no plugin registered for this tenant (create the plugin at module scope, or use createTelegramBotDO(plugin))");
57
+ return host;
58
+ }
59
+ /** Wall-clock alarm time for a delay. Override only in tests (to drive alarms by hand). */
60
+ alarmTime(delayMs) {
61
+ return Date.now() + Math.max(0, delayMs);
62
+ }
63
+ /** Scheduler role: runs the per-bot wake stages; returns the next wake time. */
64
+ onWake(rt, botId, now) {
65
+ return runWake(rt, botId, now);
66
+ }
67
+ /** RPC over fetch: `POST /kick`, `POST /wake` (`{ tenant, botId, at, replace? }`), `GET /state`. Bad input → 400 JSON. */
68
+ async fetch(request) {
69
+ const url = new URL(request.url);
70
+ if (request.method === "GET" && url.pathname === "/state") return json(await this.#load());
71
+ if (request.method !== "POST") return json({ error: "method" }, 405);
72
+ let raw;
73
+ try {
74
+ raw = await request.json();
75
+ } catch {
76
+ return json({ error: "body" }, 400);
77
+ }
78
+ const body = bodyOf(raw);
79
+ if (!body) return json({ error: "body" }, 400);
80
+ if (url.pathname === "/kick") {
81
+ await this.kick(body);
82
+ return json({ ok: true });
83
+ }
84
+ if (url.pathname === "/wake") {
85
+ const { at, replace } = raw;
86
+ if (!(typeof at === "number" && Number.isFinite(at)) && !(replace === true && at === null)) return json({ error: "at" }, 400);
87
+ await this.scheduleWake({
88
+ ...body,
89
+ at: at ?? null,
90
+ replace: replace === true
91
+ });
92
+ return json({ ok: true });
93
+ }
94
+ return json({ error: "not_found" }, 404);
95
+ }
96
+ /** Alarm handler: runs drain/wake cycles. */
97
+ async alarm() {
98
+ await this.run();
99
+ }
100
+ /** A drain is wanted now: arm the alarm at once (or loop again if a drain is running). */
101
+ async kick(body) {
102
+ const st = await this.#remember(body);
103
+ if (this.#running) {
104
+ this.#again = true;
105
+ return;
106
+ }
107
+ const rt = this.#runtime(st);
108
+ st.dueAt = rt.clock.now();
109
+ await this.#save(rt);
110
+ }
111
+ /** Scheduler role: wake at `at` (earlier wakes win), or exactly at `at` / never (`replace`). */
112
+ async scheduleWake(body) {
113
+ const st = await this.#remember(body);
114
+ st.wakeAt = body.replace === true ? body.at : minOf(st.wakeAt, body.at);
115
+ if (this.#running) {
116
+ this.#again = true;
117
+ return;
118
+ }
119
+ await this.#save(this.#runtime(st));
120
+ }
121
+ /** Runs drain cycles until nothing more was asked for. Concurrent callers share the running cycle. */
122
+ run() {
123
+ if (this.#running) {
124
+ this.#again = true;
125
+ return this.#running;
126
+ }
127
+ const loop = async () => {
128
+ try {
129
+ do {
130
+ this.#again = false;
131
+ await this.#cycle();
132
+ } while (this.#again);
133
+ } finally {
134
+ this.#running = null;
135
+ }
136
+ };
137
+ this.#running = loop();
138
+ return this.#running;
139
+ }
140
+ /** The current state (tests and debugging). */
141
+ async state() {
142
+ return this.#load();
143
+ }
144
+ async #cycle() {
145
+ const st = await this.#load();
146
+ if (!st) return;
147
+ const rt = this.#runtime(st);
148
+ const now = rt.clock.now();
149
+ if (st.wakeAt !== null && st.wakeAt <= now) {
150
+ st.wakeAt = null;
151
+ st.wakeAt = minOf(st.wakeAt, await this.onWake(rt, st.botId, now));
152
+ }
153
+ const r = await drainBot(rt, st.botId, {
154
+ lease: () => doLease(rt, st.botId),
155
+ strictPacing: true,
156
+ maxIdleWaitMs: DO_IDLE_WAIT_MS,
157
+ budgetMs: rt.limits.tickBudgetMs
158
+ });
159
+ st.dueAt = await this.#nextDue(rt, st.botId, r);
160
+ await this.#save(rt);
161
+ }
162
+ async #nextDue(rt, botId, r) {
163
+ const now = rt.clock.now();
164
+ switch (r.stoppedBy) {
165
+ case "budget":
166
+ case "cap": return now;
167
+ case "idle": return r.nextDueAt;
168
+ case "paused":
169
+ case "lease": {
170
+ if (r.nextDueAt === null) return null;
171
+ const bot = await rt.db.first(sql`SELECT paused_until FROM tg_bot WHERE tenant = ${rt.tenant} AND id = ${botId}
172
+ AND deleted_at IS NULL AND status IN ('active', 'webhook_error')`);
173
+ if (!bot) return null;
174
+ return Math.max(r.nextDueAt, bot.paused_until ?? 0, now);
175
+ }
176
+ default: return null;
177
+ }
178
+ }
179
+ #runtime(st) {
180
+ return this.plugin(st.tenant).runtime(this.env, { waitUntil: (p) => this.ctx.waitUntil(p) });
181
+ }
182
+ async #load() {
183
+ if (this.#state === void 0) this.#state = await this.ctx.storage.get(STATE_KEY) ?? null;
184
+ return this.#state;
185
+ }
186
+ async #remember(body) {
187
+ const st = await this.#load();
188
+ if (st && st.botId === body.botId && st.tenant === body.tenant) return st;
189
+ const fresh = {
190
+ tenant: body.tenant,
191
+ botId: body.botId,
192
+ dueAt: null,
193
+ wakeAt: null
194
+ };
195
+ this.#state = fresh;
196
+ return fresh;
197
+ }
198
+ /** Persists the state and arms the alarm at the earliest of `dueAt` / `wakeAt`. */
199
+ async #save(rt) {
200
+ const st = this.#state;
201
+ if (!st) return;
202
+ await this.ctx.storage.put(STATE_KEY, st);
203
+ const at = minOf(st.dueAt, st.wakeAt);
204
+ if (at === null) await this.ctx.storage.deleteAlarm();
205
+ else await this.ctx.storage.setAlarm(this.alarmTime(at - rt.clock.now()));
206
+ }
207
+ };
208
+ /**
209
+ * A TelegramBotDO bound to one plugin instance (explicit wiring, or several plugins per tenant):
210
+ * `export const TelegramBotDO = createTelegramBotDO(telegram);`
211
+ */
212
+ function createTelegramBotDO(plugin) {
213
+ return class BoundTelegramBotDO extends TelegramBotDO {
214
+ plugin() {
215
+ return plugin;
216
+ }
217
+ };
218
+ }
219
+ export { TelegramBotDO, createTelegramBotDO, doLease };