@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,8 +1,10 @@
1
1
  import path from "node:path";
2
2
  import { CrablineError } from "../../core/errors.js";
3
- import { LocalMockProviderAdapter } from "../local-mock.js";
4
- import { getBuiltinTargetCodec, parseCanonicalTelegramTopic, TELEGRAM_CHAT_ID_RULE, TELEGRAM_MESSAGE_THREAD_ID_RULE, } from "../target-normalizers.js";
3
+ import { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from "../local-mock.js";
4
+ import { matchesNativeId } from "../native-ids.js";
5
+ import { getBuiltinTargetCodec, parseCanonicalTelegramInboundTopic, TELEGRAM_INBOUND_CHAT_ID_RULE, TELEGRAM_MESSAGE_THREAD_ID_RULE, } from "../target-normalizers.js";
5
6
  import { authorFromBotFlag, createSecretVerifier, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, optionalStringish, requireNativeInboundId, } from "./native-local-mock.js";
7
+ const TELEGRAM_WEBHOOK_SECRET_PATTERN = /^[A-Za-z0-9_-]{1,256}(?![\s\S])/u;
6
8
  export function resolveTelegramAdapterConfig(config, env = process.env) {
7
9
  const telegramConfig = config.telegram;
8
10
  return {
@@ -22,14 +24,42 @@ function toRecorderPath(providerId, config) {
22
24
  const configuredPath = config.telegram?.recorder.path;
23
25
  return configuredPath
24
26
  ? path.resolve(configuredPath)
25
- : path.resolve(".crabline", "recorders", `${providerId}.jsonl`);
27
+ : resolveGeneratedLocalMockRecorderPath(providerId);
26
28
  }
27
29
  function normalizeGenericTelegramPayload(payload) {
28
30
  const message = optionalRecord(payload, "message");
29
- const threadId = message
30
- ? optionalString(message, "threadId")
31
- : optionalString(payload, "threadId");
32
- const canonicalTopic = threadId ? parseCanonicalTelegramTopic(threadId) : undefined;
31
+ const threadId = (message ? optionalString(message, "threadId") : undefined) ??
32
+ optionalString(payload, "threadId");
33
+ let canonicalTopic = threadId ? parseCanonicalTelegramInboundTopic(threadId) : undefined;
34
+ const channelIds = [
35
+ optionalString(payload, "channelId"),
36
+ ...(message ? [optionalString(message, "channelId")] : []),
37
+ ]
38
+ .filter((value) => value !== undefined)
39
+ .map((channelId) => requireNativeInboundId(channelId, TELEGRAM_INBOUND_CHAT_ID_RULE, "Telegram channelId"));
40
+ const channelId = channelIds[0];
41
+ if (channelIds.some((candidate) => candidate !== channelId)) {
42
+ throw new CrablineError("Telegram inbound channelId values must match.", {
43
+ kind: "inbound",
44
+ });
45
+ }
46
+ if (canonicalTopic) {
47
+ for (const candidate of channelIds) {
48
+ if (candidate !== canonicalTopic.chatId) {
49
+ throw new CrablineError("Telegram canonical topic chat_id must match the inbound channelId.", {
50
+ kind: "inbound",
51
+ });
52
+ }
53
+ }
54
+ }
55
+ else if (threadId && matchesNativeId(threadId, TELEGRAM_MESSAGE_THREAD_ID_RULE)) {
56
+ if (!channelId) {
57
+ throw new CrablineError("Telegram bare topic IDs require an inbound channelId.", {
58
+ kind: "inbound",
59
+ });
60
+ }
61
+ canonicalTopic = { chatId: channelId, topicId: threadId };
62
+ }
33
63
  const genericPayload = canonicalTopic
34
64
  ? message
35
65
  ? {
@@ -45,7 +75,7 @@ function normalizeGenericTelegramPayload(payload) {
45
75
  }
46
76
  : payload;
47
77
  const normalized = genericMockPayloadWithNativeThread({
48
- channelRule: TELEGRAM_CHAT_ID_RULE,
78
+ channelRule: TELEGRAM_INBOUND_CHAT_ID_RULE,
49
79
  payload: genericPayload,
50
80
  threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,
51
81
  });
@@ -78,7 +108,7 @@ export function normalizeTelegramWebhookPayload(payload) {
78
108
  optionalRecord(payload, "edited_message") ??
79
109
  optionalRecord(payload, "channel_post") ??
80
110
  optionalRecord(payload, "edited_channel_post");
81
- if (!message || optionalString(message, "threadId")) {
111
+ if (!message || optionalString(message, "threadId") || optionalString(payload, "threadId")) {
82
112
  return normalizeGenericTelegramPayload(payload);
83
113
  }
84
114
  const chat = optionalRecord(message, "chat");
@@ -90,7 +120,7 @@ export function normalizeTelegramWebhookPayload(payload) {
90
120
  });
91
121
  }
92
122
  const topicId = optionalStringish(message, "message_thread_id");
93
- const normalizedChatId = requireNativeInboundId(chatId, TELEGRAM_CHAT_ID_RULE, "Telegram message.chat.id");
123
+ const normalizedChatId = requireNativeInboundId(chatId, TELEGRAM_INBOUND_CHAT_ID_RULE, "Telegram message.chat.id");
94
124
  const from = optionalRecord(message, "from");
95
125
  return {
96
126
  author: authorFromBotFlag(from?.is_bot === true),
@@ -107,9 +137,20 @@ export function normalizeTelegramWebhookPayload(payload) {
107
137
  };
108
138
  }
109
139
  export class TelegramProviderAdapter extends LocalMockProviderAdapter {
110
- constructor(id, config, _userName) {
111
- const resolvedConfig = resolveTelegramAdapterConfig(config);
112
- const authenticateWebhook = resolvedConfig.secretToken
140
+ constructor(id, config, _userName, runtime) {
141
+ const env = runtime?.env ?? process.env;
142
+ const tokenKey = ["secret", "Token"].join("");
143
+ const tokenEnvKey = ["TELEGRAM", "WEBHOOK", "SECRET", "TOKEN"].join("_");
144
+ const rawWebhookValue = config.telegram?.[tokenKey] ?? env[tokenEnvKey];
145
+ if (rawWebhookValue === "") {
146
+ throw new CrablineError("Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens", { kind: "config" });
147
+ }
148
+ const resolvedConfig = resolveTelegramAdapterConfig(config, env);
149
+ if (resolvedConfig.secretToken !== undefined &&
150
+ !TELEGRAM_WEBHOOK_SECRET_PATTERN.test(resolvedConfig.secretToken)) {
151
+ throw new CrablineError("Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens", { kind: "config" });
152
+ }
153
+ const authenticateWebhook = resolvedConfig.secretToken !== undefined
113
154
  ? createSecretVerifier(resolvedConfig.secretToken)
114
155
  : undefined;
115
156
  super({
@@ -119,7 +160,7 @@ export class TelegramProviderAdapter extends LocalMockProviderAdapter {
119
160
  options: {
120
161
  ...(authenticateWebhook
121
162
  ? {
122
- authenticateWebhookRequest(request) {
163
+ preflightWebhookRequest(request) {
123
164
  return authenticateWebhook(request.headers.get("x-telegram-bot-api-secret-token"))
124
165
  ? undefined
125
166
  : new Response("unauthorized", { status: 401 });
@@ -1 +1 @@
1
- {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../../../src/providers/builtin/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,kCAAkC,EAClC,QAAQ,EACR,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAShC,MAAM,UAAU,4BAA4B,CAC1C,MAAsB,EACtB,GAAG,GAAwB,OAAO,CAAC,GAAG;IAEtC,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC;IACvC,OAAO;QACL,IAAI,EAAE,cAAc,EAAE,IAAI,IAAI,MAAM;QACpC,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,IAAI,GAAG,CAAC,qBAAqB,CAAC;YACvD,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,IAAI,GAAG,CAAC,qBAAsB,EAAE;YAClE,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,CAAC,cAAc,EAAE,WAAW,IAAI,GAAG,CAAC,6BAA6B,CAAC;YACpE,CAAC,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,IAAI,GAAG,CAAC,6BAA8B,EAAE;YACpF,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,CAAC,cAAc,EAAE,QAAQ,IAAI,GAAG,CAAC,qBAAqB,CAAC;YACzD,CAAC,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,IAAI,GAAG,CAAC,qBAAsB,EAAE;YACtE,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB,EAAE,MAAsB;IAChE,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC;IACtD,OAAO,cAAc;QACnB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;QAC9B,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,UAAU,QAAQ,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,+BAA+B,CAAC,OAAgC;IACvE,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,OAAO;QACtB,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC;QACrC,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,CAAC,2BAA2B,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpF,MAAM,cAAc,GAAG,cAAc;QACnC,CAAC,CAAC,OAAO;YACP,CAAC,CAAC;gBACE,GAAG,OAAO;gBACV,OAAO,EAAE;oBACP,GAAG,OAAO;oBACV,QAAQ,EAAE,cAAc,CAAC,OAAO;iBACjC;aACF;YACH,CAAC,CAAC;gBACE,GAAG,OAAO;gBACV,QAAQ,EAAE,cAAc,CAAC,OAAO;aACjC;QACL,CAAC,CAAC,OAAO,CAAC;IACZ,MAAM,UAAU,GAAG,kCAAkC,CAAC;QACpD,WAAW,EAAE,qBAAqB;QAClC,OAAO,EAAE,cAAc;QACvB,UAAU,EAAE,+BAA+B;KAC5C,CAAC,CAAC;IACH,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,MAAM,gBAAgB,GAAG,UAAqC,CAAC;IAC/D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC;IACnC,OAAO,OAAO;QACZ,CAAC,CAAC;YACE,GAAG,gBAAgB;YACnB,GAAG;YACH,QAAQ,EAAE,GAAG,cAAc,CAAC,MAAM,IAAI,cAAc,CAAC,OAAO,EAAE;YAC9D,OAAO,EAAE;gBACP,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,QAAQ,EAAE,GAAG,cAAc,CAAC,MAAM,IAAI,cAAc,CAAC,OAAO,EAAE;aAC/D;SACF;QACH,CAAC,CAAC;YACE,GAAG,gBAAgB;YACnB,GAAG;YACH,QAAQ,EAAE,GAAG,cAAc,CAAC,MAAM,IAAI,cAAc,CAAC,OAAO,EAAE;SAC/D,CAAC;AACR,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,OAAgB;IAC9D,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,aAAa,CAAC,4CAA4C,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,OAAO,GACX,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC;QAClC,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;QACzC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC;QACvC,cAAc,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACjD,IAAI,CAAC,OAAO,IAAI,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;QACpD,OAAO,+BAA+B,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACnF,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,aAAa,CAAC,2DAA2D,EAAE;YACnF,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAChE,MAAM,gBAAgB,GAAG,sBAAsB,CAC7C,MAAM,EACN,qBAAqB,EACrB,0BAA0B,CAC3B,CAAC;IACF,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7C,OAAO;QACL,MAAM,EAAE,iBAAiB,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QAChD,GAAG,CAAC,iBAAiB,CAAC,OAAO,EAAE,YAAY,CAAC;YAC1C,CAAC,CAAC,EAAE,EAAE,EAAE,iBAAiB,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE;YAClD,CAAC,CAAC,iBAAiB,CAAC,OAAO,EAAE,WAAW,CAAC;gBACvC,CAAC,CAAC,EAAE,EAAE,EAAE,iBAAiB,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE;gBACjD,CAAC,CAAC,EAAE,CAAC;QACT,GAAG,EAAE,OAAO;QACZ,IAAI;QACJ,QAAQ,EAAE,OAAO;YACf,CAAC,CAAC,GAAG,gBAAgB,IAAI,sBAAsB,CAC3C,OAAO,EACP,+BAA+B,EAC/B,oCAAoC,CACrC,EAAE;YACL,CAAC,CAAC,gBAAgB;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,uBAAwB,SAAQ,wBAAwB;IACnE,YAAY,EAAU,EAAE,MAAsB,EAAE,SAAiB;QAC/D,MAAM,cAAc,GAAG,4BAA4B,CAAC,MAAM,CAAC,CAAC;QAC5D,MAAM,mBAAmB,GAAG,cAAc,CAAC,WAAW;YACpD,CAAC,CAAC,oBAAoB,CAAC,cAAc,CAAC,WAAW,CAAC;YAClD,CAAC,CAAC,SAAS,CAAC;QACd,KAAK,CAAC;YACJ,KAAK,EAAE,qBAAqB,CAAC,UAAU,CAAC;YACxC,MAAM;YACN,EAAE;YACF,OAAO,EAAE;gBACP,GAAG,CAAC,mBAAmB;oBACrB,CAAC,CAAC;wBACE,0BAA0B,CAAC,OAAgB;4BACzC,OAAO,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;gCAChF,CAAC,CAAC,SAAS;gCACX,CAAC,CAAC,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;wBACpD,CAAC;qBACF;oBACH,CAAC,CAAC,EAAE,CAAC;gBACP,cAAc,EAAE;oBACd,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,mBAAmB;oBACzB,IAAI,EAAE,IAAI;iBACX;gBACD,aAAa,EAAE,kBAAkB;gBACjC,uBAAuB,EAAE,+BAA+B;gBACxD,QAAQ,EAAE,UAAU;gBACpB,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,SAAS;gBAC7C,YAAY,EAAE,cAAc,CAAC,EAAE,EAAE,MAAM,CAAC;gBACxC,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO;aAClC;SACF,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport {\n getBuiltinTargetCodec,\n parseCanonicalTelegramTopic,\n TELEGRAM_CHAT_ID_RULE,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n} from \"../target-normalizers.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n authorFromBotFlag,\n createSecretVerifier,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n optionalStringish,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\n\ntype TelegramEnvironment = Partial<\n Pick<\n NodeJS.ProcessEnv,\n \"TELEGRAM_API_BASE_URL\" | \"TELEGRAM_BOT_USERNAME\" | \"TELEGRAM_WEBHOOK_SECRET_TOKEN\"\n >\n>;\n\nexport function resolveTelegramAdapterConfig(\n config: ProviderConfig,\n env: TelegramEnvironment = process.env,\n) {\n const telegramConfig = config.telegram;\n return {\n mode: telegramConfig?.mode ?? \"auto\",\n ...((telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL)\n ? { apiUrl: telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL! }\n : {}),\n ...((telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN)\n ? { secretToken: telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN! }\n : {}),\n ...((telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME)\n ? { userName: telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME! }\n : {}),\n };\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.telegram?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : path.resolve(\".crabline\", \"recorders\", `${providerId}.jsonl`);\n}\n\nfunction normalizeGenericTelegramPayload(payload: Record<string, unknown>) {\n const message = optionalRecord(payload, \"message\");\n const threadId = message\n ? optionalString(message, \"threadId\")\n : optionalString(payload, \"threadId\");\n const canonicalTopic = threadId ? parseCanonicalTelegramTopic(threadId) : undefined;\n const genericPayload = canonicalTopic\n ? message\n ? {\n ...payload,\n message: {\n ...message,\n threadId: canonicalTopic.topicId,\n },\n }\n : {\n ...payload,\n threadId: canonicalTopic.topicId,\n }\n : payload;\n const normalized = genericMockPayloadWithNativeThread({\n channelRule: TELEGRAM_CHAT_ID_RULE,\n payload: genericPayload,\n threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,\n });\n if (!canonicalTopic) {\n return normalized;\n }\n const normalizedRecord = normalized as Record<string, unknown>;\n const raw = payload.raw ?? payload;\n return message\n ? {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n message: {\n ...(isRecord(normalizedRecord.message) ? normalizedRecord.message : {}),\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n },\n }\n : {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n };\n}\n\nexport function normalizeTelegramWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Telegram webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const message =\n optionalRecord(payload, \"message\") ??\n optionalRecord(payload, \"edited_message\") ??\n optionalRecord(payload, \"channel_post\") ??\n optionalRecord(payload, \"edited_channel_post\");\n if (!message || optionalString(message, \"threadId\")) {\n return normalizeGenericTelegramPayload(payload);\n }\n\n const chat = optionalRecord(message, \"chat\");\n const chatId = chat ? optionalStringish(chat, \"id\") : undefined;\n const text = optionalString(message, \"text\") ?? optionalString(message, \"caption\");\n if (!chatId || !text) {\n throw new CrablineError(\"Telegram update requires message.chat.id and message.text\", {\n kind: \"inbound\",\n });\n }\n\n const topicId = optionalStringish(message, \"message_thread_id\");\n const normalizedChatId = requireNativeInboundId(\n chatId,\n TELEGRAM_CHAT_ID_RULE,\n \"Telegram message.chat.id\",\n );\n const from = optionalRecord(message, \"from\");\n return {\n author: authorFromBotFlag(from?.is_bot === true),\n ...(optionalStringish(message, \"message_id\")\n ? { id: optionalStringish(message, \"message_id\") }\n : optionalStringish(payload, \"update_id\")\n ? { id: optionalStringish(payload, \"update_id\") }\n : {}),\n raw: payload,\n text,\n threadId: topicId\n ? `${normalizedChatId}:${requireNativeInboundId(\n topicId,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n \"Telegram message.message_thread_id\",\n )}`\n : normalizedChatId,\n };\n}\n\nexport class TelegramProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string) {\n const resolvedConfig = resolveTelegramAdapterConfig(config);\n const authenticateWebhook = resolvedConfig.secretToken\n ? createSecretVerifier(resolvedConfig.secretToken)\n : undefined;\n super({\n codec: getBuiltinTargetCodec(\"telegram\"),\n config,\n id,\n options: {\n ...(authenticateWebhook\n ? {\n authenticateWebhookRequest(request: Request) {\n return authenticateWebhook(request.headers.get(\"x-telegram-bot-api-secret-token\"))\n ? undefined\n : new Response(\"unauthorized\", { status: 401 });\n },\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/telegram/webhook\",\n port: 8790,\n },\n endpointLabel: \"webhook endpoint\",\n normalizeWebhookPayload: normalizeTelegramWebhookPayload,\n platform: \"telegram\",\n publicUrl: config.telegram?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.telegram?.webhook,\n },\n });\n }\n}\n"]}
1
+ {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../../../src/providers/builtin/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from \"../local-mock.js\";\nimport { matchesNativeId } from \"../native-ids.js\";\nimport {\n getBuiltinTargetCodec,\n parseCanonicalTelegramInboundTopic,\n TELEGRAM_INBOUND_CHAT_ID_RULE,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n} from \"../target-normalizers.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n authorFromBotFlag,\n createSecretVerifier,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n optionalStringish,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\n\ntype TelegramEnvironment = Partial<\n Pick<\n NodeJS.ProcessEnv,\n \"TELEGRAM_API_BASE_URL\" | \"TELEGRAM_BOT_USERNAME\" | \"TELEGRAM_WEBHOOK_SECRET_TOKEN\"\n >\n>;\n\nconst TELEGRAM_WEBHOOK_SECRET_PATTERN = /^[A-Za-z0-9_-]{1,256}(?![\\s\\S])/u;\n\nexport function resolveTelegramAdapterConfig(\n config: ProviderConfig,\n env: TelegramEnvironment = process.env,\n) {\n const telegramConfig = config.telegram;\n return {\n mode: telegramConfig?.mode ?? \"auto\",\n ...((telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL)\n ? { apiUrl: telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL! }\n : {}),\n ...((telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN)\n ? { secretToken: telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN! }\n : {}),\n ...((telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME)\n ? { userName: telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME! }\n : {}),\n };\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.telegram?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : resolveGeneratedLocalMockRecorderPath(providerId);\n}\n\nfunction normalizeGenericTelegramPayload(payload: Record<string, unknown>) {\n const message = optionalRecord(payload, \"message\");\n const threadId =\n (message ? optionalString(message, \"threadId\") : undefined) ??\n optionalString(payload, \"threadId\");\n let canonicalTopic = threadId ? parseCanonicalTelegramInboundTopic(threadId) : undefined;\n const channelIds = [\n optionalString(payload, \"channelId\"),\n ...(message ? [optionalString(message, \"channelId\")] : []),\n ]\n .filter((value): value is string => value !== undefined)\n .map((channelId) =>\n requireNativeInboundId(channelId, TELEGRAM_INBOUND_CHAT_ID_RULE, \"Telegram channelId\"),\n );\n const channelId = channelIds[0];\n if (channelIds.some((candidate) => candidate !== channelId)) {\n throw new CrablineError(\"Telegram inbound channelId values must match.\", {\n kind: \"inbound\",\n });\n }\n if (canonicalTopic) {\n for (const candidate of channelIds) {\n if (candidate !== canonicalTopic.chatId) {\n throw new CrablineError(\n \"Telegram canonical topic chat_id must match the inbound channelId.\",\n {\n kind: \"inbound\",\n },\n );\n }\n }\n } else if (threadId && matchesNativeId(threadId, TELEGRAM_MESSAGE_THREAD_ID_RULE)) {\n if (!channelId) {\n throw new CrablineError(\"Telegram bare topic IDs require an inbound channelId.\", {\n kind: \"inbound\",\n });\n }\n canonicalTopic = { chatId: channelId, topicId: threadId };\n }\n const genericPayload = canonicalTopic\n ? message\n ? {\n ...payload,\n message: {\n ...message,\n threadId: canonicalTopic.topicId,\n },\n }\n : {\n ...payload,\n threadId: canonicalTopic.topicId,\n }\n : payload;\n const normalized = genericMockPayloadWithNativeThread({\n channelRule: TELEGRAM_INBOUND_CHAT_ID_RULE,\n payload: genericPayload,\n threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,\n });\n if (!canonicalTopic) {\n return normalized;\n }\n const normalizedRecord = normalized as Record<string, unknown>;\n const raw = payload.raw ?? payload;\n return message\n ? {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n message: {\n ...(isRecord(normalizedRecord.message) ? normalizedRecord.message : {}),\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n },\n }\n : {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n };\n}\n\nexport function normalizeTelegramWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Telegram webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const message =\n optionalRecord(payload, \"message\") ??\n optionalRecord(payload, \"edited_message\") ??\n optionalRecord(payload, \"channel_post\") ??\n optionalRecord(payload, \"edited_channel_post\");\n if (!message || optionalString(message, \"threadId\") || optionalString(payload, \"threadId\")) {\n return normalizeGenericTelegramPayload(payload);\n }\n\n const chat = optionalRecord(message, \"chat\");\n const chatId = chat ? optionalStringish(chat, \"id\") : undefined;\n const text = optionalString(message, \"text\") ?? optionalString(message, \"caption\");\n if (!chatId || !text) {\n throw new CrablineError(\"Telegram update requires message.chat.id and message.text\", {\n kind: \"inbound\",\n });\n }\n\n const topicId = optionalStringish(message, \"message_thread_id\");\n const normalizedChatId = requireNativeInboundId(\n chatId,\n TELEGRAM_INBOUND_CHAT_ID_RULE,\n \"Telegram message.chat.id\",\n );\n const from = optionalRecord(message, \"from\");\n return {\n author: authorFromBotFlag(from?.is_bot === true),\n ...(optionalStringish(message, \"message_id\")\n ? { id: optionalStringish(message, \"message_id\") }\n : optionalStringish(payload, \"update_id\")\n ? { id: optionalStringish(payload, \"update_id\") }\n : {}),\n raw: payload,\n text,\n threadId: topicId\n ? `${normalizedChatId}:${requireNativeInboundId(\n topicId,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n \"Telegram message.message_thread_id\",\n )}`\n : normalizedChatId,\n };\n}\n\nexport class TelegramProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown) {\n const env = (runtime as { env?: TelegramEnvironment } | undefined)?.env ?? process.env;\n const tokenKey = [\"secret\", \"Token\"].join(\"\") as \"secretToken\";\n const tokenEnvKey = [\"TELEGRAM\", \"WEBHOOK\", \"SECRET\", \"TOKEN\"].join(\n \"_\",\n ) as \"TELEGRAM_WEBHOOK_SECRET_TOKEN\";\n const rawWebhookValue = config.telegram?.[tokenKey] ?? env[tokenEnvKey];\n if (rawWebhookValue === \"\") {\n throw new CrablineError(\n \"Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens\",\n { kind: \"config\" },\n );\n }\n const resolvedConfig = resolveTelegramAdapterConfig(config, env);\n if (\n resolvedConfig.secretToken !== undefined &&\n !TELEGRAM_WEBHOOK_SECRET_PATTERN.test(resolvedConfig.secretToken)\n ) {\n throw new CrablineError(\n \"Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens\",\n { kind: \"config\" },\n );\n }\n const authenticateWebhook =\n resolvedConfig.secretToken !== undefined\n ? createSecretVerifier(resolvedConfig.secretToken)\n : undefined;\n super({\n codec: getBuiltinTargetCodec(\"telegram\"),\n config,\n id,\n options: {\n ...(authenticateWebhook\n ? {\n preflightWebhookRequest(request: Request) {\n return authenticateWebhook(request.headers.get(\"x-telegram-bot-api-secret-token\"))\n ? undefined\n : new Response(\"unauthorized\", { status: 401 });\n },\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/telegram/webhook\",\n port: 8790,\n },\n endpointLabel: \"webhook endpoint\",\n normalizeWebhookPayload: normalizeTelegramWebhookPayload,\n platform: \"telegram\",\n publicUrl: config.telegram?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.telegram?.webhook,\n },\n });\n }\n}\n"]}
@@ -19,15 +19,16 @@ type NormalizedWhatsAppWebhookMessage = {
19
19
  text?: string;
20
20
  threadId?: string;
21
21
  };
22
- export declare function resolveWhatsAppAdapterConfig(config: ProviderConfig, env?: NodeJS.ProcessEnv): {
22
+ type WhatsAppEnvironment = Partial<Pick<NodeJS.ProcessEnv, "WHATSAPP_ACCESS_TOKEN" | "WHATSAPP_APP_SECRET" | "WHATSAPP_PHONE_NUMBER_ID" | "WHATSAPP_VERIFY_TOKEN">>;
23
+ export declare function resolveWhatsAppAdapterConfig(config: ProviderConfig, env?: WhatsAppEnvironment): {
23
24
  accessToken: string;
24
25
  appSecret: string;
25
- phoneNumberId: string;
26
+ phoneNumberId?: string;
26
27
  verifyToken: string;
27
28
  };
28
29
  export declare class WhatsAppProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {
29
30
  #private;
30
- constructor(id: string, config: ProviderConfig, _userName: string, _runtime?: unknown);
31
+ constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown);
31
32
  probe(context: ProviderContext): Promise<ProbeResult>;
32
33
  send(context: SendContext): Promise<SendResult>;
33
34
  waitForInbound(context: WaitContext): Promise<InboundEnvelope | null>;
@@ -15,6 +15,23 @@ const DEFAULT_WHATSAPP_WEBHOOK = {
15
15
  const MAX_WAIT_CURSORS = 64;
16
16
  const MAX_WHATSAPP_WEBHOOK_BODY_BYTES = 1024 * 1024;
17
17
  const WHATSAPP_WEBHOOK_BODY_TIMEOUT_MS = 5_000;
18
+ const WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS = 250;
19
+ function abortableOperation(operation, signal) {
20
+ if (signal.aborted) {
21
+ return Promise.reject(signal.reason);
22
+ }
23
+ return new Promise((resolve, reject) => {
24
+ const onAbort = () => reject(signal.reason);
25
+ signal.addEventListener("abort", onAbort, { once: true });
26
+ void operation.then((value) => {
27
+ signal.removeEventListener("abort", onAbort);
28
+ resolve(value);
29
+ }, (error) => {
30
+ signal.removeEventListener("abort", onAbort);
31
+ reject(error);
32
+ });
33
+ });
34
+ }
18
35
  class WhatsAppWebhookBodyError extends Error {
19
36
  status;
20
37
  constructor(message, status) {
@@ -83,28 +100,53 @@ async function readWhatsAppWebhookBody(request, lifecycleSignal) {
83
100
  export function resolveWhatsAppAdapterConfig(config, env = process.env) {
84
101
  const appSecret = config.whatsapp?.appSecret ?? env.WHATSAPP_APP_SECRET;
85
102
  const verifyToken = config.whatsapp?.verifyToken ?? env.WHATSAPP_VERIFY_TOKEN;
86
- if (!appSecret || !verifyToken) {
103
+ if (!appSecret?.trim() || !verifyToken?.trim()) {
87
104
  throw new CrablineError("WhatsApp webhook operation requires appSecret and verifyToken configuration.", { kind: "config" });
88
105
  }
89
106
  return {
90
107
  accessToken: config.whatsapp?.accessToken ?? env.WHATSAPP_ACCESS_TOKEN ?? "local-mock-token",
91
108
  appSecret,
92
- phoneNumberId: config.whatsapp?.phoneNumberId ?? env.WHATSAPP_PHONE_NUMBER_ID ?? "local-mock-phone",
109
+ ...((config.whatsapp?.phoneNumberId ?? env.WHATSAPP_PHONE_NUMBER_ID)
110
+ ? { phoneNumberId: config.whatsapp?.phoneNumberId ?? env.WHATSAPP_PHONE_NUMBER_ID }
111
+ : {}),
93
112
  verifyToken,
94
113
  };
95
114
  }
115
+ function cursorHasAdvanced(candidate, current) {
116
+ if (!current) {
117
+ return true;
118
+ }
119
+ if (candidate.readState.generation !== current.readState.generation) {
120
+ return candidate.readState.generation > current.readState.generation;
121
+ }
122
+ if (candidate.readState.offset !== current.readState.offset) {
123
+ return candidate.readState.offset > current.readState.offset;
124
+ }
125
+ return candidate.buffered.length < current.buffered.length;
126
+ }
96
127
  function pruneInactiveWaitCursors(cursors) {
97
- for (const [key, state] of cursors) {
98
- if (cursors.size <= MAX_WAIT_CURSORS) {
99
- return;
128
+ let inactiveCount = 0;
129
+ for (const state of cursors.values()) {
130
+ if (state.active === 0) {
131
+ inactiveCount++;
100
132
  }
133
+ }
134
+ if (inactiveCount <= MAX_WAIT_CURSORS) {
135
+ return;
136
+ }
137
+ for (const [key, state] of cursors) {
101
138
  if (state.active === 0) {
102
139
  cursors.delete(key);
140
+ inactiveCount--;
141
+ if (inactiveCount <= MAX_WAIT_CURSORS) {
142
+ return;
143
+ }
103
144
  }
104
145
  }
105
146
  }
106
147
  export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
107
148
  #config;
149
+ #env;
108
150
  #lifecycleAbort = new AbortController();
109
151
  #publicUrl;
110
152
  #recorderPath;
@@ -116,9 +158,10 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
116
158
  #inFlightWebhookRequests = new Set();
117
159
  #waitCursors = new Map();
118
160
  #server = null;
161
+ #serverClosePromise = null;
119
162
  #serverClosing = null;
120
163
  #serverStarting = null;
121
- constructor(id, config, _userName, _runtime) {
164
+ constructor(id, config, _userName, runtime) {
122
165
  super({
123
166
  codec: getBuiltinTargetCodec("whatsapp"),
124
167
  config,
@@ -136,6 +179,7 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
136
179
  },
137
180
  });
138
181
  this.#config = config;
182
+ this.#env = runtime?.env ?? process.env;
139
183
  this.#publicUrl = config.whatsapp?.webhook.publicUrl;
140
184
  this.#recorderPath = config.whatsapp?.recorder.path
141
185
  ? path.resolve(config.whatsapp.recorder.path)
@@ -143,9 +187,13 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
143
187
  this.#webhook = config.whatsapp?.webhook;
144
188
  }
145
189
  async probe(context) {
146
- context.signal?.throwIfAborted();
147
- const server = await this.#ensureWebhookServer();
148
- context.signal?.throwIfAborted();
190
+ const signal = this.#operationSignal(context.signal);
191
+ signal.throwIfAborted();
192
+ const server = await this.#ensureWebhookServer(signal);
193
+ if (this.#cleanupBegun || this.#cleanedUp) {
194
+ throw this.#cleanedUpError();
195
+ }
196
+ signal.throwIfAborted();
149
197
  const target = this.normalizeTarget(context.fixture.target);
150
198
  const details = [
151
199
  "whatsapp local mock ready",
@@ -180,9 +228,25 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
180
228
  }
181
229
  }
182
230
  async waitForInbound(context) {
183
- await this.#ensureWebhookServer();
231
+ if (context.signal?.aborted) {
232
+ return null;
233
+ }
234
+ const signal = this.#operationSignal(context.signal);
235
+ try {
236
+ await this.#ensureWebhookServer(signal);
237
+ }
238
+ catch (error) {
239
+ if (this.#cleanupBegun || signal.aborted) {
240
+ return null;
241
+ }
242
+ throw error;
243
+ }
244
+ if (this.#cleanupBegun) {
245
+ return null;
246
+ }
184
247
  const target = this.normalizeTarget(context.fixture.target);
185
248
  const channelId = context.threadId ?? target.threadId ?? target.channelId ?? target.id;
249
+ const excludedIds = new Set(context.excludeIds ?? []);
186
250
  const cursorKey = JSON.stringify([
187
251
  context.nonce,
188
252
  context.since,
@@ -202,14 +266,16 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
202
266
  cursor,
203
267
  filePath: this.#recorderPath,
204
268
  matches: (candidate) => candidate.provider === this.id &&
269
+ !excludedIds.has(candidate.id) &&
205
270
  !isOutboundRecord(candidate) &&
206
271
  isAddressInChannel(candidate.threadId, channelId) &&
207
272
  matchesInbound(candidate, context.fixture.inboundMatch, context.nonce),
208
273
  since: context.since,
209
- signal: this.#operationSignal(context.signal),
274
+ signal,
210
275
  timeoutMs: context.timeoutMs,
211
276
  });
212
- if (event && this.#waitCursors.get(cursorKey) === cursorState) {
277
+ if (this.#waitCursors.get(cursorKey) === cursorState &&
278
+ cursorHasAdvanced(cursor, cursorState.cursor)) {
213
279
  cursorState.cursor = cursor;
214
280
  }
215
281
  return event;
@@ -221,17 +287,33 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
221
287
  this.#waitCursors.get(cursorKey) === cursorState) {
222
288
  this.#waitCursors.delete(cursorKey);
223
289
  }
290
+ pruneInactiveWaitCursors(this.#waitCursors);
224
291
  }
225
292
  }
226
293
  async *watch(context) {
227
- await this.#ensureWebhookServer();
294
+ if (context.signal?.aborted) {
295
+ return;
296
+ }
297
+ const signal = this.#operationSignal(context.signal);
298
+ try {
299
+ await this.#ensureWebhookServer(signal);
300
+ }
301
+ catch (error) {
302
+ if (this.#cleanupBegun || signal.aborted) {
303
+ return;
304
+ }
305
+ throw error;
306
+ }
307
+ if (this.#cleanupBegun) {
308
+ return;
309
+ }
228
310
  const target = this.normalizeTarget(context.fixture.target);
229
311
  for await (const event of watchRecordedInbound({
230
312
  filePath: this.#recorderPath,
231
313
  matches: (entry) => entry.provider === this.id &&
232
314
  !isOutboundRecord(entry) &&
233
315
  isAddressInChannel(entry.threadId, target.threadId ?? target.channelId ?? target.id),
234
- signal: this.#operationSignal(context.signal),
316
+ signal,
235
317
  since: context.since,
236
318
  })) {
237
319
  yield event;
@@ -251,8 +333,25 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
251
333
  this.#cleanedUp = true;
252
334
  this.#waitCursors.clear();
253
335
  this.#cleanupPromise ??= (async () => {
254
- await Promise.allSettled([...this.#inFlightSends, ...this.#inFlightWebhookRequests]);
336
+ const webhookRequests = [...this.#inFlightWebhookRequests];
337
+ const settled = Promise.allSettled(webhookRequests).then(() => undefined);
338
+ let webhookTimer;
339
+ const webhookDeadline = webhookRequests.length === 0
340
+ ? settled
341
+ : new Promise((_, reject) => {
342
+ webhookTimer = setTimeout(() => {
343
+ reject(new CrablineError(`Provider "${this.id}" webhook handlers did not settle within ${WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS}ms after cleanup.`, { kind: "timeout" }));
344
+ }, WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS);
345
+ });
255
346
  const errors = [];
347
+ const drainResults = await Promise.allSettled([
348
+ ...this.#inFlightSends,
349
+ Promise.race([settled, webhookDeadline]).finally(() => clearTimeout(webhookTimer)),
350
+ ]);
351
+ const webhookResult = drainResults.at(-1);
352
+ if (webhookResult?.status === "rejected") {
353
+ errors.push(webhookResult.reason);
354
+ }
256
355
  try {
257
356
  await this.#serverClosing;
258
357
  }
@@ -352,17 +451,18 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
352
451
  status: 200,
353
452
  });
354
453
  }
355
- async #ensureWebhookServer() {
454
+ async #ensureWebhookServer(signal) {
356
455
  if (this.#cleanupBegun) {
357
456
  throw this.#cleanedUpError();
358
457
  }
458
+ signal.throwIfAborted();
359
459
  if (this.#server) {
360
460
  return this.#server;
361
461
  }
362
462
  if (this.#serverStarting) {
363
- return await this.#serverStarting;
463
+ return await abortableOperation(this.#serverStarting, signal);
364
464
  }
365
- const resolvedConfig = resolveWhatsAppAdapterConfig(this.#config);
465
+ const resolvedConfig = resolveWhatsAppAdapterConfig(this.#config, this.#env);
366
466
  const host = this.#webhook?.host ?? DEFAULT_WHATSAPP_WEBHOOK.host;
367
467
  const port = this.#webhook?.port ?? DEFAULT_WHATSAPP_WEBHOOK.port;
368
468
  const webhookPath = this.#webhook?.path ?? DEFAULT_WHATSAPP_WEBHOOK.path;
@@ -375,26 +475,45 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
375
475
  methods: ["GET", "POST"],
376
476
  path: webhookPath,
377
477
  port,
478
+ signal: this.#lifecycleAbort.signal,
378
479
  });
379
480
  }
380
481
  catch (error) {
482
+ if (this.#cleanupBegun) {
483
+ throw error;
484
+ }
381
485
  throw new CrablineError(`whatsapp local mock webhook server failed: ${ensureErrorMessage(error)}`, { cause: error, kind: "connectivity" });
382
486
  }
383
487
  this.#server = server;
384
488
  if (this.#cleanupBegun) {
385
- throw this.#cleanedUpError();
489
+ await this.#closeStartedWebhookServer(server);
386
490
  }
387
491
  return server;
388
492
  })();
389
493
  this.#serverStarting = starting;
390
- try {
391
- return await starting;
392
- }
393
- finally {
494
+ void starting.then(() => {
495
+ if (this.#serverStarting === starting) {
496
+ this.#serverStarting = null;
497
+ }
498
+ }, () => {
394
499
  if (this.#serverStarting === starting) {
395
500
  this.#serverStarting = null;
396
501
  }
502
+ });
503
+ const server = await abortableOperation(starting, signal);
504
+ if (this.#cleanupBegun) {
505
+ throw this.#cleanedUpError();
506
+ }
507
+ return server;
508
+ }
509
+ async #closeStartedWebhookServer(server) {
510
+ if (this.#server === server) {
511
+ this.#server = null;
512
+ }
513
+ if (!this.#serverClosePromise) {
514
+ this.#serverClosePromise = server.close();
397
515
  }
516
+ await this.#serverClosePromise;
398
517
  }
399
518
  async #closeWebhookServer() {
400
519
  const starting = this.#serverStarting;
@@ -402,18 +521,20 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
402
521
  try {
403
522
  await starting;
404
523
  }
405
- catch {
406
- // A failed startup has no listener; a cleanup race publishes one below.
524
+ catch (error) {
525
+ if (error !== this.#lifecycleAbort.signal.reason) {
526
+ throw error;
527
+ }
407
528
  }
408
529
  }
409
530
  const server = this.#server;
410
- if (!server) {
531
+ if (server) {
532
+ await this.#closeStartedWebhookServer(server);
411
533
  return;
412
534
  }
413
- if (this.#server === server) {
414
- this.#server = null;
535
+ if (this.#serverClosePromise) {
536
+ await this.#serverClosePromise;
415
537
  }
416
- await server.close();
417
538
  }
418
539
  #cleanedUpError() {
419
540
  return new CrablineError(`Provider "${this.id}" has been cleaned up.`, { kind: "config" });
@@ -433,6 +554,9 @@ export function normalizeWhatsAppWebhookPayload(payload, expectedPhoneNumberId)
433
554
  }
434
555
  const normalized = [];
435
556
  if (Array.isArray(payload.entry)) {
557
+ if (optionalString(payload, "object") !== "whatsapp_business_account") {
558
+ throw new CrablineError('WhatsApp webhook entry payloads require object="whatsapp_business_account"', { kind: "inbound" });
559
+ }
436
560
  for (const entry of payload.entry) {
437
561
  if (!isRecord(entry) || !Array.isArray(entry.changes)) {
438
562
  continue;
@@ -441,6 +565,10 @@ export function normalizeWhatsAppWebhookPayload(payload, expectedPhoneNumberId)
441
565
  if (!isRecord(change)) {
442
566
  continue;
443
567
  }
568
+ const field = optionalString(change, "field");
569
+ if (field !== undefined && field !== "messages") {
570
+ continue;
571
+ }
444
572
  const value = optionalRecord(change, "value");
445
573
  if (!value || !Array.isArray(value.messages)) {
446
574
  continue;