@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1,257 @@
1
+ import { isNotEmulated, notEmulated, statefulRoute } from "../stateful-emulator.mjs";
2
+ import { Predicate } from "effect";
3
+ //#region src/telegram/api.ts
4
+ /**
5
+ * Telegram Bot API emulator API: the route table (evidence, query allowlist, guarded path,
6
+ * handler) and the token-aware route resolution (internal; re-exported by `src/telegram.ts`, which
7
+ * runs the table on the shared stateful wrapper, `src/stateful-emulator.ts`, in its resolved
8
+ * mode).
9
+ *
10
+ * The bot token is part of every Bot API URL (`/bot<token>/<method>`, `/file/bot<token>/<path>`).
11
+ * It is required, but it never reaches the core, the state, the ledger, a not-emulated message, or
12
+ * a response. Resolution fails closed: only a request whose raw path is exactly an emulated route
13
+ * shape is recognised, and its token is taken from that exact segment; every other request is
14
+ * ledgered and answered with constant text only. For a recognised request the ledgered path reads
15
+ * `/bot<redacted>/...`, the routes learn only whether the token names a bot, and the wrapper
16
+ * scrubs the token and its secret part from the ledger and refuses a path, query, or body that
17
+ * repeats them (raw, percent-decoded, or in any parsed JSON key, string value, or number).
18
+ *
19
+ * Only the routes the four Telegram conformance cases need are emulated, with wire shapes copied
20
+ * from the synthetic fixtures; everything else answers the wrapper's ledgered 400 not-emulated.
21
+ *
22
+ * @experimental
23
+ */
24
+ /**
25
+ * The route parameter the resolution supplies instead of the token: whether the token names a bot
26
+ * (`known`) or not (`none`). The token itself never reaches a route.
27
+ */
28
+ const botParam = "bot";
29
+ const validateCase = "telegram.validate.get-chat";
30
+ const errorCase = "telegram.errors.error-envelope";
31
+ const fileCase = "telegram.files.get-file-path";
32
+ const sendCase = "telegram.messages.send-message";
33
+ /** Bot API JSON answers, as the fixtures record them. */
34
+ const json = (status, body) => new Response(JSON.stringify(body), {
35
+ status,
36
+ headers: { "content-type": "application/json" }
37
+ });
38
+ /** A chat id: an integer (negative for groups and channels) or a public `@username`. */
39
+ const chatIdPattern = /^(?:-?[1-9][0-9]{0,19}|@[A-Za-z][A-Za-z0-9_]{3,31})$/;
40
+ const isJsonObject = (value) => value !== void 0 && value !== null && Predicate.isObject(value) && !Array.isArray(value);
41
+ const mediaType = (contentType) => contentType?.split(";", 1)[0]?.trim().toLowerCase();
42
+ /**
43
+ * A JSON body with exactly the `keys` the fixture records, or a refusal. Refusals never name a
44
+ * request key or value (constant text).
45
+ */
46
+ const bodyObject = (request, keys) => {
47
+ if (mediaType(request.contentType) !== "application/json") return notEmulated("Bot API bodies other than application/json are not emulated");
48
+ if (!isJsonObject(request.body)) return notEmulated("a request body that is not a JSON object is not emulated");
49
+ const sent = Object.keys(request.body);
50
+ if (sent.some((key) => !keys.includes(key))) return notEmulated("a body field this method does not take is not emulated");
51
+ return keys.every((key) => sent.includes(key)) ? request.body : notEmulated(`requests without each of the recorded body fields (${keys.join(", ")}) are not emulated`);
52
+ };
53
+ const noBody = (request) => request.hasBody ? notEmulated("a request body is not emulated on this method") : void 0;
54
+ /** The chat a well-formed `chat_id` names, `null` for none, or a refusal. */
55
+ const chatOf = (state, chatId) => {
56
+ if (!Predicate.isString(chatId) || !chatIdPattern.test(chatId)) return notEmulated("chat_id must be a chat id string (an integer or a public @username)");
57
+ return state.chats.find((chat) => String(chat.id) === chatId) ?? null;
58
+ };
59
+ /** The Bot API error envelope of the error fixture. */
60
+ const botApiError = (env, status, description) => json(env.drills.errorsAs200 ? 200 : status, {
61
+ ok: false,
62
+ error_code: status,
63
+ description
64
+ });
65
+ const knownBotOnly = (request) => request.knownBot ? void 0 : notEmulated("a bot token that names no bot is emulated only on getChat (as recorded)");
66
+ const getChat = (state, request, env) => {
67
+ const body = bodyObject(request, ["chat_id"]);
68
+ if (isNotEmulated(body)) return body;
69
+ const chat = chatOf(state, body.chat_id);
70
+ if (isNotEmulated(chat)) return chat;
71
+ if (!request.knownBot) return () => botApiError(env, 401, "Unauthorized");
72
+ if (chat === null) return () => botApiError(env, 400, "Bad Request: chat not found");
73
+ return () => json(200, {
74
+ ok: !env.drills.getChatOkFalse,
75
+ result: {
76
+ id: chat.id,
77
+ title: chat.title,
78
+ type: chat.type,
79
+ permissions: { can_send_messages: chat.permissions.can_send_messages },
80
+ accent_color_id: chat.accent_color_id,
81
+ max_reaction_count: chat.max_reaction_count
82
+ }
83
+ });
84
+ };
85
+ const getFile = (state, request, env) => {
86
+ const refused = noBody(request) ?? knownBotOnly(request);
87
+ if (refused !== void 0) return refused;
88
+ const fileId = request.query.get("file_id");
89
+ const file = state.files.find((item) => item.file_id === fileId);
90
+ if (file === void 0) return notEmulated("getFile of a file id the bot did not receive is not emulated");
91
+ return () => json(200, {
92
+ ok: true,
93
+ result: {
94
+ file_id: file.file_id,
95
+ file_unique_id: file.file_unique_id,
96
+ file_size: file.file_size + (env.drills.fileSizeOffByOne ? 1 : 0),
97
+ file_path: file.file_path
98
+ }
99
+ });
100
+ };
101
+ const downloadFile = (state, request) => {
102
+ const refused = noBody(request) ?? knownBotOnly(request);
103
+ if (refused !== void 0) return refused;
104
+ const file = state.files.find((item) => item.file_path === request.filePath);
105
+ if (file === void 0) return notEmulated("a file path getFile did not answer is not emulated");
106
+ return () => new Response(new TextEncoder().encode(file.content), {
107
+ status: 200,
108
+ headers: { "content-type": "application/octet-stream" }
109
+ });
110
+ };
111
+ const sendMessage = (state, request, env) => {
112
+ const refused = knownBotOnly(request);
113
+ if (refused !== void 0) return refused;
114
+ const body = bodyObject(request, [
115
+ "chat_id",
116
+ "text",
117
+ "disable_web_page_preview"
118
+ ]);
119
+ if (isNotEmulated(body)) return body;
120
+ const chat = chatOf(state, body.chat_id);
121
+ if (isNotEmulated(chat)) return chat;
122
+ if (chat === null) return notEmulated("sendMessage to a chat the bot is not a member of is not emulated");
123
+ const { text, disable_web_page_preview: disablePreview } = body;
124
+ if (!Predicate.isString(text) || text === "") return notEmulated("text must be a non-empty string");
125
+ if (disablePreview !== true) return notEmulated("disable_web_page_preview other than true (as recorded) is not emulated");
126
+ return () => {
127
+ const now = env.now();
128
+ if (!Number.isFinite(now)) throw new Error("the emulator clock is not a finite instant");
129
+ const date = Math.floor(now / 1e3);
130
+ const messageId = state.counters.nextMessageId;
131
+ state.counters = {
132
+ ...state.counters,
133
+ nextMessageId: messageId + 1
134
+ };
135
+ state.sentMessages = [...state.sentMessages, {
136
+ message_id: messageId,
137
+ chat_id: chat.id,
138
+ text,
139
+ date,
140
+ disable_web_page_preview: true
141
+ }];
142
+ return json(200, {
143
+ ok: !env.drills.sendOkFalse,
144
+ result: {
145
+ message_id: messageId,
146
+ from: {
147
+ id: state.bot.id,
148
+ is_bot: true,
149
+ first_name: state.bot.first_name,
150
+ username: state.bot.username
151
+ },
152
+ chat: {
153
+ id: chat.id,
154
+ title: chat.title,
155
+ type: chat.type
156
+ },
157
+ date,
158
+ text
159
+ }
160
+ });
161
+ };
162
+ };
163
+ /** The route's request, as the handlers see it (the wrapper checked and parsed the body). */
164
+ const routeRequest = (request) => ({
165
+ query: request.query,
166
+ filePath: request.params.filePath,
167
+ body: request.json,
168
+ hasBody: request.json !== void 0,
169
+ contentType: request.header("content-type") ?? null,
170
+ knownBot: request.params[botParam] === "known"
171
+ });
172
+ const route = (method, path, guardedPath, write, caseIds, handler, queryKeys = []) => ({
173
+ route: statefulRoute({
174
+ method,
175
+ path,
176
+ kind: "connector",
177
+ write,
178
+ caseIds,
179
+ evidence: "unverified"
180
+ }, "json-or-empty", routeRequest, (state, request, context) => handler(state, request, context.env)),
181
+ queryKeys,
182
+ guardedPath
183
+ });
184
+ /** The route table: evidence plus handler. `telegramEmulatorRoutes` is its evidence part. */
185
+ const telegramApiRoutes = [
186
+ route("POST", "/bot{token}/getChat", "/getChat", false, [validateCase, errorCase], getChat),
187
+ route("GET", "/bot{token}/getFile", "/getFile", false, [fileCase], getFile, ["file_id"]),
188
+ route("GET", "/file/bot{token}/{filePath}", "/file", false, [fileCase], downloadFile),
189
+ route("POST", "/bot{token}/sendMessage", "/sendMessage", true, [sendCase], sendMessage)
190
+ ];
191
+ /**
192
+ * A Bot API token as the connector sends it: `<bot id>:<secret>`, with a secret of at least 8
193
+ * characters, so the secret part of every accepted token is long enough to be guarded on its own.
194
+ * Matched on the RAW path segment: a percent-encoded character makes the request unrecognised.
195
+ */
196
+ const tokenSource = "[0-9]+:[A-Za-z0-9_-]{8,}";
197
+ /** `/bot<token>/<method>`, exactly: one of the emulated Bot API methods, no other path text. */
198
+ const botPathPattern = new RegExp(`^/bot(${tokenSource})/(getChat|getFile|sendMessage)$`);
199
+ /**
200
+ * `/file/bot<token>/<file_path>`, exactly: a relative file path of letters, digits, and `_ . -`
201
+ * segments (no percent-encoding, no empty or dot segments), the shape the download helper sends.
202
+ */
203
+ const filePathPattern = new RegExp(`^/file/bot(${tokenSource})/((?:[A-Za-z0-9_.-]+/)*[A-Za-z0-9_.-]+)$`);
204
+ const isPlainFilePath = (path) => path.split("/").every((part) => part !== "." && part !== "..");
205
+ /** The constant reason of every unrecognised request (no method, path, or query is quoted). */
206
+ const unrecognisedReason = "no emulated Bot API route for this method and path";
207
+ /**
208
+ * Resolve one Bot API request, failing closed. Only a request whose raw path is exactly an
209
+ * emulated route shape (`/bot<token>/<method>` or `/file/bot<token>/<file_path>`, the strict token
210
+ * on the raw segment, an emulated method, and no other path text) under that route's HTTP method
211
+ * is recognised; the token is extracted from that exact segment, never searched for elsewhere.
212
+ * Every other request is `unrecognised`: the wrapper ledgers and answers it with constant text
213
+ * only, so nothing it carries can leak.
214
+ */
215
+ const resolveTelegramRequest = (request, url) => {
216
+ const method = request.method.toUpperCase();
217
+ const fileMatch = filePathPattern.exec(url.pathname);
218
+ const botMatch = fileMatch === null ? botPathPattern.exec(url.pathname) : null;
219
+ const token = fileMatch?.[1] ?? botMatch?.[1];
220
+ const template = fileMatch !== null ? "/file/bot{token}/{filePath}" : botMatch === null ? void 0 : `/bot{token}/${botMatch[2] ?? ""}`;
221
+ const matched = telegramApiRoutes.find((candidate) => candidate.route.method === method && candidate.route.path === template);
222
+ const filePath = fileMatch?.[2];
223
+ if (matched === void 0 || token === void 0 || filePath !== void 0 && !isPlainFilePath(filePath)) return {
224
+ kind: "unrecognised",
225
+ reason: unrecognisedReason
226
+ };
227
+ const secrets = [token, token.slice(token.indexOf(":") + 1)];
228
+ const ledgerPath = filePath === void 0 ? `/bot<redacted>/${botMatch?.[2] ?? ""}` : `/file/bot<redacted>/${filePath}`;
229
+ const refuse = (reason) => ({
230
+ kind: "refused",
231
+ ledgerPath,
232
+ reason,
233
+ route: matched.route,
234
+ secrets
235
+ });
236
+ const keys = [...url.searchParams.keys()];
237
+ if (keys.some((key) => !matched.queryKeys.includes(key))) return refuse("a query parameter this method does not take is not emulated");
238
+ const missing = matched.queryKeys.find((key) => !keys.includes(key));
239
+ if (missing !== void 0) return refuse(`requests without the query parameter ${missing} are not emulated`);
240
+ if (keys.length !== matched.queryKeys.length) return refuse("repeated query parameters are not emulated");
241
+ const bot = /^0+:/.test(token) ? "none" : "known";
242
+ return {
243
+ kind: "route",
244
+ ledgerPath,
245
+ route: matched.route,
246
+ params: filePath === void 0 ? { [botParam]: bot } : {
247
+ [botParam]: bot,
248
+ filePath
249
+ },
250
+ guardedPath: filePath === void 0 ? `${matched.guardedPath}${url.search}` : `${matched.guardedPath}/${encodeURIComponent(filePath)}`,
251
+ secrets
252
+ };
253
+ };
254
+ //#endregion
255
+ export { resolveTelegramRequest, telegramApiRoutes };
256
+
257
+ //# sourceMappingURL=api.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/telegram/api.ts"],"sourcesContent":["/**\n * Telegram Bot API emulator API: the route table (evidence, query allowlist, guarded path,\n * handler) and the token-aware route resolution (internal; re-exported by `src/telegram.ts`, which\n * runs the table on the shared stateful wrapper, `src/stateful-emulator.ts`, in its resolved\n * mode).\n *\n * The bot token is part of every Bot API URL (`/bot<token>/<method>`, `/file/bot<token>/<path>`).\n * It is required, but it never reaches the core, the state, the ledger, a not-emulated message, or\n * a response. Resolution fails closed: only a request whose raw path is exactly an emulated route\n * shape is recognised, and its token is taken from that exact segment; every other request is\n * ledgered and answered with constant text only. For a recognised request the ledgered path reads\n * `/bot<redacted>/...`, the routes learn only whether the token names a bot, and the wrapper\n * scrubs the token and its secret part from the ledger and refuses a path, query, or body that\n * repeats them (raw, percent-decoded, or in any parsed JSON key, string value, or number).\n *\n * Only the routes the four Telegram conformance cases need are emulated, with wire shapes copied\n * from the synthetic fixtures; everything else answers the wrapper's ledgered 400 not-emulated.\n *\n * @experimental\n */\nimport { Predicate } from 'effect'\nimport type * as Schema from 'effect/Schema'\nimport {\n isNotEmulated,\n notEmulated,\n statefulRoute,\n type Commit,\n type EmulatedRequest,\n type NotEmulated,\n type StatefulResolution,\n type StatefulRoute\n} from '../stateful-emulator.ts'\nimport type { TelegramEmulatorChat, TelegramEmulatorState } from './state.ts'\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type TelegramEmulatorDrills = {\n /** `true`: `getChat` of a member chat answers 200 with `ok: false`. */\n readonly getChatOkFalse?: boolean\n /** `true`: the `getChat` errors (absent chat, token naming no bot) answer 200, not 4xx. */\n readonly errorsAs200?: boolean\n /** `true`: `getFile` reports a `file_size` one byte larger than the file. */\n readonly fileSizeOffByOne?: boolean\n /** `true`: `sendMessage` answers 200 with `ok: false` (the message is still recorded as sent). */\n readonly sendOkFalse?: boolean\n}\n\nexport type TelegramApiEnv = {\n /** Clock in epoch milliseconds (`date` of sent messages). */\n readonly now: () => number\n readonly drills: Required<TelegramEmulatorDrills>\n}\n\n/**\n * The route parameter the resolution supplies instead of the token: whether the token names a bot\n * (`known`) or not (`none`). The token itself never reaches a route.\n */\nconst botParam = 'bot'\n\ntype RouteRequest = {\n readonly query: URLSearchParams\n /** Decoded file path of the file route. */\n readonly filePath: string | undefined\n readonly body: Schema.Json | undefined\n readonly hasBody: boolean\n readonly contentType: string | null\n /** The token names a bot (its bot id is not 0). */\n readonly knownBot: boolean\n}\n\n/** A route handler (the wrapper's plan): validate without writing, then return the commit. */\ntype RouteHandler = (\n state: TelegramEmulatorState,\n request: RouteRequest,\n env: TelegramApiEnv\n) => Commit | NotEmulated\n\ntype TelegramApiRoute = {\n readonly route: StatefulRoute<TelegramEmulatorState, TelegramApiEnv>\n readonly queryKeys: ReadonlyArray<string>\n /** The token-free path the request must not repeat the token in (the method, or `/file`). */\n readonly guardedPath: string\n}\n\nconst validateCase = 'telegram.validate.get-chat'\n\nconst errorCase = 'telegram.errors.error-envelope'\n\nconst fileCase = 'telegram.files.get-file-path'\n\nconst sendCase = 'telegram.messages.send-message'\n\n/** Bot API JSON answers, as the fixtures record them. */\nconst json = (status: number, body: unknown): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' }\n })\n\n/** A chat id: an integer (negative for groups and channels) or a public `@username`. */\nconst chatIdPattern = /^(?:-?[1-9][0-9]{0,19}|@[A-Za-z][A-Za-z0-9_]{3,31})$/\n\nconst isJsonObject = (value: Schema.Json | undefined): value is Schema.JsonObject =>\n value !== undefined && value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\nconst mediaType = (contentType: string | null): string | undefined =>\n contentType?.split(';', 1)[0]?.trim().toLowerCase()\n\n/**\n * A JSON body with exactly the `keys` the fixture records, or a refusal. Refusals never name a\n * request key or value (constant text).\n */\nconst bodyObject = (\n request: RouteRequest,\n keys: ReadonlyArray<string>\n): Schema.JsonObject | NotEmulated => {\n if (mediaType(request.contentType) !== 'application/json') {\n return notEmulated('Bot API bodies other than application/json are not emulated')\n }\n\n if (!isJsonObject(request.body)) {\n return notEmulated('a request body that is not a JSON object is not emulated')\n }\n\n const sent = Object.keys(request.body)\n\n if (sent.some(key => !keys.includes(key))) {\n return notEmulated('a body field this method does not take is not emulated')\n }\n\n return keys.every(key => sent.includes(key))\n ? request.body\n : notEmulated(\n `requests without each of the recorded body fields (${keys.join(', ')}) are not emulated`\n )\n}\n\nconst noBody = (request: RouteRequest): NotEmulated | undefined =>\n request.hasBody ? notEmulated('a request body is not emulated on this method') : undefined\n\n/** The chat a well-formed `chat_id` names, `null` for none, or a refusal. */\nconst chatOf = (\n state: TelegramEmulatorState,\n chatId: Schema.Json | undefined\n): TelegramEmulatorChat | null | NotEmulated => {\n if (!Predicate.isString(chatId) || !chatIdPattern.test(chatId)) {\n return notEmulated('chat_id must be a chat id string (an integer or a public @username)')\n }\n\n return state.chats.find(chat => String(chat.id) === chatId) ?? null\n}\n\n/** The Bot API error envelope of the error fixture. */\nconst botApiError = (env: TelegramApiEnv, status: number, description: string): Response =>\n json(env.drills.errorsAs200 ? 200 : status, { ok: false, error_code: status, description })\n\nconst knownBotOnly = (request: RouteRequest): NotEmulated | undefined =>\n request.knownBot\n ? undefined\n : notEmulated('a bot token that names no bot is emulated only on getChat (as recorded)')\n\n// Handlers: validate (not emulated) first, then return the commit that writes.\n\nconst getChat: RouteHandler = (state, request, env) => {\n const body = bodyObject(request, ['chat_id'])\n\n if (isNotEmulated(body)) return body\n\n const chat = chatOf(state, body.chat_id)\n\n if (isNotEmulated(chat)) return chat\n\n if (!request.knownBot) return () => botApiError(env, 401, 'Unauthorized')\n\n if (chat === null) return () => botApiError(env, 400, 'Bad Request: chat not found')\n\n return () =>\n json(200, {\n ok: !env.drills.getChatOkFalse,\n result: {\n id: chat.id,\n title: chat.title,\n type: chat.type,\n permissions: { can_send_messages: chat.permissions.can_send_messages },\n accent_color_id: chat.accent_color_id,\n max_reaction_count: chat.max_reaction_count\n }\n })\n}\n\nconst getFile: RouteHandler = (state, request, env) => {\n const refused = noBody(request) ?? knownBotOnly(request)\n\n if (refused !== undefined) return refused\n\n const fileId = request.query.get('file_id')\n const file = state.files.find(item => item.file_id === fileId)\n\n if (file === undefined) {\n return notEmulated('getFile of a file id the bot did not receive is not emulated')\n }\n\n return () =>\n json(200, {\n ok: true,\n result: {\n file_id: file.file_id,\n file_unique_id: file.file_unique_id,\n file_size: file.file_size + (env.drills.fileSizeOffByOne ? 1 : 0),\n file_path: file.file_path\n }\n })\n}\n\nconst downloadFile: RouteHandler = (state, request) => {\n const refused = noBody(request) ?? knownBotOnly(request)\n\n if (refused !== undefined) return refused\n\n const file = state.files.find(item => item.file_path === request.filePath)\n\n if (file === undefined) return notEmulated('a file path getFile did not answer is not emulated')\n\n return () =>\n new Response(new TextEncoder().encode(file.content), {\n status: 200,\n headers: { 'content-type': 'application/octet-stream' }\n })\n}\n\nconst sendMessage: RouteHandler = (state, request, env) => {\n const refused = knownBotOnly(request)\n\n if (refused !== undefined) return refused\n\n const body = bodyObject(request, ['chat_id', 'text', 'disable_web_page_preview'])\n\n if (isNotEmulated(body)) return body\n\n const chat = chatOf(state, body.chat_id)\n\n if (isNotEmulated(chat)) return chat\n\n if (chat === null) {\n return notEmulated('sendMessage to a chat the bot is not a member of is not emulated')\n }\n\n const { text, disable_web_page_preview: disablePreview } = body\n\n if (!Predicate.isString(text) || text === '') {\n return notEmulated('text must be a non-empty string')\n }\n\n if (disablePreview !== true) {\n return notEmulated('disable_web_page_preview other than true (as recorded) is not emulated')\n }\n\n return () => {\n // Read the clock before anything is written: a failing clock sends nothing.\n const now = env.now()\n\n if (!Number.isFinite(now)) throw new Error('the emulator clock is not a finite instant')\n\n const date = Math.floor(now / 1000)\n const messageId = state.counters.nextMessageId\n\n state.counters = { ...state.counters, nextMessageId: messageId + 1 }\n state.sentMessages = [\n ...state.sentMessages,\n { message_id: messageId, chat_id: chat.id, text, date, disable_web_page_preview: true }\n ]\n\n return json(200, {\n ok: !env.drills.sendOkFalse,\n result: {\n message_id: messageId,\n from: {\n id: state.bot.id,\n is_bot: true,\n first_name: state.bot.first_name,\n username: state.bot.username\n },\n chat: { id: chat.id, title: chat.title, type: chat.type },\n date,\n text\n }\n })\n }\n}\n\n/** The route's request, as the handlers see it (the wrapper checked and parsed the body). */\nconst routeRequest = (request: EmulatedRequest): RouteRequest => ({\n query: request.query,\n filePath: request.params.filePath,\n body: request.json,\n // A non-empty body is valid JSON by now (`json-or-empty`), so it always parses.\n hasBody: request.json !== undefined,\n contentType: request.header('content-type') ?? null,\n knownBot: request.params[botParam] === 'known'\n})\n\nconst route = (\n method: string,\n path: string,\n guardedPath: string,\n write: boolean,\n caseIds: ReadonlyArray<string>,\n handler: RouteHandler,\n queryKeys: ReadonlyArray<string> = []\n): TelegramApiRoute => ({\n route: statefulRoute(\n { method, path, kind: 'connector', write, caseIds, evidence: 'unverified' },\n 'json-or-empty',\n routeRequest,\n (state, request, context) => handler(state, request, context.env)\n ),\n queryKeys,\n guardedPath\n})\n\n/** The route table: evidence plus handler. `telegramEmulatorRoutes` is its evidence part. */\nexport const telegramApiRoutes: ReadonlyArray<TelegramApiRoute> = [\n route('POST', '/bot{token}/getChat', '/getChat', false, [validateCase, errorCase], getChat),\n route('GET', '/bot{token}/getFile', '/getFile', false, [fileCase], getFile, ['file_id']),\n route('GET', '/file/bot{token}/{filePath}', '/file', false, [fileCase], downloadFile),\n route('POST', '/bot{token}/sendMessage', '/sendMessage', true, [sendCase], sendMessage)\n]\n\n/**\n * A Bot API token as the connector sends it: `<bot id>:<secret>`, with a secret of at least 8\n * characters, so the secret part of every accepted token is long enough to be guarded on its own.\n * Matched on the RAW path segment: a percent-encoded character makes the request unrecognised.\n */\nconst tokenSource = '[0-9]+:[A-Za-z0-9_-]{8,}'\n\n/** `/bot<token>/<method>`, exactly: one of the emulated Bot API methods, no other path text. */\nconst botPathPattern = new RegExp(`^/bot(${tokenSource})/(getChat|getFile|sendMessage)$`)\n\n/**\n * `/file/bot<token>/<file_path>`, exactly: a relative file path of letters, digits, and `_ . -`\n * segments (no percent-encoding, no empty or dot segments), the shape the download helper sends.\n */\nconst filePathPattern = new RegExp(\n `^/file/bot(${tokenSource})/((?:[A-Za-z0-9_.-]+/)*[A-Za-z0-9_.-]+)$`\n)\n\nconst isPlainFilePath = (path: string): boolean =>\n path.split('/').every(part => part !== '.' && part !== '..')\n\n/** The constant reason of every unrecognised request (no method, path, or query is quoted). */\nconst unrecognisedReason = 'no emulated Bot API route for this method and path'\n\n/**\n * Resolve one Bot API request, failing closed. Only a request whose raw path is exactly an\n * emulated route shape (`/bot<token>/<method>` or `/file/bot<token>/<file_path>`, the strict token\n * on the raw segment, an emulated method, and no other path text) under that route's HTTP method\n * is recognised; the token is extracted from that exact segment, never searched for elsewhere.\n * Every other request is `unrecognised`: the wrapper ledgers and answers it with constant text\n * only, so nothing it carries can leak.\n */\nexport const resolveTelegramRequest = (\n request: Request,\n url: URL\n): StatefulResolution<TelegramEmulatorState, TelegramApiEnv> => {\n const method = request.method.toUpperCase()\n const fileMatch = filePathPattern.exec(url.pathname)\n const botMatch = fileMatch === null ? botPathPattern.exec(url.pathname) : null\n const token = fileMatch?.[1] ?? botMatch?.[1]\n\n const template =\n fileMatch !== null\n ? '/file/bot{token}/{filePath}'\n : botMatch === null\n ? undefined\n : `/bot{token}/${botMatch[2] ?? ''}`\n\n const matched = telegramApiRoutes.find(\n candidate => candidate.route.method === method && candidate.route.path === template\n )\n\n const filePath = fileMatch?.[2]\n\n if (\n matched === undefined ||\n token === undefined ||\n (filePath !== undefined && !isPlainFilePath(filePath))\n ) {\n return { kind: 'unrecognised', reason: unrecognisedReason }\n }\n\n // The guarded values: the token and its secret part (plain text: the segment is unencoded).\n const secrets = [token, token.slice(token.indexOf(':') + 1)]\n\n const ledgerPath =\n filePath === undefined\n ? `/bot<redacted>/${botMatch?.[2] ?? ''}`\n : `/file/bot<redacted>/${filePath}`\n\n const refuse = (reason: string): StatefulResolution<TelegramEmulatorState, TelegramApiEnv> => ({\n kind: 'refused',\n ledgerPath,\n reason,\n route: matched.route,\n secrets\n })\n\n const keys = [...url.searchParams.keys()]\n\n // Constant text: a query key may carry anything, the token included.\n if (keys.some(key => !matched.queryKeys.includes(key))) {\n return refuse('a query parameter this method does not take is not emulated')\n }\n\n const missing = matched.queryKeys.find(key => !keys.includes(key))\n\n if (missing !== undefined) {\n return refuse(`requests without the query parameter ${missing} are not emulated`)\n }\n\n if (keys.length !== matched.queryKeys.length) {\n return refuse('repeated query parameters are not emulated')\n }\n\n // Routes see whether the token names a bot and the file path, never the token.\n const bot = /^0+:/.test(token) ? 'none' : 'known'\n\n return {\n kind: 'route',\n ledgerPath,\n route: matched.route,\n params: filePath === undefined ? { [botParam]: bot } : { [botParam]: bot, filePath },\n guardedPath:\n filePath === undefined\n ? `${matched.guardedPath}${url.search}`\n : `${matched.guardedPath}/${encodeURIComponent(filePath)}`,\n secrets\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ //#region src/telegram/state.d.ts
4
+ /** The bot every token with a non-zero bot id answers as (`from` of sent messages). */
5
+ declare const TelegramEmulatorBot: Schema.Struct<{
6
+ readonly id: Schema.Int;
7
+ readonly first_name: Schema.String;
8
+ readonly username: Schema.String;
9
+ }>;
10
+ type TelegramEmulatorBot = typeof TelegramEmulatorBot.Type;
11
+ /** A chat the bot is a member of, in the `getChat` fixture's wire shape. */
12
+ declare const TelegramEmulatorChat: Schema.Struct<{
13
+ readonly id: Schema.Int;
14
+ readonly title: Schema.String;
15
+ readonly type: Schema.String;
16
+ readonly permissions: Schema.Struct<{
17
+ readonly can_send_messages: Schema.Boolean;
18
+ }>;
19
+ readonly accent_color_id: Schema.Int;
20
+ readonly max_reaction_count: Schema.Int;
21
+ }>;
22
+ type TelegramEmulatorChat = typeof TelegramEmulatorChat.Type;
23
+ /** A file the bot received: its `getFile` answer and its UTF-8 text content. */
24
+ declare const TelegramEmulatorFile: Schema.Struct<{
25
+ readonly file_id: Schema.String;
26
+ readonly file_unique_id: Schema.String;
27
+ readonly file_size: Schema.Int;
28
+ readonly file_path: Schema.String; /** UTF-8 text; `file_size` must equal its byte length. */
29
+ readonly content: Schema.String;
30
+ }>;
31
+ type TelegramEmulatorFile = typeof TelegramEmulatorFile.Type;
32
+ /** A message the bot sent (irreversible: kept until reset or seed). */
33
+ declare const TelegramEmulatorSentMessage: Schema.Struct<{
34
+ readonly message_id: Schema.Int;
35
+ readonly chat_id: Schema.Int;
36
+ readonly text: Schema.String;
37
+ readonly date: Schema.Int;
38
+ readonly disable_web_page_preview: Schema.Boolean;
39
+ }>;
40
+ type TelegramEmulatorSentMessage = typeof TelegramEmulatorSentMessage.Type;
41
+ declare const Counters: Schema.Struct<{
42
+ /** Next `message_id` of a sent message. */readonly nextMessageId: Schema.Int;
43
+ }>;
44
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
45
+ declare const TelegramEmulatorStateSchema: Schema.Struct<{
46
+ readonly bot: Schema.Struct<{
47
+ readonly id: Schema.Int;
48
+ readonly first_name: Schema.String;
49
+ readonly username: Schema.String;
50
+ }>;
51
+ readonly chats: Schema.$Array<Schema.Struct<{
52
+ readonly id: Schema.Int;
53
+ readonly title: Schema.String;
54
+ readonly type: Schema.String;
55
+ readonly permissions: Schema.Struct<{
56
+ readonly can_send_messages: Schema.Boolean;
57
+ }>;
58
+ readonly accent_color_id: Schema.Int;
59
+ readonly max_reaction_count: Schema.Int;
60
+ }>>;
61
+ readonly files: Schema.$Array<Schema.Struct<{
62
+ readonly file_id: Schema.String;
63
+ readonly file_unique_id: Schema.String;
64
+ readonly file_size: Schema.Int;
65
+ readonly file_path: Schema.String; /** UTF-8 text; `file_size` must equal its byte length. */
66
+ readonly content: Schema.String;
67
+ }>>;
68
+ readonly sentMessages: Schema.$Array<Schema.Struct<{
69
+ readonly message_id: Schema.Int;
70
+ readonly chat_id: Schema.Int;
71
+ readonly text: Schema.String;
72
+ readonly date: Schema.Int;
73
+ readonly disable_web_page_preview: Schema.Boolean;
74
+ }>>;
75
+ readonly counters: Schema.Struct<{
76
+ /** Next `message_id` of a sent message. */readonly nextMessageId: Schema.Int;
77
+ }>;
78
+ }>;
79
+ /** The emulator state. The container is mutable; entities are replaced, never edited in place. */
80
+ type TelegramEmulatorState = {
81
+ bot: TelegramEmulatorBot;
82
+ chats: ReadonlyArray<TelegramEmulatorChat>;
83
+ files: ReadonlyArray<TelegramEmulatorFile>;
84
+ sentMessages: ReadonlyArray<TelegramEmulatorSentMessage>;
85
+ counters: typeof Counters.Type;
86
+ };
87
+ /** Account-variance profiles for the default seed. */
88
+ declare const TelegramEmulatorProfile: Schema.Literals<readonly ["default", "empty"]>;
89
+ type TelegramEmulatorProfile = typeof TelegramEmulatorProfile.Type;
90
+ /**
91
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entities; `'empty'` keeps
92
+ * only the bot); every other key, when given, replaces that part of the profile.
93
+ * `nextMessageId` defaults to 101 (the send fixture's `message_id`).
94
+ */
95
+ declare const TelegramEmulatorSeed: Schema.Struct<{
96
+ readonly profile: Schema.optionalKey<Schema.Literals<readonly ["default", "empty"]>>;
97
+ readonly bot: Schema.optionalKey<Schema.Struct<{
98
+ readonly id: Schema.Int;
99
+ readonly first_name: Schema.String;
100
+ readonly username: Schema.String;
101
+ }>>;
102
+ readonly chats: Schema.optionalKey<Schema.$Array<Schema.Struct<{
103
+ readonly id: Schema.Int;
104
+ readonly title: Schema.String;
105
+ readonly type: Schema.String;
106
+ readonly permissions: Schema.Struct<{
107
+ readonly can_send_messages: Schema.Boolean;
108
+ }>;
109
+ readonly accent_color_id: Schema.Int;
110
+ readonly max_reaction_count: Schema.Int;
111
+ }>>>;
112
+ readonly files: Schema.optionalKey<Schema.$Array<Schema.Struct<{
113
+ readonly file_id: Schema.String;
114
+ readonly file_unique_id: Schema.String;
115
+ readonly file_size: Schema.Int;
116
+ readonly file_path: Schema.String; /** UTF-8 text; `file_size` must equal its byte length. */
117
+ readonly content: Schema.String;
118
+ }>>>;
119
+ readonly nextMessageId: Schema.optionalKey<Schema.Int>;
120
+ }>;
121
+ type TelegramEmulatorSeed = typeof TelegramEmulatorSeed.Type;
122
+ /** Decode and build a seed; a string is the reason it is invalid. */
123
+ declare const buildTelegramSeedState: (input: unknown) => TelegramEmulatorState | string;
124
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
125
+ declare const decodeTelegramState: (input: unknown) => TelegramEmulatorState | string;
126
+ //#endregion
127
+ export { TelegramEmulatorBot, TelegramEmulatorChat, TelegramEmulatorFile, TelegramEmulatorProfile, TelegramEmulatorSeed, TelegramEmulatorSentMessage, TelegramEmulatorState, TelegramEmulatorStateSchema, buildTelegramSeedState, decodeTelegramState };
128
+ //# sourceMappingURL=state.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.mts","names":[],"sources":["../../src/telegram/state.ts"],"mappings":";;;;cAea,mBAAA,EAAmB,MAAA,CAAA,MAAA;EAAA;;;;KAMpB,mBAAA,UAA6B,mBAAA,CAAoB,IAAI;;cAGpD,oBAAA,EAAoB,MAAA,CAAA,MAAA;EAAA;;;;;;;;;KASrB,oBAAA,UAA8B,oBAAA,CAAqB,IAAI;;cAgBtD,oBAAA,EAAoB,MAAA,CAAA,MAAA;EAAA;;;qCA5BF;EAAA;;KAqCnB,oBAAA,UAA8B,oBAAA,CAAqB,IAAI;AAlCnE;AAAA,cAqCa,2BAAA,EAA2B,MAAA,CAAA,MAAA;EAAA;;;;;;KAQ5B,2BAAA,UAAqC,2BAAA,CAA4B,IAAI;AAAA,cAE3E,QAAA,EAAQ,MAAA,CAAA,MAAA;EA/CmB,mEAkD/B,MAAA,CAAA,GAAA;AAAA;;cAGW,2BAAA,EAA2B,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAS5B,qBAAA;EACV,GAAA,EAAK,mBAAA;EACL,KAAA,EAAO,aAAA,CAAc,oBAAA;EACrB,KAAA,EAAO,aAAA,CAAc,oBAAA;EACrB,YAAA,EAAc,aAAA,CAAc,2BAAA;EAC5B,QAAA,SAAiB,QAAA,CAAS,IAAA;AAAA;;cAIf,uBAAA,EAAuB,MAAA,CAAA,QAAA;AAAA,KAExB,uBAAA,UAAiC,uBAAA,CAAwB,IAAI;;;;;;cAO5D,oBAAA,EAAoB,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;uCA3CO;IAAA;;;;KAmD5B,oBAAA,UAA8B,oBAAA,CAAqB,IAAI;;cAuGtD,sBAAA,GAA0B,KAAA,cAAiB,qBAAqB;;cAShE,mBAAA,GAAuB,KAAA,cAAiB,qBAAqB"}
@@ -0,0 +1,154 @@
1
+ import { Result } from "effect";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/telegram/state.ts
4
+ /**
5
+ * Telegram Bot API emulator state: the typed entities, the seed input, the default seed, and the
6
+ * profiles (internal; re-exported by `src/telegram.ts`).
7
+ *
8
+ * Entity shapes and the default entities are copied as data from the synthetic Telegram
9
+ * conformance fixtures (the same chat and file ids as `telegramConformanceFixtureSeeds`, the bot
10
+ * the send fixture answers as `from`, and the 32-byte synthetic file), never imported from SDK
11
+ * code. The state holds no bot token: tokens are never stored.
12
+ *
13
+ * @experimental
14
+ */
15
+ /** The bot every token with a non-zero bot id answers as (`from` of sent messages). */
16
+ const TelegramEmulatorBot = Schema.Struct({
17
+ id: Schema.Int,
18
+ first_name: Schema.String,
19
+ username: Schema.String
20
+ });
21
+ /** A chat the bot is a member of, in the `getChat` fixture's wire shape. */
22
+ const TelegramEmulatorChat = Schema.Struct({
23
+ id: Schema.Int,
24
+ title: Schema.String,
25
+ type: Schema.String,
26
+ permissions: Schema.Struct({ can_send_messages: Schema.Boolean }),
27
+ accent_color_id: Schema.Int,
28
+ max_reaction_count: Schema.Int
29
+ });
30
+ const FileId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,256}$/));
31
+ /** A relative file path the download helper accepts (no empty or dot segments). */
32
+ const FilePath = Schema.String.check(Schema.makeFilter((path) => /^[A-Za-z0-9_./-]+$/.test(path) && !path.startsWith("/") && path.split("/").every((part) => part !== "" && part !== "." && part !== "..") ? void 0 : "a relative path of letters, digits, and _ . / - without empty or dot segments"));
33
+ /** A file the bot received: its `getFile` answer and its UTF-8 text content. */
34
+ const TelegramEmulatorFile = Schema.Struct({
35
+ file_id: FileId,
36
+ file_unique_id: FileId,
37
+ file_size: Schema.Int,
38
+ file_path: FilePath,
39
+ /** UTF-8 text; `file_size` must equal its byte length. */
40
+ content: Schema.String
41
+ });
42
+ /** A message the bot sent (irreversible: kept until reset or seed). */
43
+ const TelegramEmulatorSentMessage = Schema.Struct({
44
+ message_id: Schema.Int,
45
+ chat_id: Schema.Int,
46
+ text: Schema.String,
47
+ date: Schema.Int,
48
+ disable_web_page_preview: Schema.Boolean
49
+ });
50
+ const Counters = Schema.Struct({
51
+ /** Next `message_id` of a sent message. */
52
+ nextMessageId: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)) });
53
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
54
+ const TelegramEmulatorStateSchema = Schema.Struct({
55
+ bot: TelegramEmulatorBot,
56
+ chats: Schema.Array(TelegramEmulatorChat),
57
+ files: Schema.Array(TelegramEmulatorFile),
58
+ sentMessages: Schema.Array(TelegramEmulatorSentMessage),
59
+ counters: Counters
60
+ });
61
+ /** Account-variance profiles for the default seed. */
62
+ const TelegramEmulatorProfile = Schema.Literals(["default", "empty"]);
63
+ /**
64
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entities; `'empty'` keeps
65
+ * only the bot); every other key, when given, replaces that part of the profile.
66
+ * `nextMessageId` defaults to 101 (the send fixture's `message_id`).
67
+ */
68
+ const TelegramEmulatorSeed = Schema.Struct({
69
+ profile: Schema.optionalKey(TelegramEmulatorProfile),
70
+ bot: Schema.optionalKey(TelegramEmulatorBot),
71
+ chats: Schema.optionalKey(Schema.Array(TelegramEmulatorChat)),
72
+ files: Schema.optionalKey(Schema.Array(TelegramEmulatorFile)),
73
+ nextMessageId: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)))
74
+ });
75
+ const strict = { onExcessProperty: "error" };
76
+ const decodeSeedInput = Schema.decodeUnknownResult(TelegramEmulatorSeed, strict);
77
+ const decodeStateInput = Schema.decodeUnknownResult(TelegramEmulatorStateSchema, strict);
78
+ const issueMessage = (issue) => new Schema.SchemaError(issue).message;
79
+ const defaultBot = {
80
+ id: 123456789,
81
+ first_name: "Synthetic Practice Bot",
82
+ username: "yolk_synthetic_bot"
83
+ };
84
+ const defaultChats = [{
85
+ id: -0xe9103fda01,
86
+ title: "Synthetic practice group",
87
+ type: "supergroup",
88
+ permissions: { can_send_messages: true },
89
+ accent_color_id: 0,
90
+ max_reaction_count: 11
91
+ }];
92
+ const defaultFiles = [{
93
+ file_id: "BQACAgIAAxkDAAIC-yolk_synthetic_file_0001",
94
+ file_unique_id: "AgADyolkSynthetic01",
95
+ file_size: 32,
96
+ file_path: "documents/file_0.txt",
97
+ content: "yolk-conformance synthetic file\n"
98
+ }];
99
+ const profileEntities = (profile) => {
100
+ switch (profile) {
101
+ case "default": return {
102
+ bot: defaultBot,
103
+ chats: defaultChats,
104
+ files: defaultFiles
105
+ };
106
+ case "empty": return {
107
+ bot: defaultBot,
108
+ chats: [],
109
+ files: []
110
+ };
111
+ }
112
+ };
113
+ const duplicate = (values) => values.find((value, index) => values.indexOf(value) !== index);
114
+ const utf8Length = (text) => new TextEncoder().encode(text).byteLength;
115
+ const seedProblem = (entities) => {
116
+ if (entities.bot.id <= 0) return "the bot id must be positive";
117
+ const chat = duplicate(entities.chats.map((item) => item.id));
118
+ if (chat !== void 0) return `duplicate chat id ${chat}`;
119
+ const fileId = duplicate(entities.files.map((item) => item.file_id));
120
+ if (fileId !== void 0) return `duplicate file id ${fileId}`;
121
+ const filePath = duplicate(entities.files.map((item) => item.file_path));
122
+ if (filePath !== void 0) return `duplicate file path ${filePath}`;
123
+ const sized = entities.files.find((item) => item.file_size !== utf8Length(item.content));
124
+ return sized === void 0 ? void 0 : `file ${sized.file_id} has a file_size other than its content's byte length`;
125
+ };
126
+ const stateFromSeed = (seed) => {
127
+ const profile = profileEntities(seed.profile ?? "default");
128
+ const entities = {
129
+ bot: seed.bot ?? profile.bot,
130
+ chats: seed.chats ?? profile.chats,
131
+ files: seed.files ?? profile.files
132
+ };
133
+ const problem = seedProblem(entities);
134
+ if (problem !== void 0) return problem;
135
+ return {
136
+ ...entities,
137
+ sentMessages: [],
138
+ counters: { nextMessageId: seed.nextMessageId ?? 101 }
139
+ };
140
+ };
141
+ /** Decode and build a seed; a string is the reason it is invalid. */
142
+ const buildTelegramSeedState = (input) => {
143
+ const decoded = decodeSeedInput(input);
144
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : stateFromSeed(decoded.success);
145
+ };
146
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
147
+ const decodeTelegramState = (input) => {
148
+ const decoded = decodeStateInput(input);
149
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success };
150
+ };
151
+ //#endregion
152
+ export { TelegramEmulatorBot, TelegramEmulatorChat, TelegramEmulatorFile, TelegramEmulatorProfile, TelegramEmulatorSeed, TelegramEmulatorSentMessage, TelegramEmulatorStateSchema, buildTelegramSeedState, decodeTelegramState };
153
+
154
+ //# sourceMappingURL=state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.mjs","names":[],"sources":["../../src/telegram/state.ts"],"sourcesContent":["/**\n * Telegram Bot API emulator state: the typed entities, the seed input, the default seed, and the\n * profiles (internal; re-exported by `src/telegram.ts`).\n *\n * Entity shapes and the default entities are copied as data from the synthetic Telegram\n * conformance fixtures (the same chat and file ids as `telegramConformanceFixtureSeeds`, the bot\n * the send fixture answers as `from`, and the 32-byte synthetic file), never imported from SDK\n * code. The state holds no bot token: tokens are never stored.\n *\n * @experimental\n */\nimport { Result } from 'effect'\nimport * as Schema from 'effect/Schema'\n\n/** The bot every token with a non-zero bot id answers as (`from` of sent messages). */\nexport const TelegramEmulatorBot = Schema.Struct({\n id: Schema.Int,\n first_name: Schema.String,\n username: Schema.String\n})\n\nexport type TelegramEmulatorBot = typeof TelegramEmulatorBot.Type\n\n/** A chat the bot is a member of, in the `getChat` fixture's wire shape. */\nexport const TelegramEmulatorChat = Schema.Struct({\n id: Schema.Int,\n title: Schema.String,\n type: Schema.String,\n permissions: Schema.Struct({ can_send_messages: Schema.Boolean }),\n accent_color_id: Schema.Int,\n max_reaction_count: Schema.Int\n})\n\nexport type TelegramEmulatorChat = typeof TelegramEmulatorChat.Type\n\nconst FileId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,256}$/))\n\n/** A relative file path the download helper accepts (no empty or dot segments). */\nconst FilePath = Schema.String.check(\n Schema.makeFilter(path =>\n /^[A-Za-z0-9_./-]+$/.test(path) &&\n !path.startsWith('/') &&\n path.split('/').every(part => part !== '' && part !== '.' && part !== '..')\n ? undefined\n : 'a relative path of letters, digits, and _ . / - without empty or dot segments'\n )\n)\n\n/** A file the bot received: its `getFile` answer and its UTF-8 text content. */\nexport const TelegramEmulatorFile = Schema.Struct({\n file_id: FileId,\n file_unique_id: FileId,\n file_size: Schema.Int,\n file_path: FilePath,\n /** UTF-8 text; `file_size` must equal its byte length. */\n content: Schema.String\n})\n\nexport type TelegramEmulatorFile = typeof TelegramEmulatorFile.Type\n\n/** A message the bot sent (irreversible: kept until reset or seed). */\nexport const TelegramEmulatorSentMessage = Schema.Struct({\n message_id: Schema.Int,\n chat_id: Schema.Int,\n text: Schema.String,\n date: Schema.Int,\n disable_web_page_preview: Schema.Boolean\n})\n\nexport type TelegramEmulatorSentMessage = typeof TelegramEmulatorSentMessage.Type\n\nconst Counters = Schema.Struct({\n /** Next `message_id` of a sent message. */\n nextMessageId: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))\n})\n\n/** The whole emulator state (JSON-compatible; what `snapshot()` returns). */\nexport const TelegramEmulatorStateSchema = Schema.Struct({\n bot: TelegramEmulatorBot,\n chats: Schema.Array(TelegramEmulatorChat),\n files: Schema.Array(TelegramEmulatorFile),\n sentMessages: Schema.Array(TelegramEmulatorSentMessage),\n counters: Counters\n})\n\n/** The emulator state. The container is mutable; entities are replaced, never edited in place. */\nexport type TelegramEmulatorState = {\n bot: TelegramEmulatorBot\n chats: ReadonlyArray<TelegramEmulatorChat>\n files: ReadonlyArray<TelegramEmulatorFile>\n sentMessages: ReadonlyArray<TelegramEmulatorSentMessage>\n counters: typeof Counters.Type\n}\n\n/** Account-variance profiles for the default seed. */\nexport const TelegramEmulatorProfile = Schema.Literals(['default', 'empty'])\n\nexport type TelegramEmulatorProfile = typeof TelegramEmulatorProfile.Type\n\n/**\n * A typed seed. Start from `profile` (default `'default'`, the fixture entities; `'empty'` keeps\n * only the bot); every other key, when given, replaces that part of the profile.\n * `nextMessageId` defaults to 101 (the send fixture's `message_id`).\n */\nexport const TelegramEmulatorSeed = Schema.Struct({\n profile: Schema.optionalKey(TelegramEmulatorProfile),\n bot: Schema.optionalKey(TelegramEmulatorBot),\n chats: Schema.optionalKey(Schema.Array(TelegramEmulatorChat)),\n files: Schema.optionalKey(Schema.Array(TelegramEmulatorFile)),\n nextMessageId: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)))\n})\n\nexport type TelegramEmulatorSeed = typeof TelegramEmulatorSeed.Type\n\nconst strict = { onExcessProperty: 'error' } as const\n\nconst decodeSeedInput = Schema.decodeUnknownResult(TelegramEmulatorSeed, strict)\n\nconst decodeStateInput = Schema.decodeUnknownResult(TelegramEmulatorStateSchema, strict)\n\nconst issueMessage = (issue: Schema.SchemaError['issue']): string =>\n new Schema.SchemaError(issue).message\n\n// Default entities, copied from the fixtures.\n\nconst defaultBot: TelegramEmulatorBot = {\n id: 123456789,\n first_name: 'Synthetic Practice Bot',\n username: 'yolk_synthetic_bot'\n}\n\nconst defaultChats: ReadonlyArray<TelegramEmulatorChat> = [\n {\n id: -1001000000001,\n title: 'Synthetic practice group',\n type: 'supergroup',\n permissions: { can_send_messages: true },\n accent_color_id: 0,\n max_reaction_count: 11\n }\n]\n\nconst defaultFiles: ReadonlyArray<TelegramEmulatorFile> = [\n {\n file_id: 'BQACAgIAAxkDAAIC-yolk_synthetic_file_0001',\n file_unique_id: 'AgADyolkSynthetic01',\n file_size: 32,\n file_path: 'documents/file_0.txt',\n content: 'yolk-conformance synthetic file\\n'\n }\n]\n\ntype ProfileEntities = {\n readonly bot: TelegramEmulatorBot\n readonly chats: ReadonlyArray<TelegramEmulatorChat>\n readonly files: ReadonlyArray<TelegramEmulatorFile>\n}\n\nconst profileEntities = (profile: TelegramEmulatorProfile): ProfileEntities => {\n switch (profile) {\n case 'default':\n return { bot: defaultBot, chats: defaultChats, files: defaultFiles }\n case 'empty':\n return { bot: defaultBot, chats: [], files: [] }\n }\n}\n\nconst duplicate = <A>(values: ReadonlyArray<A>): A | undefined =>\n values.find((value, index) => values.indexOf(value) !== index)\n\nconst utf8Length = (text: string): number => new TextEncoder().encode(text).byteLength\n\nconst seedProblem = (entities: ProfileEntities): string | undefined => {\n if (entities.bot.id <= 0) return 'the bot id must be positive'\n\n const chat = duplicate(entities.chats.map(item => item.id))\n\n if (chat !== undefined) return `duplicate chat id ${chat}`\n\n const fileId = duplicate(entities.files.map(item => item.file_id))\n\n if (fileId !== undefined) return `duplicate file id ${fileId}`\n\n const filePath = duplicate(entities.files.map(item => item.file_path))\n\n if (filePath !== undefined) return `duplicate file path ${filePath}`\n\n const sized = entities.files.find(item => item.file_size !== utf8Length(item.content))\n\n return sized === undefined\n ? undefined\n : `file ${sized.file_id} has a file_size other than its content's byte length`\n}\n\nconst stateFromSeed = (seed: TelegramEmulatorSeed): TelegramEmulatorState | string => {\n const profile = profileEntities(seed.profile ?? 'default')\n\n const entities: ProfileEntities = {\n bot: seed.bot ?? profile.bot,\n chats: seed.chats ?? profile.chats,\n files: seed.files ?? profile.files\n }\n\n const problem = seedProblem(entities)\n\n if (problem !== undefined) return problem\n\n return {\n ...entities,\n sentMessages: [],\n counters: { nextMessageId: seed.nextMessageId ?? 101 }\n }\n}\n\n/** Decode and build a seed; a string is the reason it is invalid. */\nexport const buildTelegramSeedState = (input: unknown): TelegramEmulatorState | string => {\n const decoded = decodeSeedInput(input)\n\n return Result.isFailure(decoded)\n ? issueMessage(decoded.failure.issue)\n : stateFromSeed(decoded.success)\n}\n\n/** Decode a full state (a restored snapshot); a string is the reason it is invalid. */\nexport const decodeTelegramState = (input: unknown): TelegramEmulatorState | string => {\n const decoded = decodeStateInput(input)\n\n return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }\n}\n"],"mappings":"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"}