@openclaw/crabline 0.1.10 → 0.1.12

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 (197) hide show
  1. package/README.md +99 -12
  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 +264 -50
  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/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +532 -42
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1241 -22
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1193 -118
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -6,7 +6,9 @@ import { recordServerEvent } from "./recorder.js";
6
6
  import { resolveMaxPendingInboundEvents } from "./pending-events.js";
7
7
  import { postWebhookRequest, validateWebhookTarget, } from "./webhook-target.js";
8
8
  const DEFAULT_WEBHOOK_DELIVERY_TIMEOUT_MS = 5_000;
9
+ const DEFAULT_MAX_PENDING_INBOUND_BYTES = 64 * 1024 * 1024;
9
10
  const MAX_ACTIVE_ZALO_WEBHOOK_VALIDATIONS = 8;
11
+ const MAX_ZALO_REDACTION_DEPTH = 32;
10
12
  const MAX_WEBHOOK_DELIVERY_TIMEOUT_MS = 30_000;
11
13
  const MAX_ZALO_WEBHOOK_SECRET_BYTES = 256;
12
14
  async function appendEvent(state, event) {
@@ -56,22 +58,39 @@ const SENSITIVE_PARAM_NAMES = new Set([
56
58
  "webhooksecret",
57
59
  ]);
58
60
  function isSensitiveParam(name) {
59
- const canonicalName = name
60
- .normalize("NFKC")
61
- .replace(/[^A-Za-z0-9]/gu, "")
62
- .toLowerCase();
61
+ const normalizedName = name.normalize("NFKC");
62
+ const canonicalName = normalizedName.replace(/[^A-Za-z0-9]/gu, "").toLowerCase();
63
63
  return (SENSITIVE_PARAM_NAMES.has(canonicalName) ||
64
- /(?:^|[_-])(?:access[_-]?token|api[_-]?key|authorization|key|password|secret|signature|token)(?:$|[_-])/iu.test(name));
65
- }
66
- function redactParams(params) {
67
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [
68
- key,
69
- isSensitiveParam(key)
70
- ? "<redacted>"
71
- : key === "url" && typeof value === "string"
72
- ? redactUrlCredentials(value)
73
- : value,
74
- ]));
64
+ /[a-z0-9](?:credentials?|keys?|passwords?|passwds?|secrets?|signatures?|tokens?)$/u.test(canonicalName) ||
65
+ /(?:^|[_-])(?:access[_-]?token|api[_-]?key|authorization|key|password|secret|signature|token)(?:$|[_-])/iu.test(normalizedName));
66
+ }
67
+ function isUrlParam(name) {
68
+ return /(?:urls?|uris?)$/iu.test(name.normalize("NFKC").replace(/[^A-Za-z0-9]/gu, ""));
69
+ }
70
+ function redactParamValue(value, key, depth) {
71
+ if (isSensitiveParam(key)) {
72
+ return "<redacted>";
73
+ }
74
+ if (Array.isArray(value)) {
75
+ if (depth >= MAX_ZALO_REDACTION_DEPTH) {
76
+ return "<redacted>";
77
+ }
78
+ return value.map((entry) => redactParamValue(entry, key, depth + 1));
79
+ }
80
+ if (isJsonObject(value)) {
81
+ if (depth >= MAX_ZALO_REDACTION_DEPTH) {
82
+ return "<redacted>";
83
+ }
84
+ return redactParams(value, depth + 1);
85
+ }
86
+ if (typeof value === "string" &&
87
+ (isUrlParam(key) || /^(?:\s*[a-z][a-z0-9+.-]*:)?\/\//iu.test(value))) {
88
+ return redactUrlCredentials(value);
89
+ }
90
+ return value;
91
+ }
92
+ function redactParams(params, depth = 0) {
93
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, redactParamValue(value, key, depth)]));
75
94
  }
76
95
  function redactSensitiveSearchParams(searchParams) {
77
96
  let redacted = false;
@@ -83,20 +102,28 @@ function redactSensitiveSearchParams(searchParams) {
83
102
  }
84
103
  return redacted;
85
104
  }
105
+ function redactUrlFragment(value) {
106
+ const fragmentStart = value.indexOf("#");
107
+ return fragmentStart < 0 ? value : `${value.slice(0, fragmentStart)}#<redacted>`;
108
+ }
86
109
  function redactMalformedUrlCredentials(value) {
87
110
  const credentialRedacted = value.replace(/^(\s*)((?:[a-z][a-z0-9+.-]*:)?\/\/)[^/?#]*@/iu, "$1$2<redacted>@");
88
- const queryStart = credentialRedacted.indexOf("?");
89
- const fragmentStart = credentialRedacted.indexOf("#", queryStart + 1);
111
+ if (credentialRedacted === value) {
112
+ return "<redacted>";
113
+ }
114
+ const fragmentRedacted = redactUrlFragment(credentialRedacted);
115
+ const queryStart = fragmentRedacted.indexOf("?");
116
+ const fragmentStart = fragmentRedacted.indexOf("#", queryStart + 1);
90
117
  if (queryStart < 0 || (fragmentStart >= 0 && fragmentStart < queryStart)) {
91
- return credentialRedacted;
118
+ return fragmentRedacted;
92
119
  }
93
- const queryEnd = fragmentStart >= 0 ? fragmentStart : credentialRedacted.length;
94
- const searchParams = new URLSearchParams(credentialRedacted.slice(queryStart + 1, queryEnd));
120
+ const queryEnd = fragmentStart >= 0 ? fragmentStart : fragmentRedacted.length;
121
+ const searchParams = new URLSearchParams(fragmentRedacted.slice(queryStart + 1, queryEnd));
95
122
  if (!redactSensitiveSearchParams(searchParams)) {
96
- return credentialRedacted;
123
+ return fragmentRedacted;
97
124
  }
98
125
  const search = searchParams.toString().replaceAll("%3Credacted%3E", "<redacted>");
99
- return `${credentialRedacted.slice(0, queryStart)}?${search}${credentialRedacted.slice(queryEnd)}`;
126
+ return `${fragmentRedacted.slice(0, queryStart)}?${search}${fragmentRedacted.slice(queryEnd)}`;
100
127
  }
101
128
  function redactUrlCredentials(value) {
102
129
  let url;
@@ -108,11 +135,12 @@ function redactUrlCredentials(value) {
108
135
  }
109
136
  const hasCredentials = Boolean(url.username || url.password);
110
137
  const redactedQuery = redactSensitiveSearchParams(url.searchParams);
138
+ const hasFragment = url.hash.length > 0;
111
139
  if (!hasCredentials && !redactedQuery) {
112
- return value;
140
+ return hasFragment ? redactUrlFragment(value) : value;
113
141
  }
114
142
  const search = url.search.replaceAll("%3Credacted%3E", "<redacted>");
115
- return `${url.protocol}//${hasCredentials ? "<redacted>@" : ""}${url.host}${url.pathname}${search}${url.hash}`;
143
+ return `${url.protocol}//${hasCredentials ? "<redacted>@" : ""}${url.host}${url.pathname}${search}${hasFragment ? "#<redacted>" : ""}`;
116
144
  }
117
145
  function requireParam(body, name) {
118
146
  return readTrimmedString(body[name]) ?? zaloError(`${name} is required`, 400);
@@ -126,6 +154,15 @@ function webhookDeliveryTimeoutMs(value) {
126
154
  }
127
155
  return Math.max(1, Math.min(Math.floor(value), MAX_WEBHOOK_DELIVERY_TIMEOUT_MS));
128
156
  }
157
+ function maxPendingInboundBytes(value) {
158
+ if (value === undefined) {
159
+ return DEFAULT_MAX_PENDING_INBOUND_BYTES;
160
+ }
161
+ if (!Number.isSafeInteger(value) || value < 1) {
162
+ throw new Error("maxPendingInboundBytes must be a positive safe integer.");
163
+ }
164
+ return value;
165
+ }
129
166
  async function validateWebhookUrl(url, state, signal) {
130
167
  const target = await validateWebhookTarget({
131
168
  allowLoopbackHttp: state.allowLoopbackHttpWebhook,
@@ -222,9 +259,7 @@ function nextUpdate(request, response, state) {
222
259
  if (!update) {
223
260
  return undefined;
224
261
  }
225
- const order = pollingUpdateOrder(state, update);
226
- if (state.failedUpdateOrders.has(order) &&
227
- [...state.reservedUpdateOrders].some((reservedOrder) => reservedOrder < order)) {
262
+ if (hasEarlierReservedPollingUpdate(state, update)) {
228
263
  return undefined;
229
264
  }
230
265
  state.updates.shift();
@@ -240,6 +275,35 @@ function pollingUpdateOrder(state, update) {
240
275
  state.updateOrders.set(update, order);
241
276
  return order;
242
277
  }
278
+ function pollingUpdateBytes(state, update) {
279
+ const existing = state.updateBytes.get(update);
280
+ if (existing !== undefined) {
281
+ return existing;
282
+ }
283
+ const bytes = Buffer.byteLength(JSON.stringify(update), "utf8");
284
+ state.updateBytes.set(update, bytes);
285
+ return bytes;
286
+ }
287
+ function retainPollingUpdate(state, update) {
288
+ const bytes = pollingUpdateBytes(state, update);
289
+ if (state.retainedUpdateCount >= state.maxPendingInboundEvents ||
290
+ state.retainedUpdateBytes + bytes > state.maxPendingInboundBytes) {
291
+ state.updateBytes.delete(update);
292
+ return false;
293
+ }
294
+ state.retainedUpdateCount += 1;
295
+ state.retainedUpdateBytes += bytes;
296
+ return true;
297
+ }
298
+ function releasePollingUpdate(state, update) {
299
+ const bytes = state.updateBytes.get(update);
300
+ if (bytes === undefined) {
301
+ throw new Error("Zalo polling update retention was released without a reservation.");
302
+ }
303
+ state.updateBytes.delete(update);
304
+ state.retainedUpdateCount -= 1;
305
+ state.retainedUpdateBytes -= bytes;
306
+ }
243
307
  function queuePollingUpdate(state, update) {
244
308
  const order = pollingUpdateOrder(state, update);
245
309
  const insertionIndex = state.updates.findIndex((queued) => pollingUpdateOrder(state, queued) > order);
@@ -250,9 +314,12 @@ function queuePollingUpdate(state, update) {
250
314
  state.updates.splice(insertionIndex, 0, update);
251
315
  }
252
316
  }
317
+ function hasEarlierReservedPollingUpdate(state, update) {
318
+ const order = pollingUpdateOrder(state, update);
319
+ return [...state.reservedUpdateOrders].some((reservedOrder) => reservedOrder < order);
320
+ }
253
321
  function reservePollingUpdate(state, update) {
254
322
  const order = pollingUpdateOrder(state, update);
255
- state.failedUpdateOrders.delete(order);
256
323
  state.reservedUpdateOrders.add(order);
257
324
  }
258
325
  function flushPendingPollingUpdate(state) {
@@ -265,9 +332,7 @@ function flushPendingPollingUpdate(state) {
265
332
  settlePendingUpdate(state, pending, { kind: "disconnect" });
266
333
  return;
267
334
  }
268
- const order = pollingUpdateOrder(state, update);
269
- if (state.failedUpdateOrders.has(order) &&
270
- [...state.reservedUpdateOrders].some((reservedOrder) => reservedOrder < order)) {
335
+ if (hasEarlierReservedPollingUpdate(state, update)) {
271
336
  return;
272
337
  }
273
338
  state.updates.shift();
@@ -276,7 +341,7 @@ function flushPendingPollingUpdate(state) {
276
341
  }
277
342
  function reservedUpdateResponse(state, update) {
278
343
  let settled = false;
279
- const release = () => {
344
+ const releaseReservation = () => {
280
345
  if (settled) {
281
346
  return false;
282
347
  }
@@ -286,15 +351,15 @@ function reservedUpdateResponse(state, update) {
286
351
  };
287
352
  return {
288
353
  onWriteFailure() {
289
- if (!release()) {
354
+ if (!releaseReservation()) {
290
355
  return;
291
356
  }
292
- state.failedUpdateOrders.add(pollingUpdateOrder(state, update));
293
357
  queuePollingUpdate(state, update);
294
358
  flushPendingPollingUpdate(state);
295
359
  },
296
360
  onWriteSuccess() {
297
- if (release()) {
361
+ if (releaseReservation()) {
362
+ releasePollingUpdate(state, update);
298
363
  flushPendingPollingUpdate(state);
299
364
  }
300
365
  },
@@ -374,28 +439,21 @@ async function waitForUpdate(request, response, state, timeoutSeconds) {
374
439
  }
375
440
  function deliverPollingUpdate(state, update) {
376
441
  pollingUpdateOrder(state, update);
377
- if (state.updates.length > 0) {
378
- if (state.updates.length + state.reservedUpdateOrders.size >= state.maxPendingInboundEvents) {
379
- return false;
380
- }
442
+ if (state.updates.length > 0 || hasEarlierReservedPollingUpdate(state, update)) {
381
443
  queuePollingUpdate(state, update);
382
444
  flushPendingPollingUpdate(state);
383
- return true;
445
+ return;
384
446
  }
385
447
  const pending = state.pendingRequest;
386
448
  if (pending) {
387
449
  if (isPendingUpdateRequestLive(pending)) {
388
450
  reservePollingUpdate(state, update);
389
451
  settlePendingUpdate(state, pending, { kind: "update", update });
390
- return true;
452
+ return;
391
453
  }
392
454
  settlePendingUpdate(state, pending, { kind: "disconnect" });
393
455
  }
394
- if (state.updates.length + state.reservedUpdateOrders.size >= state.maxPendingInboundEvents) {
395
- return false;
396
- }
397
456
  queuePollingUpdate(state, update);
398
- return true;
399
457
  }
400
458
  async function deliverWebhookUpdate(state, webhook, update) {
401
459
  const deadlineAt = Date.now() + state.webhookDeliveryTimeoutMs;
@@ -444,28 +502,37 @@ async function handleAdminInbound(request, state, url) {
444
502
  drainRequestBody(request);
445
503
  return adminAuthError();
446
504
  }
447
- const parsedBody = await parseUnknownRequestBody(request);
448
- if (!isJsonObject(parsedBody)) {
449
- return zaloError("Bad Request: can't parse JSON object", 400);
505
+ if (state.webhookTransitionPending) {
506
+ drainRequestBody(request);
507
+ return zaloError("Webhook configuration is in progress", 409);
450
508
  }
451
- const body = parsedBody;
452
- const chatId = requireParam(body, "chatId");
453
- const senderId = requireParam(body, "senderId");
454
- const text = requireParam(body, "text");
455
- if (chatId instanceof Response || senderId instanceof Response || text instanceof Response) {
456
- return [chatId, senderId, text].find((value) => value instanceof Response);
509
+ const retainedUpdates = state.webhook?.url === undefined ? state.retainedUpdateCount : 0;
510
+ if (retainedUpdates + state.pendingInboundAdmissions >= state.maxPendingInboundEvents) {
511
+ drainRequestBody(request);
512
+ return zaloError(`Pending inbound queue is full (${state.maxPendingInboundEvents} updates)`, 429);
457
513
  }
514
+ state.pendingInboundAdmissions += 1;
458
515
  let releaseAdmission;
459
516
  const previousAdmission = state.inboundAdmission;
460
517
  state.inboundAdmission = new Promise((resolve) => {
461
518
  releaseAdmission = resolve;
462
519
  });
463
- await previousAdmission;
520
+ let admissionPending = true;
521
+ let retainedPollingUpdate;
464
522
  try {
465
- if (!state.webhook?.url &&
466
- state.updates.length + state.reservedUpdateOrders.size >= state.maxPendingInboundEvents) {
467
- return zaloError(`Pending inbound queue is full (${state.maxPendingInboundEvents} updates)`, 429);
468
- }
523
+ const parsedBody = await parseUnknownRequestBody(request);
524
+ await previousAdmission;
525
+ if (!isJsonObject(parsedBody)) {
526
+ return zaloError("Bad Request: can't parse JSON object", 400);
527
+ }
528
+ const body = parsedBody;
529
+ const chatId = requireParam(body, "chatId");
530
+ const senderId = requireParam(body, "senderId");
531
+ const text = requireParam(body, "text");
532
+ if (chatId instanceof Response || senderId instanceof Response || text instanceof Response) {
533
+ return [chatId, senderId, text].find((value) => value instanceof Response);
534
+ }
535
+ const webhook = state.webhook;
469
536
  const update = {
470
537
  event_name: "message.text.received",
471
538
  message: {
@@ -480,27 +547,43 @@ async function handleAdminInbound(request, state, url) {
480
547
  text,
481
548
  },
482
549
  };
550
+ if (!webhook?.url) {
551
+ if (!retainPollingUpdate(state, update)) {
552
+ return zaloError(`Pending inbound queue is full (${state.maxPendingInboundEvents} updates)`, 429);
553
+ }
554
+ retainedPollingUpdate = update;
555
+ state.pendingInboundAdmissions -= 1;
556
+ admissionPending = false;
557
+ }
483
558
  await appendEvent(state, {
484
559
  at: new Date().toISOString(),
485
- body,
560
+ body: redactParams(body),
486
561
  method: request.method ?? "POST",
487
562
  path: url.pathname,
488
- query: queryRecord(url),
563
+ query: redactParams(queryRecord(url)),
489
564
  type: "admin",
490
565
  });
491
- if (state.webhook?.url) {
492
- const error = await deliverWebhookUpdate(state, state.webhook, update);
566
+ if (webhook?.url) {
567
+ const error = await deliverWebhookUpdate(state, webhook, update);
493
568
  if (error) {
494
569
  return error;
495
570
  }
496
571
  }
497
- else if (!deliverPollingUpdate(state, update)) {
498
- return zaloError(`Pending inbound queue is full (${state.maxPendingInboundEvents} updates)`, 429);
572
+ else {
573
+ deliverPollingUpdate(state, update);
574
+ retainedPollingUpdate = undefined;
499
575
  }
500
576
  return zaloOk(update);
501
577
  }
502
578
  finally {
579
+ if (retainedPollingUpdate) {
580
+ releasePollingUpdate(state, retainedPollingUpdate);
581
+ }
582
+ await previousAdmission;
503
583
  releaseAdmission();
584
+ if (admissionPending) {
585
+ state.pendingInboundAdmissions -= 1;
586
+ }
504
587
  }
505
588
  }
506
589
  async function handleZaloMethod(request, response, state, url, method) {
@@ -533,17 +616,22 @@ async function handleZaloMethod(request, response, state, url, method) {
533
616
  }
534
617
  return sendResponse;
535
618
  }
536
- await appendEvent(state, event);
537
619
  if (method === "getMe") {
538
- return zaloOk({
620
+ const getMeResponse = zaloOk({
539
621
  account_name: state.botName,
540
622
  account_type: "BASIC",
541
623
  can_join_groups: true,
542
624
  id: state.botId,
543
625
  });
626
+ event.accepted = getMeResponse.ok;
627
+ await appendEvent(state, event);
628
+ return getMeResponse;
629
+ }
630
+ if (method !== "setWebhook") {
631
+ await appendEvent(state, event);
544
632
  }
545
633
  if (method === "getUpdates") {
546
- if (state.webhook?.url) {
634
+ if (state.webhook?.url || state.webhookTransitionPending) {
547
635
  return zaloError("Webhook is configured; delete it before using getUpdates", 400);
548
636
  }
549
637
  const parsedTimeout = Number(readTrimmedString(body.timeout) ?? "30");
@@ -563,48 +651,66 @@ async function handleZaloMethod(request, response, state, url, method) {
563
651
  return zaloOk();
564
652
  }
565
653
  if (method === "setWebhook") {
654
+ const reject = async (result) => {
655
+ if (isJsonObject(event.body) && "url" in event.body) {
656
+ event.body.url = "<redacted>";
657
+ }
658
+ await appendEvent(state, event);
659
+ return result;
660
+ };
566
661
  const webhookUrl = requireParam(body, "url");
567
662
  const secretToken = requireParam(body, "secret_token");
568
663
  const error = firstError(webhookUrl, secretToken);
569
664
  if (error) {
570
- return error;
665
+ return await reject(error);
571
666
  }
572
667
  if (typeof webhookUrl !== "string" || typeof secretToken !== "string") {
573
- return zaloError("Invalid webhook parameters", 400);
668
+ return await reject(zaloError("Invalid webhook parameters", 400));
574
669
  }
575
670
  const secretError = validateZaloWebhookSecret(secretToken);
576
671
  if (secretError) {
577
- return secretError;
672
+ return await reject(secretError);
578
673
  }
579
674
  let parsedUrl;
580
675
  try {
581
676
  parsedUrl = new URL(webhookUrl);
582
677
  }
583
678
  catch {
584
- return zaloError("url must be a valid HTTPS URL", 400);
679
+ return await reject(zaloError("url must be a valid HTTPS URL", 400));
585
680
  }
586
681
  if (state.activeWebhookValidations.size >= MAX_ACTIVE_ZALO_WEBHOOK_VALIDATIONS) {
587
- return zaloError("Too many webhook validations", 429);
682
+ return await reject(zaloError("Too many webhook validations", 429));
588
683
  }
589
684
  let target;
590
685
  try {
591
686
  target = await validateWebhookUrlWithDeadline(parsedUrl, state, Date.now() + state.webhookDeliveryTimeoutMs);
592
687
  }
593
688
  catch {
594
- return zaloError("url host could not be resolved", 400);
689
+ return await reject(zaloError("url host could not be resolved", 400));
595
690
  }
596
691
  if (target instanceof Response) {
597
- return target;
692
+ return await reject(target);
598
693
  }
599
- if (state.reservedUpdateOrders.size > 0) {
600
- return zaloError("Polling deliveries are still in progress", 409);
694
+ if (state.webhookTransitionPending || state.reservedUpdateOrders.size > 0) {
695
+ return await reject(zaloError("Polling deliveries are still in progress", 409));
601
696
  }
602
697
  const webhook = { secretToken, updatedAt: Date.now(), url: parsedUrl.href };
603
- if (state.pendingRequest) {
604
- settlePendingUpdate(state, state.pendingRequest, { kind: "conflict" });
698
+ state.webhookTransitionPending = true;
699
+ try {
700
+ await state.inboundAdmission;
701
+ if (state.reservedUpdateOrders.size > 0) {
702
+ return await reject(zaloError("Polling deliveries are still in progress", 409));
703
+ }
704
+ await appendEvent(state, event);
705
+ if (state.pendingRequest) {
706
+ settlePendingUpdate(state, state.pendingRequest, { kind: "conflict" });
707
+ }
708
+ state.webhook = webhook;
709
+ return zaloOk({ updated_at: webhook.updatedAt, url: webhook.url });
710
+ }
711
+ finally {
712
+ state.webhookTransitionPending = false;
605
713
  }
606
- state.webhook = webhook;
607
- return zaloOk({ updated_at: webhook.updatedAt, url: webhook.url });
608
714
  }
609
715
  if (method === "deleteWebhook") {
610
716
  const updatedAt = Date.now();
@@ -628,20 +734,25 @@ export async function startZaloServer(params = {}) {
628
734
  botToken: params.botToken ??
629
735
  (isLoopbackHost(host) ? "crabline-zalo-bot-token" : randomBytes(32).toString("base64url")),
630
736
  closing: false,
631
- failedUpdateOrders: new Set(),
632
737
  inboundAdmission: Promise.resolve(),
738
+ maxPendingInboundBytes: maxPendingInboundBytes(params.maxPendingInboundBytes),
633
739
  maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
634
740
  nextMessage: 1,
635
741
  nextUpdateOrder: 1,
636
742
  onEvent: params.onEvent,
743
+ pendingInboundAdmissions: 0,
637
744
  pendingRequest: undefined,
638
745
  recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "zalo.jsonl"),
746
+ retainedUpdateBytes: 0,
747
+ retainedUpdateCount: 0,
639
748
  reservedUpdateOrders: new Set(),
640
749
  restrictWebhookTargets: true,
750
+ updateBytes: new WeakMap(),
641
751
  updateOrders: new WeakMap(),
642
752
  updates: [],
643
753
  webhook: undefined,
644
754
  webhookDeliveryTimeoutMs: webhookDeliveryTimeoutMs(params.webhookDeliveryTimeoutMs),
755
+ webhookTransitionPending: false,
645
756
  };
646
757
  const httpServer = await startHttpJsonServer({
647
758
  handle: async (request, response) => {