@openclaw/crabline 0.1.8 → 0.1.10
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 +52 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +23 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +195 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +2 -0
- package/dist/src/servers/matrix.js +466 -52
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +7 -0
- package/dist/src/servers/mattermost.js +307 -59
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +12 -0
- package/dist/src/servers/recorder.js +51 -0
- package/dist/src/servers/recorder.js.map +1 -0
- package/dist/src/servers/signal.d.ts +4 -0
- package/dist/src/servers/signal.js +445 -57
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +7 -0
- package/dist/src/servers/slack.js +516 -50
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +113 -0
- package/dist/src/servers/telegram.js +814 -155
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +14 -7
- package/dist/src/servers/whatsapp.js +272 -110
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +16 -0
- package/dist/src/servers/zalo.js +524 -91
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
package/dist/src/servers/zalo.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import { validateHeaderValue, } from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { adminAuthError, hasAdminToken, jsonResponse,
|
|
4
|
+
import { adminAuthError, drainRequestBody, hasAdminToken, InvalidJsonBodyError, isJsonObject, isLoopbackHost, jsonResponse, parseUnknownRequestBody, queryRecord, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
|
|
5
|
+
import { recordServerEvent } from "./recorder.js";
|
|
6
|
+
import { resolveMaxPendingInboundEvents } from "./pending-events.js";
|
|
7
|
+
import { postWebhookRequest, validateWebhookTarget, } from "./webhook-target.js";
|
|
8
|
+
const DEFAULT_WEBHOOK_DELIVERY_TIMEOUT_MS = 5_000;
|
|
9
|
+
const MAX_ACTIVE_ZALO_WEBHOOK_VALIDATIONS = 8;
|
|
10
|
+
const MAX_WEBHOOK_DELIVERY_TIMEOUT_MS = 30_000;
|
|
11
|
+
const MAX_ZALO_WEBHOOK_SECRET_BYTES = 256;
|
|
5
12
|
async function appendEvent(state, event) {
|
|
6
|
-
await
|
|
7
|
-
await fs.appendFile(state.recorderPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
13
|
+
await recordServerEvent({ event, onEvent: state.onEvent, recorderPath: state.recorderPath });
|
|
8
14
|
}
|
|
9
15
|
function zaloOk(result) {
|
|
10
16
|
return jsonResponse(result === undefined ? { ok: true } : { ok: true, result });
|
|
@@ -21,126 +27,513 @@ function readChatType(value) {
|
|
|
21
27
|
function requestParams(url, body) {
|
|
22
28
|
return { ...Object.fromEntries(url.searchParams.entries()), ...body };
|
|
23
29
|
}
|
|
30
|
+
const SENSITIVE_PARAM_NAMES = new Set([
|
|
31
|
+
"accesskey",
|
|
32
|
+
"accesstoken",
|
|
33
|
+
"apikey",
|
|
34
|
+
"auth",
|
|
35
|
+
"authorization",
|
|
36
|
+
"authtoken",
|
|
37
|
+
"bearertoken",
|
|
38
|
+
"clientsecret",
|
|
39
|
+
"consumersecret",
|
|
40
|
+
"credential",
|
|
41
|
+
"credentials",
|
|
42
|
+
"idtoken",
|
|
43
|
+
"key",
|
|
44
|
+
"oauthtoken",
|
|
45
|
+
"password",
|
|
46
|
+
"passwd",
|
|
47
|
+
"privatekey",
|
|
48
|
+
"refreshtoken",
|
|
49
|
+
"secret",
|
|
50
|
+
"secretkey",
|
|
51
|
+
"secrettoken",
|
|
52
|
+
"sessiontoken",
|
|
53
|
+
"signature",
|
|
54
|
+
"signingsecret",
|
|
55
|
+
"token",
|
|
56
|
+
"webhooksecret",
|
|
57
|
+
]);
|
|
58
|
+
function isSensitiveParam(name) {
|
|
59
|
+
const canonicalName = name
|
|
60
|
+
.normalize("NFKC")
|
|
61
|
+
.replace(/[^A-Za-z0-9]/gu, "")
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
return (SENSITIVE_PARAM_NAMES.has(canonicalName) ||
|
|
64
|
+
/(?:^|[_-])(?:access[_-]?token|api[_-]?key|authorization|key|password|secret|signature|token)(?:$|[_-])/iu.test(name));
|
|
65
|
+
}
|
|
24
66
|
function redactParams(params) {
|
|
25
67
|
return Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
26
68
|
key,
|
|
27
|
-
key
|
|
69
|
+
isSensitiveParam(key)
|
|
70
|
+
? "<redacted>"
|
|
71
|
+
: key === "url" && typeof value === "string"
|
|
72
|
+
? redactUrlCredentials(value)
|
|
73
|
+
: value,
|
|
28
74
|
]));
|
|
29
75
|
}
|
|
76
|
+
function redactSensitiveSearchParams(searchParams) {
|
|
77
|
+
let redacted = false;
|
|
78
|
+
for (const key of searchParams.keys()) {
|
|
79
|
+
if (isSensitiveParam(key)) {
|
|
80
|
+
searchParams.set(key, "<redacted>");
|
|
81
|
+
redacted = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return redacted;
|
|
85
|
+
}
|
|
86
|
+
function redactMalformedUrlCredentials(value) {
|
|
87
|
+
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);
|
|
90
|
+
if (queryStart < 0 || (fragmentStart >= 0 && fragmentStart < queryStart)) {
|
|
91
|
+
return credentialRedacted;
|
|
92
|
+
}
|
|
93
|
+
const queryEnd = fragmentStart >= 0 ? fragmentStart : credentialRedacted.length;
|
|
94
|
+
const searchParams = new URLSearchParams(credentialRedacted.slice(queryStart + 1, queryEnd));
|
|
95
|
+
if (!redactSensitiveSearchParams(searchParams)) {
|
|
96
|
+
return credentialRedacted;
|
|
97
|
+
}
|
|
98
|
+
const search = searchParams.toString().replaceAll("%3Credacted%3E", "<redacted>");
|
|
99
|
+
return `${credentialRedacted.slice(0, queryStart)}?${search}${credentialRedacted.slice(queryEnd)}`;
|
|
100
|
+
}
|
|
101
|
+
function redactUrlCredentials(value) {
|
|
102
|
+
let url;
|
|
103
|
+
try {
|
|
104
|
+
url = new URL(value);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return redactMalformedUrlCredentials(value);
|
|
108
|
+
}
|
|
109
|
+
const hasCredentials = Boolean(url.username || url.password);
|
|
110
|
+
const redactedQuery = redactSensitiveSearchParams(url.searchParams);
|
|
111
|
+
if (!hasCredentials && !redactedQuery) {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
const search = url.search.replaceAll("%3Credacted%3E", "<redacted>");
|
|
115
|
+
return `${url.protocol}//${hasCredentials ? "<redacted>@" : ""}${url.host}${url.pathname}${search}${url.hash}`;
|
|
116
|
+
}
|
|
30
117
|
function requireParam(body, name) {
|
|
31
118
|
return readTrimmedString(body[name]) ?? zaloError(`${name} is required`, 400);
|
|
32
119
|
}
|
|
33
120
|
function firstError(...values) {
|
|
34
121
|
return values.find((value) => value instanceof Response);
|
|
35
122
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
123
|
+
function webhookDeliveryTimeoutMs(value) {
|
|
124
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
125
|
+
return DEFAULT_WEBHOOK_DELIVERY_TIMEOUT_MS;
|
|
126
|
+
}
|
|
127
|
+
return Math.max(1, Math.min(Math.floor(value), MAX_WEBHOOK_DELIVERY_TIMEOUT_MS));
|
|
128
|
+
}
|
|
129
|
+
async function validateWebhookUrl(url, state, signal) {
|
|
130
|
+
const target = await validateWebhookTarget({
|
|
131
|
+
allowLoopbackHttp: state.allowLoopbackHttpWebhook,
|
|
132
|
+
restrictPrivateAddresses: state.restrictWebhookTargets,
|
|
133
|
+
signal,
|
|
134
|
+
url,
|
|
135
|
+
});
|
|
136
|
+
if ("error" in target) {
|
|
137
|
+
return target.error === "unresolvable"
|
|
138
|
+
? zaloError("url host could not be resolved", 400)
|
|
139
|
+
: target.error === "private-address"
|
|
140
|
+
? zaloError("url must not target a private or link-local address", 400)
|
|
141
|
+
: zaloError("url must use HTTPS", 400);
|
|
142
|
+
}
|
|
143
|
+
return target;
|
|
144
|
+
}
|
|
145
|
+
function validateZaloWebhookSecret(secretToken) {
|
|
146
|
+
if (Buffer.byteLength(secretToken) > MAX_ZALO_WEBHOOK_SECRET_BYTES) {
|
|
147
|
+
return zaloError(`secret_token must not exceed ${MAX_ZALO_WEBHOOK_SECRET_BYTES} bytes`, 400);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
validateHeaderValue("x-bot-api-secret-token", secretToken);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return zaloError("secret_token contains invalid HTTP header characters", 400);
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
async function validateWebhookUrlWithDeadline(url, state, deadlineAt) {
|
|
158
|
+
const remainingMs = deadlineAt - Date.now();
|
|
159
|
+
if (remainingMs <= 0) {
|
|
160
|
+
throw webhookTimeoutError(state.webhookDeliveryTimeoutMs);
|
|
161
|
+
}
|
|
162
|
+
const validation = new AbortController();
|
|
163
|
+
state.activeWebhookValidations.add(validation);
|
|
164
|
+
const timer = setTimeout(() => validation.abort(webhookTimeoutError(state.webhookDeliveryTimeoutMs)), remainingMs);
|
|
165
|
+
timer.unref();
|
|
166
|
+
try {
|
|
167
|
+
return await validateWebhookUrl(url, state, validation.signal);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
state.activeWebhookValidations.delete(validation);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function webhookTimeoutError(timeoutMs) {
|
|
175
|
+
return new DOMException(`Webhook delivery timed out after ${timeoutMs}ms`, "TimeoutError");
|
|
176
|
+
}
|
|
177
|
+
/** @internal */
|
|
178
|
+
export async function postZaloWebhook(params) {
|
|
179
|
+
const activeRequests = params.activeRequests ?? new Set();
|
|
180
|
+
const deadlineAt = Date.now() + params.timeoutMs;
|
|
181
|
+
const addresses = params.addresses && params.addresses.length > 0 ? params.addresses : [undefined];
|
|
182
|
+
let lastError;
|
|
183
|
+
for (const [index, address] of addresses.entries()) {
|
|
184
|
+
if (params.shouldCancel?.()) {
|
|
185
|
+
throw new Error("Webhook delivery cancelled.");
|
|
186
|
+
}
|
|
187
|
+
const remainingMs = deadlineAt - Date.now();
|
|
188
|
+
if (remainingMs <= 0) {
|
|
189
|
+
throw webhookTimeoutError(params.timeoutMs);
|
|
190
|
+
}
|
|
191
|
+
const attemptsRemaining = addresses.length - index;
|
|
192
|
+
const attemptTimeoutMs = Math.max(1, Math.floor(remainingMs / attemptsRemaining));
|
|
193
|
+
try {
|
|
194
|
+
return await postWebhookRequest({
|
|
195
|
+
activeRequests,
|
|
196
|
+
address,
|
|
197
|
+
body: params.body,
|
|
198
|
+
headerEntries: [["x-bot-api-secret-token", params.verificationValue]],
|
|
199
|
+
timeoutMs: attemptTimeoutMs,
|
|
200
|
+
url: params.url,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (params.shouldCancel?.()) {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
lastError = error;
|
|
208
|
+
if (error instanceof DOMException &&
|
|
209
|
+
error.name === "TimeoutError" &&
|
|
210
|
+
attemptsRemaining === 1) {
|
|
211
|
+
throw webhookTimeoutError(params.timeoutMs);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw lastError;
|
|
216
|
+
}
|
|
217
|
+
function nextUpdate(request, response, state) {
|
|
218
|
+
if (request.aborted || request.socket.destroyed || response.destroyed) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const update = state.updates[0];
|
|
222
|
+
if (!update) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const order = pollingUpdateOrder(state, update);
|
|
226
|
+
if (state.failedUpdateOrders.has(order) &&
|
|
227
|
+
[...state.reservedUpdateOrders].some((reservedOrder) => reservedOrder < order)) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
state.updates.shift();
|
|
231
|
+
reservePollingUpdate(state, update);
|
|
232
|
+
return reservedUpdateResponse(state, update);
|
|
233
|
+
}
|
|
234
|
+
function pollingUpdateOrder(state, update) {
|
|
235
|
+
const existing = state.updateOrders.get(update);
|
|
236
|
+
if (existing !== undefined) {
|
|
237
|
+
return existing;
|
|
238
|
+
}
|
|
239
|
+
const order = state.nextUpdateOrder++;
|
|
240
|
+
state.updateOrders.set(update, order);
|
|
241
|
+
return order;
|
|
242
|
+
}
|
|
243
|
+
function queuePollingUpdate(state, update) {
|
|
244
|
+
const order = pollingUpdateOrder(state, update);
|
|
245
|
+
const insertionIndex = state.updates.findIndex((queued) => pollingUpdateOrder(state, queued) > order);
|
|
246
|
+
if (insertionIndex < 0) {
|
|
247
|
+
state.updates.push(update);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
state.updates.splice(insertionIndex, 0, update);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function reservePollingUpdate(state, update) {
|
|
254
|
+
const order = pollingUpdateOrder(state, update);
|
|
255
|
+
state.failedUpdateOrders.delete(order);
|
|
256
|
+
state.reservedUpdateOrders.add(order);
|
|
257
|
+
}
|
|
258
|
+
function flushPendingPollingUpdate(state) {
|
|
259
|
+
const pending = state.pendingRequest;
|
|
260
|
+
const update = state.updates[0];
|
|
261
|
+
if (!pending || !update) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!isPendingUpdateRequestLive(pending)) {
|
|
265
|
+
settlePendingUpdate(state, pending, { kind: "disconnect" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const order = pollingUpdateOrder(state, update);
|
|
269
|
+
if (state.failedUpdateOrders.has(order) &&
|
|
270
|
+
[...state.reservedUpdateOrders].some((reservedOrder) => reservedOrder < order)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
state.updates.shift();
|
|
274
|
+
reservePollingUpdate(state, update);
|
|
275
|
+
settlePendingUpdate(state, pending, { kind: "update", update });
|
|
276
|
+
}
|
|
277
|
+
function reservedUpdateResponse(state, update) {
|
|
278
|
+
let settled = false;
|
|
279
|
+
const release = () => {
|
|
280
|
+
if (settled) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
settled = true;
|
|
284
|
+
state.reservedUpdateOrders.delete(pollingUpdateOrder(state, update));
|
|
285
|
+
return true;
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
onWriteFailure() {
|
|
289
|
+
if (!release()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
state.failedUpdateOrders.add(pollingUpdateOrder(state, update));
|
|
293
|
+
queuePollingUpdate(state, update);
|
|
294
|
+
flushPendingPollingUpdate(state);
|
|
295
|
+
},
|
|
296
|
+
onWriteSuccess() {
|
|
297
|
+
if (release()) {
|
|
298
|
+
flushPendingPollingUpdate(state);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
response: zaloOk(update),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function settlePendingUpdate(state, pending, result) {
|
|
305
|
+
if (!pending.active) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
pending.active = false;
|
|
309
|
+
if (state.pendingRequest === pending) {
|
|
310
|
+
state.pendingRequest = undefined;
|
|
311
|
+
}
|
|
312
|
+
if (pending.timeout) {
|
|
313
|
+
clearTimeout(pending.timeout);
|
|
314
|
+
}
|
|
315
|
+
pending.request.socket.off("close", pending.onDisconnect);
|
|
316
|
+
pending.request.off("aborted", pending.onDisconnect);
|
|
317
|
+
pending.response.off("close", pending.onDisconnect);
|
|
318
|
+
pending.resolve(result);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
function isPendingUpdateRequestLive(pending) {
|
|
322
|
+
return (!pending.request.aborted && !pending.request.socket.destroyed && !pending.response.destroyed);
|
|
39
323
|
}
|
|
40
|
-
function waitForUpdate(state, timeoutSeconds) {
|
|
41
|
-
|
|
324
|
+
async function waitForUpdate(request, response, state, timeoutSeconds) {
|
|
325
|
+
if (request.aborted || request.socket.destroyed || response.destroyed) {
|
|
326
|
+
return zaloError("Client closed request", 499);
|
|
327
|
+
}
|
|
328
|
+
const previous = state.pendingRequest;
|
|
329
|
+
if (previous) {
|
|
330
|
+
settlePendingUpdate(state, previous, { kind: "conflict" });
|
|
331
|
+
}
|
|
332
|
+
const queued = nextUpdate(request, response, state);
|
|
42
333
|
if (queued) {
|
|
43
|
-
return
|
|
334
|
+
return queued;
|
|
44
335
|
}
|
|
45
336
|
if (timeoutSeconds <= 0) {
|
|
46
|
-
return
|
|
337
|
+
return zaloError("Request timeout", 408);
|
|
47
338
|
}
|
|
48
|
-
|
|
339
|
+
const result = await new Promise((resolve) => {
|
|
49
340
|
const pending = {
|
|
341
|
+
active: true,
|
|
342
|
+
onDisconnect: () => {
|
|
343
|
+
settlePendingUpdate(state, pending, { kind: "disconnect" });
|
|
344
|
+
},
|
|
345
|
+
request,
|
|
346
|
+
response,
|
|
50
347
|
resolve,
|
|
51
|
-
timeout:
|
|
52
|
-
const index = state.pendingRequests.indexOf(pending);
|
|
53
|
-
if (index >= 0) {
|
|
54
|
-
state.pendingRequests.splice(index, 1);
|
|
55
|
-
}
|
|
56
|
-
resolve(zaloError("Request timeout", 408));
|
|
57
|
-
}, timeoutSeconds * 1000),
|
|
348
|
+
timeout: undefined,
|
|
58
349
|
};
|
|
59
|
-
state.
|
|
350
|
+
state.pendingRequest = pending;
|
|
351
|
+
request.once("aborted", pending.onDisconnect);
|
|
352
|
+
request.socket.once("close", pending.onDisconnect);
|
|
353
|
+
response.once("close", pending.onDisconnect);
|
|
354
|
+
pending.timeout = setTimeout(() => {
|
|
355
|
+
settlePendingUpdate(state, pending, { kind: "timeout" });
|
|
356
|
+
}, timeoutSeconds * 1000);
|
|
357
|
+
pending.timeout.unref();
|
|
358
|
+
if (request.socket.destroyed || response.destroyed) {
|
|
359
|
+
pending.onDisconnect();
|
|
360
|
+
}
|
|
60
361
|
});
|
|
362
|
+
switch (result.kind) {
|
|
363
|
+
case "conflict":
|
|
364
|
+
return zaloError("Conflict: terminated by other getUpdates request", 409);
|
|
365
|
+
case "disconnect":
|
|
366
|
+
return zaloError("Client closed request", 499);
|
|
367
|
+
case "shutdown":
|
|
368
|
+
return zaloError("Server shutting down", 503);
|
|
369
|
+
case "timeout":
|
|
370
|
+
return zaloError("Request timeout", 408);
|
|
371
|
+
case "update":
|
|
372
|
+
return reservedUpdateResponse(state, result.update);
|
|
373
|
+
}
|
|
61
374
|
}
|
|
62
375
|
function deliverPollingUpdate(state, update) {
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
state.updates.
|
|
66
|
-
|
|
376
|
+
pollingUpdateOrder(state, update);
|
|
377
|
+
if (state.updates.length > 0) {
|
|
378
|
+
if (state.updates.length + state.reservedUpdateOrders.size >= state.maxPendingInboundEvents) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
queuePollingUpdate(state, update);
|
|
382
|
+
flushPendingPollingUpdate(state);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
const pending = state.pendingRequest;
|
|
386
|
+
if (pending) {
|
|
387
|
+
if (isPendingUpdateRequestLive(pending)) {
|
|
388
|
+
reservePollingUpdate(state, update);
|
|
389
|
+
settlePendingUpdate(state, pending, { kind: "update", update });
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
settlePendingUpdate(state, pending, { kind: "disconnect" });
|
|
393
|
+
}
|
|
394
|
+
if (state.updates.length + state.reservedUpdateOrders.size >= state.maxPendingInboundEvents) {
|
|
395
|
+
return false;
|
|
67
396
|
}
|
|
68
|
-
|
|
69
|
-
|
|
397
|
+
queuePollingUpdate(state, update);
|
|
398
|
+
return true;
|
|
70
399
|
}
|
|
71
|
-
async function deliverWebhookUpdate(webhook, update) {
|
|
400
|
+
async function deliverWebhookUpdate(state, webhook, update) {
|
|
401
|
+
const deadlineAt = Date.now() + state.webhookDeliveryTimeoutMs;
|
|
402
|
+
const url = new URL(webhook.url);
|
|
403
|
+
let target;
|
|
72
404
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
405
|
+
target = await validateWebhookUrlWithDeadline(url, state, deadlineAt);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
409
|
+
return zaloError(`Webhook delivery timed out after ${state.webhookDeliveryTimeoutMs}ms`, 502);
|
|
410
|
+
}
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
if (target instanceof Response) {
|
|
414
|
+
return target;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const remainingMs = deadlineAt - Date.now();
|
|
418
|
+
if (remainingMs <= 0) {
|
|
419
|
+
throw webhookTimeoutError(state.webhookDeliveryTimeoutMs);
|
|
420
|
+
}
|
|
421
|
+
const status = await postZaloWebhook({
|
|
422
|
+
activeRequests: state.activeWebhookRequests,
|
|
423
|
+
addresses: target.addresses,
|
|
424
|
+
body: JSON.stringify(update),
|
|
425
|
+
shouldCancel: () => state.closing,
|
|
426
|
+
timeoutMs: remainingMs,
|
|
427
|
+
url,
|
|
428
|
+
verificationValue: webhook.secretToken,
|
|
80
429
|
});
|
|
81
|
-
if (
|
|
82
|
-
return zaloError(`Webhook delivery failed with HTTP ${
|
|
430
|
+
if (status < 200 || status >= 300) {
|
|
431
|
+
return zaloError(`Webhook delivery failed with HTTP ${status}`, 502);
|
|
83
432
|
}
|
|
84
433
|
}
|
|
85
434
|
catch (error) {
|
|
435
|
+
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
436
|
+
return zaloError(`Webhook delivery timed out after ${state.webhookDeliveryTimeoutMs}ms`, 502);
|
|
437
|
+
}
|
|
86
438
|
return zaloError(`Webhook delivery failed: ${error instanceof Error ? error.message : String(error)}`, 502);
|
|
87
439
|
}
|
|
88
440
|
return undefined;
|
|
89
441
|
}
|
|
90
442
|
async function handleAdminInbound(request, state, url) {
|
|
91
443
|
if (!hasAdminToken(request, state.adminToken)) {
|
|
444
|
+
drainRequestBody(request);
|
|
92
445
|
return adminAuthError();
|
|
93
446
|
}
|
|
94
|
-
const
|
|
447
|
+
const parsedBody = await parseUnknownRequestBody(request);
|
|
448
|
+
if (!isJsonObject(parsedBody)) {
|
|
449
|
+
return zaloError("Bad Request: can't parse JSON object", 400);
|
|
450
|
+
}
|
|
451
|
+
const body = parsedBody;
|
|
95
452
|
const chatId = requireParam(body, "chatId");
|
|
96
453
|
const senderId = requireParam(body, "senderId");
|
|
97
454
|
const text = requireParam(body, "text");
|
|
98
455
|
if (chatId instanceof Response || senderId instanceof Response || text instanceof Response) {
|
|
99
456
|
return [chatId, senderId, text].find((value) => value instanceof Response);
|
|
100
457
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
date: Date.now(),
|
|
106
|
-
from: {
|
|
107
|
-
display_name: readTrimmedString(body.senderName) ?? senderId,
|
|
108
|
-
id: senderId,
|
|
109
|
-
is_bot: false,
|
|
110
|
-
},
|
|
111
|
-
message_id: messageId(state),
|
|
112
|
-
text,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
await appendEvent(state, {
|
|
116
|
-
at: new Date().toISOString(),
|
|
117
|
-
body,
|
|
118
|
-
method: request.method ?? "POST",
|
|
119
|
-
path: url.pathname,
|
|
120
|
-
query: queryRecord(url),
|
|
121
|
-
type: "admin",
|
|
458
|
+
let releaseAdmission;
|
|
459
|
+
const previousAdmission = state.inboundAdmission;
|
|
460
|
+
state.inboundAdmission = new Promise((resolve) => {
|
|
461
|
+
releaseAdmission = resolve;
|
|
122
462
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
463
|
+
await previousAdmission;
|
|
464
|
+
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
|
+
}
|
|
469
|
+
const update = {
|
|
470
|
+
event_name: "message.text.received",
|
|
471
|
+
message: {
|
|
472
|
+
chat: { chat_type: readChatType(body.chatType), id: chatId },
|
|
473
|
+
date: Date.now(),
|
|
474
|
+
from: {
|
|
475
|
+
display_name: readTrimmedString(body.senderName) ?? senderId,
|
|
476
|
+
id: senderId,
|
|
477
|
+
is_bot: false,
|
|
478
|
+
},
|
|
479
|
+
message_id: messageId(state),
|
|
480
|
+
text,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
await appendEvent(state, {
|
|
484
|
+
at: new Date().toISOString(),
|
|
485
|
+
body,
|
|
486
|
+
method: request.method ?? "POST",
|
|
487
|
+
path: url.pathname,
|
|
488
|
+
query: queryRecord(url),
|
|
489
|
+
type: "admin",
|
|
490
|
+
});
|
|
491
|
+
if (state.webhook?.url) {
|
|
492
|
+
const error = await deliverWebhookUpdate(state, state.webhook, update);
|
|
493
|
+
if (error) {
|
|
494
|
+
return error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (!deliverPollingUpdate(state, update)) {
|
|
498
|
+
return zaloError(`Pending inbound queue is full (${state.maxPendingInboundEvents} updates)`, 429);
|
|
127
499
|
}
|
|
500
|
+
return zaloOk(update);
|
|
128
501
|
}
|
|
129
|
-
|
|
130
|
-
|
|
502
|
+
finally {
|
|
503
|
+
releaseAdmission();
|
|
131
504
|
}
|
|
132
|
-
return zaloOk(update);
|
|
133
505
|
}
|
|
134
|
-
async function handleZaloMethod(request, state, url, method) {
|
|
135
|
-
const
|
|
136
|
-
|
|
506
|
+
async function handleZaloMethod(request, response, state, url, method) {
|
|
507
|
+
const parsedBody = await parseUnknownRequestBody(request);
|
|
508
|
+
if (!isJsonObject(parsedBody)) {
|
|
509
|
+
return zaloError("Bad Request: can't parse JSON object", 400);
|
|
510
|
+
}
|
|
511
|
+
const body = requestParams(url, parsedBody);
|
|
512
|
+
const event = {
|
|
137
513
|
at: new Date().toISOString(),
|
|
138
514
|
...(Object.keys(body).length > 0 ? { body: redactParams(body) } : {}),
|
|
139
515
|
method: request.method ?? "GET",
|
|
140
516
|
path: `/bot<redacted>/${method}`,
|
|
141
517
|
query: redactParams(queryRecord(url)),
|
|
142
518
|
type: "api",
|
|
143
|
-
}
|
|
519
|
+
};
|
|
520
|
+
if (method === "sendMessage" || method === "sendPhoto") {
|
|
521
|
+
const chatId = requireParam(body, "chat_id");
|
|
522
|
+
const content = requireParam(body, method === "sendMessage" ? "text" : "photo");
|
|
523
|
+
const error = firstError(chatId, content);
|
|
524
|
+
const sendResponse = error ?? zaloOk({ date: Date.now(), message_id: messageId(state) });
|
|
525
|
+
event.accepted = sendResponse.ok;
|
|
526
|
+
try {
|
|
527
|
+
await appendEvent(state, event);
|
|
528
|
+
}
|
|
529
|
+
catch (appendError) {
|
|
530
|
+
if (!event.accepted) {
|
|
531
|
+
throw appendError;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return sendResponse;
|
|
535
|
+
}
|
|
536
|
+
await appendEvent(state, event);
|
|
144
537
|
if (method === "getMe") {
|
|
145
538
|
return zaloOk({
|
|
146
539
|
account_name: state.botName,
|
|
@@ -155,16 +548,7 @@ async function handleZaloMethod(request, state, url, method) {
|
|
|
155
548
|
}
|
|
156
549
|
const parsedTimeout = Number(readTrimmedString(body.timeout) ?? "30");
|
|
157
550
|
const timeout = Number.isFinite(parsedTimeout) ? Math.max(0, Math.min(parsedTimeout, 50)) : 30;
|
|
158
|
-
return await waitForUpdate(state, timeout);
|
|
159
|
-
}
|
|
160
|
-
if (method === "sendMessage" || method === "sendPhoto") {
|
|
161
|
-
const chatId = requireParam(body, "chat_id");
|
|
162
|
-
const content = requireParam(body, method === "sendMessage" ? "text" : "photo");
|
|
163
|
-
const error = firstError(chatId, content);
|
|
164
|
-
if (error) {
|
|
165
|
-
return error;
|
|
166
|
-
}
|
|
167
|
-
return zaloOk({ date: Date.now(), message_id: messageId(state) });
|
|
551
|
+
return await waitForUpdate(request, response, state, timeout);
|
|
168
552
|
}
|
|
169
553
|
if (method === "sendChatAction") {
|
|
170
554
|
const chatId = requireParam(body, "chat_id");
|
|
@@ -188,18 +572,37 @@ async function handleZaloMethod(request, state, url, method) {
|
|
|
188
572
|
if (typeof webhookUrl !== "string" || typeof secretToken !== "string") {
|
|
189
573
|
return zaloError("Invalid webhook parameters", 400);
|
|
190
574
|
}
|
|
575
|
+
const secretError = validateZaloWebhookSecret(secretToken);
|
|
576
|
+
if (secretError) {
|
|
577
|
+
return secretError;
|
|
578
|
+
}
|
|
191
579
|
let parsedUrl;
|
|
192
580
|
try {
|
|
193
581
|
parsedUrl = new URL(webhookUrl);
|
|
194
582
|
}
|
|
195
583
|
catch {
|
|
196
|
-
return zaloError("url must be a valid
|
|
584
|
+
return zaloError("url must be a valid HTTPS URL", 400);
|
|
197
585
|
}
|
|
198
|
-
if (
|
|
199
|
-
return zaloError("
|
|
586
|
+
if (state.activeWebhookValidations.size >= MAX_ACTIVE_ZALO_WEBHOOK_VALIDATIONS) {
|
|
587
|
+
return zaloError("Too many webhook validations", 429);
|
|
588
|
+
}
|
|
589
|
+
let target;
|
|
590
|
+
try {
|
|
591
|
+
target = await validateWebhookUrlWithDeadline(parsedUrl, state, Date.now() + state.webhookDeliveryTimeoutMs);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return zaloError("url host could not be resolved", 400);
|
|
595
|
+
}
|
|
596
|
+
if (target instanceof Response) {
|
|
597
|
+
return target;
|
|
598
|
+
}
|
|
599
|
+
if (state.reservedUpdateOrders.size > 0) {
|
|
600
|
+
return zaloError("Polling deliveries are still in progress", 409);
|
|
200
601
|
}
|
|
201
|
-
state.updates.length = 0;
|
|
202
602
|
const webhook = { secretToken, updatedAt: Date.now(), url: parsedUrl.href };
|
|
603
|
+
if (state.pendingRequest) {
|
|
604
|
+
settlePendingUpdate(state, state.pendingRequest, { kind: "conflict" });
|
|
605
|
+
}
|
|
203
606
|
state.webhook = webhook;
|
|
204
607
|
return zaloOk({ updated_at: webhook.updatedAt, url: webhook.url });
|
|
205
608
|
}
|
|
@@ -216,18 +619,32 @@ async function handleZaloMethod(request, state, url, method) {
|
|
|
216
619
|
export async function startZaloServer(params = {}) {
|
|
217
620
|
const host = params.host ?? "127.0.0.1";
|
|
218
621
|
const state = {
|
|
622
|
+
activeWebhookRequests: new Set(),
|
|
623
|
+
activeWebhookValidations: new Set(),
|
|
219
624
|
adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
|
|
625
|
+
allowLoopbackHttpWebhook: isLoopbackHost(host),
|
|
220
626
|
botId: params.botId ?? "1459232241454765289",
|
|
221
627
|
botName: params.botName ?? "bot.crabline",
|
|
222
|
-
botToken: params.botToken ??
|
|
628
|
+
botToken: params.botToken ??
|
|
629
|
+
(isLoopbackHost(host) ? "crabline-zalo-bot-token" : randomBytes(32).toString("base64url")),
|
|
630
|
+
closing: false,
|
|
631
|
+
failedUpdateOrders: new Set(),
|
|
632
|
+
inboundAdmission: Promise.resolve(),
|
|
633
|
+
maxPendingInboundEvents: resolveMaxPendingInboundEvents(params.maxPendingInboundEvents),
|
|
223
634
|
nextMessage: 1,
|
|
224
|
-
|
|
635
|
+
nextUpdateOrder: 1,
|
|
636
|
+
onEvent: params.onEvent,
|
|
637
|
+
pendingRequest: undefined,
|
|
225
638
|
recorderPath: params.recorderPath ?? path.resolve(".crabline", "servers", "zalo.jsonl"),
|
|
639
|
+
reservedUpdateOrders: new Set(),
|
|
640
|
+
restrictWebhookTargets: true,
|
|
641
|
+
updateOrders: new WeakMap(),
|
|
226
642
|
updates: [],
|
|
227
643
|
webhook: undefined,
|
|
644
|
+
webhookDeliveryTimeoutMs: webhookDeliveryTimeoutMs(params.webhookDeliveryTimeoutMs),
|
|
228
645
|
};
|
|
229
646
|
const httpServer = await startHttpJsonServer({
|
|
230
|
-
handle: async (request) => {
|
|
647
|
+
handle: async (request, response) => {
|
|
231
648
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? host}`);
|
|
232
649
|
if (request.method === "POST" && url.pathname === "/crabline/zalo/inbound") {
|
|
233
650
|
return await handleAdminInbound(request, state, url);
|
|
@@ -237,9 +654,19 @@ export async function startZaloServer(params = {}) {
|
|
|
237
654
|
return zaloError("Not found", 404);
|
|
238
655
|
}
|
|
239
656
|
if (match[1] !== state.botToken) {
|
|
657
|
+
drainRequestBody(request);
|
|
240
658
|
return zaloError("Unauthorized", 401);
|
|
241
659
|
}
|
|
242
|
-
return await handleZaloMethod(request, state, url, match[2] ?? "");
|
|
660
|
+
return await handleZaloMethod(request, response, state, url, match[2] ?? "");
|
|
661
|
+
},
|
|
662
|
+
handleError: (error) => {
|
|
663
|
+
if (error instanceof InvalidJsonBodyError) {
|
|
664
|
+
return zaloError("Bad Request: can't parse JSON object", 400);
|
|
665
|
+
}
|
|
666
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
667
|
+
return zaloError("Request Entity Too Large", 413);
|
|
668
|
+
}
|
|
669
|
+
return undefined;
|
|
243
670
|
},
|
|
244
671
|
host,
|
|
245
672
|
port: params.port ?? 0,
|
|
@@ -264,9 +691,15 @@ export async function startZaloServer(params = {}) {
|
|
|
264
691
|
};
|
|
265
692
|
return {
|
|
266
693
|
async close() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
694
|
+
state.closing = true;
|
|
695
|
+
for (const request of state.activeWebhookRequests) {
|
|
696
|
+
request.destroy(new Error("Zalo server is shutting down."));
|
|
697
|
+
}
|
|
698
|
+
for (const validation of state.activeWebhookValidations) {
|
|
699
|
+
validation.abort(new Error("Zalo server is shutting down."));
|
|
700
|
+
}
|
|
701
|
+
if (state.pendingRequest) {
|
|
702
|
+
settlePendingUpdate(state, state.pendingRequest, { kind: "shutdown" });
|
|
270
703
|
}
|
|
271
704
|
await httpServer.close();
|
|
272
705
|
},
|