@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.
- package/README.md +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +12 -2
- package/dist/src/openclaw/artifact-generation.js +436 -15
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +40 -3
- package/dist/src/openclaw/private-file.js +2070 -95
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +696 -187
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +352 -29
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +532 -42
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1241 -22
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1193 -118
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
package/dist/src/servers/zalo.js
CHANGED
|
@@ -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
|
|
60
|
-
|
|
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
|
-
/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
118
|
+
return fragmentRedacted;
|
|
92
119
|
}
|
|
93
|
-
const queryEnd = fragmentStart >= 0 ? fragmentStart :
|
|
94
|
-
const searchParams = new URLSearchParams(
|
|
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
|
|
123
|
+
return fragmentRedacted;
|
|
97
124
|
}
|
|
98
125
|
const search = searchParams.toString().replaceAll("%3Credacted%3E", "<redacted>");
|
|
99
|
-
return `${
|
|
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}${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
return zaloError("
|
|
505
|
+
if (state.webhookTransitionPending) {
|
|
506
|
+
drainRequestBody(request);
|
|
507
|
+
return zaloError("Webhook configuration is in progress", 409);
|
|
450
508
|
}
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
520
|
+
let admissionPending = true;
|
|
521
|
+
let retainedPollingUpdate;
|
|
464
522
|
try {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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 (
|
|
492
|
-
const error = await deliverWebhookUpdate(state,
|
|
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
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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) => {
|