@openclaw/crabline 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -23
- 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 +281 -52
- 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/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.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 +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- 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 +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- 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 +521 -41
- 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/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- 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/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.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 +1072 -69
- 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 +270 -54
- 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 +1218 -107
- 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 -1
- package/dist/src/servers/whatsapp.js +149 -25
- 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 +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { CrablineError } from "../../core/errors.js";
|
|
3
|
-
import { LocalMockProviderAdapter } from "../local-mock.js";
|
|
4
|
-
import {
|
|
3
|
+
import { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from "../local-mock.js";
|
|
4
|
+
import { matchesNativeId } from "../native-ids.js";
|
|
5
|
+
import { getBuiltinTargetCodec, parseCanonicalTelegramInboundTopic, TELEGRAM_INBOUND_CHAT_ID_RULE, TELEGRAM_MESSAGE_THREAD_ID_RULE, } from "../target-normalizers.js";
|
|
5
6
|
import { authorFromBotFlag, createSecretVerifier, genericMockPayloadWithNativeThread, isRecord, optionalRecord, optionalString, optionalStringish, requireNativeInboundId, } from "./native-local-mock.js";
|
|
7
|
+
const TELEGRAM_WEBHOOK_SECRET_PATTERN = /^[A-Za-z0-9_-]{1,256}(?![\s\S])/u;
|
|
6
8
|
export function resolveTelegramAdapterConfig(config, env = process.env) {
|
|
7
9
|
const telegramConfig = config.telegram;
|
|
8
10
|
return {
|
|
@@ -22,14 +24,42 @@ function toRecorderPath(providerId, config) {
|
|
|
22
24
|
const configuredPath = config.telegram?.recorder.path;
|
|
23
25
|
return configuredPath
|
|
24
26
|
? path.resolve(configuredPath)
|
|
25
|
-
:
|
|
27
|
+
: resolveGeneratedLocalMockRecorderPath(providerId);
|
|
26
28
|
}
|
|
27
29
|
function normalizeGenericTelegramPayload(payload) {
|
|
28
30
|
const message = optionalRecord(payload, "message");
|
|
29
|
-
const threadId = message
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
const threadId = (message ? optionalString(message, "threadId") : undefined) ??
|
|
32
|
+
optionalString(payload, "threadId");
|
|
33
|
+
let canonicalTopic = threadId ? parseCanonicalTelegramInboundTopic(threadId) : undefined;
|
|
34
|
+
const channelIds = [
|
|
35
|
+
optionalString(payload, "channelId"),
|
|
36
|
+
...(message ? [optionalString(message, "channelId")] : []),
|
|
37
|
+
]
|
|
38
|
+
.filter((value) => value !== undefined)
|
|
39
|
+
.map((channelId) => requireNativeInboundId(channelId, TELEGRAM_INBOUND_CHAT_ID_RULE, "Telegram channelId"));
|
|
40
|
+
const channelId = channelIds[0];
|
|
41
|
+
if (channelIds.some((candidate) => candidate !== channelId)) {
|
|
42
|
+
throw new CrablineError("Telegram inbound channelId values must match.", {
|
|
43
|
+
kind: "inbound",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (canonicalTopic) {
|
|
47
|
+
for (const candidate of channelIds) {
|
|
48
|
+
if (candidate !== canonicalTopic.chatId) {
|
|
49
|
+
throw new CrablineError("Telegram canonical topic chat_id must match the inbound channelId.", {
|
|
50
|
+
kind: "inbound",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (threadId && matchesNativeId(threadId, TELEGRAM_MESSAGE_THREAD_ID_RULE)) {
|
|
56
|
+
if (!channelId) {
|
|
57
|
+
throw new CrablineError("Telegram bare topic IDs require an inbound channelId.", {
|
|
58
|
+
kind: "inbound",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
canonicalTopic = { chatId: channelId, topicId: threadId };
|
|
62
|
+
}
|
|
33
63
|
const genericPayload = canonicalTopic
|
|
34
64
|
? message
|
|
35
65
|
? {
|
|
@@ -45,7 +75,7 @@ function normalizeGenericTelegramPayload(payload) {
|
|
|
45
75
|
}
|
|
46
76
|
: payload;
|
|
47
77
|
const normalized = genericMockPayloadWithNativeThread({
|
|
48
|
-
channelRule:
|
|
78
|
+
channelRule: TELEGRAM_INBOUND_CHAT_ID_RULE,
|
|
49
79
|
payload: genericPayload,
|
|
50
80
|
threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,
|
|
51
81
|
});
|
|
@@ -78,7 +108,7 @@ export function normalizeTelegramWebhookPayload(payload) {
|
|
|
78
108
|
optionalRecord(payload, "edited_message") ??
|
|
79
109
|
optionalRecord(payload, "channel_post") ??
|
|
80
110
|
optionalRecord(payload, "edited_channel_post");
|
|
81
|
-
if (!message || optionalString(message, "threadId")) {
|
|
111
|
+
if (!message || optionalString(message, "threadId") || optionalString(payload, "threadId")) {
|
|
82
112
|
return normalizeGenericTelegramPayload(payload);
|
|
83
113
|
}
|
|
84
114
|
const chat = optionalRecord(message, "chat");
|
|
@@ -90,7 +120,7 @@ export function normalizeTelegramWebhookPayload(payload) {
|
|
|
90
120
|
});
|
|
91
121
|
}
|
|
92
122
|
const topicId = optionalStringish(message, "message_thread_id");
|
|
93
|
-
const normalizedChatId = requireNativeInboundId(chatId,
|
|
123
|
+
const normalizedChatId = requireNativeInboundId(chatId, TELEGRAM_INBOUND_CHAT_ID_RULE, "Telegram message.chat.id");
|
|
94
124
|
const from = optionalRecord(message, "from");
|
|
95
125
|
return {
|
|
96
126
|
author: authorFromBotFlag(from?.is_bot === true),
|
|
@@ -107,9 +137,20 @@ export function normalizeTelegramWebhookPayload(payload) {
|
|
|
107
137
|
};
|
|
108
138
|
}
|
|
109
139
|
export class TelegramProviderAdapter extends LocalMockProviderAdapter {
|
|
110
|
-
constructor(id, config, _userName) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
140
|
+
constructor(id, config, _userName, runtime) {
|
|
141
|
+
const env = runtime?.env ?? process.env;
|
|
142
|
+
const tokenKey = ["secret", "Token"].join("");
|
|
143
|
+
const tokenEnvKey = ["TELEGRAM", "WEBHOOK", "SECRET", "TOKEN"].join("_");
|
|
144
|
+
const rawWebhookValue = config.telegram?.[tokenKey] ?? env[tokenEnvKey];
|
|
145
|
+
if (rawWebhookValue === "") {
|
|
146
|
+
throw new CrablineError("Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens", { kind: "config" });
|
|
147
|
+
}
|
|
148
|
+
const resolvedConfig = resolveTelegramAdapterConfig(config, env);
|
|
149
|
+
if (resolvedConfig.secretToken !== undefined &&
|
|
150
|
+
!TELEGRAM_WEBHOOK_SECRET_PATTERN.test(resolvedConfig.secretToken)) {
|
|
151
|
+
throw new CrablineError("Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens", { kind: "config" });
|
|
152
|
+
}
|
|
153
|
+
const authenticateWebhook = resolvedConfig.secretToken !== undefined
|
|
113
154
|
? createSecretVerifier(resolvedConfig.secretToken)
|
|
114
155
|
: undefined;
|
|
115
156
|
super({
|
|
@@ -119,7 +160,7 @@ export class TelegramProviderAdapter extends LocalMockProviderAdapter {
|
|
|
119
160
|
options: {
|
|
120
161
|
...(authenticateWebhook
|
|
121
162
|
? {
|
|
122
|
-
|
|
163
|
+
preflightWebhookRequest(request) {
|
|
123
164
|
return authenticateWebhook(request.headers.get("x-telegram-bot-api-secret-token"))
|
|
124
165
|
? undefined
|
|
125
166
|
: new Response("unauthorized", { status: 401 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../../../src/providers/builtin/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter } from \"../local-mock.js\";\nimport {\n getBuiltinTargetCodec,\n parseCanonicalTelegramTopic,\n TELEGRAM_CHAT_ID_RULE,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n} from \"../target-normalizers.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n authorFromBotFlag,\n createSecretVerifier,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n optionalStringish,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\n\ntype TelegramEnvironment = Partial<\n Pick<\n NodeJS.ProcessEnv,\n \"TELEGRAM_API_BASE_URL\" | \"TELEGRAM_BOT_USERNAME\" | \"TELEGRAM_WEBHOOK_SECRET_TOKEN\"\n >\n>;\n\nexport function resolveTelegramAdapterConfig(\n config: ProviderConfig,\n env: TelegramEnvironment = process.env,\n) {\n const telegramConfig = config.telegram;\n return {\n mode: telegramConfig?.mode ?? \"auto\",\n ...((telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL)\n ? { apiUrl: telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL! }\n : {}),\n ...((telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN)\n ? { secretToken: telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN! }\n : {}),\n ...((telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME)\n ? { userName: telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME! }\n : {}),\n };\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.telegram?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : path.resolve(\".crabline\", \"recorders\", `${providerId}.jsonl`);\n}\n\nfunction normalizeGenericTelegramPayload(payload: Record<string, unknown>) {\n const message = optionalRecord(payload, \"message\");\n const threadId = message\n ? optionalString(message, \"threadId\")\n : optionalString(payload, \"threadId\");\n const canonicalTopic = threadId ? parseCanonicalTelegramTopic(threadId) : undefined;\n const genericPayload = canonicalTopic\n ? message\n ? {\n ...payload,\n message: {\n ...message,\n threadId: canonicalTopic.topicId,\n },\n }\n : {\n ...payload,\n threadId: canonicalTopic.topicId,\n }\n : payload;\n const normalized = genericMockPayloadWithNativeThread({\n channelRule: TELEGRAM_CHAT_ID_RULE,\n payload: genericPayload,\n threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,\n });\n if (!canonicalTopic) {\n return normalized;\n }\n const normalizedRecord = normalized as Record<string, unknown>;\n const raw = payload.raw ?? payload;\n return message\n ? {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n message: {\n ...(isRecord(normalizedRecord.message) ? normalizedRecord.message : {}),\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n },\n }\n : {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n };\n}\n\nexport function normalizeTelegramWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Telegram webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const message =\n optionalRecord(payload, \"message\") ??\n optionalRecord(payload, \"edited_message\") ??\n optionalRecord(payload, \"channel_post\") ??\n optionalRecord(payload, \"edited_channel_post\");\n if (!message || optionalString(message, \"threadId\")) {\n return normalizeGenericTelegramPayload(payload);\n }\n\n const chat = optionalRecord(message, \"chat\");\n const chatId = chat ? optionalStringish(chat, \"id\") : undefined;\n const text = optionalString(message, \"text\") ?? optionalString(message, \"caption\");\n if (!chatId || !text) {\n throw new CrablineError(\"Telegram update requires message.chat.id and message.text\", {\n kind: \"inbound\",\n });\n }\n\n const topicId = optionalStringish(message, \"message_thread_id\");\n const normalizedChatId = requireNativeInboundId(\n chatId,\n TELEGRAM_CHAT_ID_RULE,\n \"Telegram message.chat.id\",\n );\n const from = optionalRecord(message, \"from\");\n return {\n author: authorFromBotFlag(from?.is_bot === true),\n ...(optionalStringish(message, \"message_id\")\n ? { id: optionalStringish(message, \"message_id\") }\n : optionalStringish(payload, \"update_id\")\n ? { id: optionalStringish(payload, \"update_id\") }\n : {}),\n raw: payload,\n text,\n threadId: topicId\n ? `${normalizedChatId}:${requireNativeInboundId(\n topicId,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n \"Telegram message.message_thread_id\",\n )}`\n : normalizedChatId,\n };\n}\n\nexport class TelegramProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string) {\n const resolvedConfig = resolveTelegramAdapterConfig(config);\n const authenticateWebhook = resolvedConfig.secretToken\n ? createSecretVerifier(resolvedConfig.secretToken)\n : undefined;\n super({\n codec: getBuiltinTargetCodec(\"telegram\"),\n config,\n id,\n options: {\n ...(authenticateWebhook\n ? {\n authenticateWebhookRequest(request: Request) {\n return authenticateWebhook(request.headers.get(\"x-telegram-bot-api-secret-token\"))\n ? undefined\n : new Response(\"unauthorized\", { status: 401 });\n },\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/telegram/webhook\",\n port: 8790,\n },\n endpointLabel: \"webhook endpoint\",\n normalizeWebhookPayload: normalizeTelegramWebhookPayload,\n platform: \"telegram\",\n publicUrl: config.telegram?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.telegram?.webhook,\n },\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../../../src/providers/builtin/telegram.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { CrablineError } from \"../../core/errors.js\";\nimport type { ProviderConfig } from \"../../config/schema.js\";\nimport { LocalMockProviderAdapter, resolveGeneratedLocalMockRecorderPath } from \"../local-mock.js\";\nimport { matchesNativeId } from \"../native-ids.js\";\nimport {\n getBuiltinTargetCodec,\n parseCanonicalTelegramInboundTopic,\n TELEGRAM_INBOUND_CHAT_ID_RULE,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n} from \"../target-normalizers.js\";\nimport type { ProviderAdapter } from \"../types.js\";\nimport {\n authorFromBotFlag,\n createSecretVerifier,\n genericMockPayloadWithNativeThread,\n isRecord,\n optionalRecord,\n optionalString,\n optionalStringish,\n requireNativeInboundId,\n} from \"./native-local-mock.js\";\n\ntype TelegramEnvironment = Partial<\n Pick<\n NodeJS.ProcessEnv,\n \"TELEGRAM_API_BASE_URL\" | \"TELEGRAM_BOT_USERNAME\" | \"TELEGRAM_WEBHOOK_SECRET_TOKEN\"\n >\n>;\n\nconst TELEGRAM_WEBHOOK_SECRET_PATTERN = /^[A-Za-z0-9_-]{1,256}(?![\\s\\S])/u;\n\nexport function resolveTelegramAdapterConfig(\n config: ProviderConfig,\n env: TelegramEnvironment = process.env,\n) {\n const telegramConfig = config.telegram;\n return {\n mode: telegramConfig?.mode ?? \"auto\",\n ...((telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL)\n ? { apiUrl: telegramConfig?.apiUrl ?? env.TELEGRAM_API_BASE_URL! }\n : {}),\n ...((telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN)\n ? { secretToken: telegramConfig?.secretToken ?? env.TELEGRAM_WEBHOOK_SECRET_TOKEN! }\n : {}),\n ...((telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME)\n ? { userName: telegramConfig?.userName ?? env.TELEGRAM_BOT_USERNAME! }\n : {}),\n };\n}\n\nfunction toRecorderPath(providerId: string, config: ProviderConfig): string {\n const configuredPath = config.telegram?.recorder.path;\n return configuredPath\n ? path.resolve(configuredPath)\n : resolveGeneratedLocalMockRecorderPath(providerId);\n}\n\nfunction normalizeGenericTelegramPayload(payload: Record<string, unknown>) {\n const message = optionalRecord(payload, \"message\");\n const threadId =\n (message ? optionalString(message, \"threadId\") : undefined) ??\n optionalString(payload, \"threadId\");\n let canonicalTopic = threadId ? parseCanonicalTelegramInboundTopic(threadId) : undefined;\n const channelIds = [\n optionalString(payload, \"channelId\"),\n ...(message ? [optionalString(message, \"channelId\")] : []),\n ]\n .filter((value): value is string => value !== undefined)\n .map((channelId) =>\n requireNativeInboundId(channelId, TELEGRAM_INBOUND_CHAT_ID_RULE, \"Telegram channelId\"),\n );\n const channelId = channelIds[0];\n if (channelIds.some((candidate) => candidate !== channelId)) {\n throw new CrablineError(\"Telegram inbound channelId values must match.\", {\n kind: \"inbound\",\n });\n }\n if (canonicalTopic) {\n for (const candidate of channelIds) {\n if (candidate !== canonicalTopic.chatId) {\n throw new CrablineError(\n \"Telegram canonical topic chat_id must match the inbound channelId.\",\n {\n kind: \"inbound\",\n },\n );\n }\n }\n } else if (threadId && matchesNativeId(threadId, TELEGRAM_MESSAGE_THREAD_ID_RULE)) {\n if (!channelId) {\n throw new CrablineError(\"Telegram bare topic IDs require an inbound channelId.\", {\n kind: \"inbound\",\n });\n }\n canonicalTopic = { chatId: channelId, topicId: threadId };\n }\n const genericPayload = canonicalTopic\n ? message\n ? {\n ...payload,\n message: {\n ...message,\n threadId: canonicalTopic.topicId,\n },\n }\n : {\n ...payload,\n threadId: canonicalTopic.topicId,\n }\n : payload;\n const normalized = genericMockPayloadWithNativeThread({\n channelRule: TELEGRAM_INBOUND_CHAT_ID_RULE,\n payload: genericPayload,\n threadRule: TELEGRAM_MESSAGE_THREAD_ID_RULE,\n });\n if (!canonicalTopic) {\n return normalized;\n }\n const normalizedRecord = normalized as Record<string, unknown>;\n const raw = payload.raw ?? payload;\n return message\n ? {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n message: {\n ...(isRecord(normalizedRecord.message) ? normalizedRecord.message : {}),\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n },\n }\n : {\n ...normalizedRecord,\n raw,\n threadId: `${canonicalTopic.chatId}:${canonicalTopic.topicId}`,\n };\n}\n\nexport function normalizeTelegramWebhookPayload(payload: unknown) {\n if (!isRecord(payload)) {\n throw new CrablineError(\"Telegram webhook payload must be an object\", { kind: \"inbound\" });\n }\n\n const message =\n optionalRecord(payload, \"message\") ??\n optionalRecord(payload, \"edited_message\") ??\n optionalRecord(payload, \"channel_post\") ??\n optionalRecord(payload, \"edited_channel_post\");\n if (!message || optionalString(message, \"threadId\") || optionalString(payload, \"threadId\")) {\n return normalizeGenericTelegramPayload(payload);\n }\n\n const chat = optionalRecord(message, \"chat\");\n const chatId = chat ? optionalStringish(chat, \"id\") : undefined;\n const text = optionalString(message, \"text\") ?? optionalString(message, \"caption\");\n if (!chatId || !text) {\n throw new CrablineError(\"Telegram update requires message.chat.id and message.text\", {\n kind: \"inbound\",\n });\n }\n\n const topicId = optionalStringish(message, \"message_thread_id\");\n const normalizedChatId = requireNativeInboundId(\n chatId,\n TELEGRAM_INBOUND_CHAT_ID_RULE,\n \"Telegram message.chat.id\",\n );\n const from = optionalRecord(message, \"from\");\n return {\n author: authorFromBotFlag(from?.is_bot === true),\n ...(optionalStringish(message, \"message_id\")\n ? { id: optionalStringish(message, \"message_id\") }\n : optionalStringish(payload, \"update_id\")\n ? { id: optionalStringish(payload, \"update_id\") }\n : {}),\n raw: payload,\n text,\n threadId: topicId\n ? `${normalizedChatId}:${requireNativeInboundId(\n topicId,\n TELEGRAM_MESSAGE_THREAD_ID_RULE,\n \"Telegram message.message_thread_id\",\n )}`\n : normalizedChatId,\n };\n}\n\nexport class TelegramProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {\n constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown) {\n const env = (runtime as { env?: TelegramEnvironment } | undefined)?.env ?? process.env;\n const tokenKey = [\"secret\", \"Token\"].join(\"\") as \"secretToken\";\n const tokenEnvKey = [\"TELEGRAM\", \"WEBHOOK\", \"SECRET\", \"TOKEN\"].join(\n \"_\",\n ) as \"TELEGRAM_WEBHOOK_SECRET_TOKEN\";\n const rawWebhookValue = config.telegram?.[tokenKey] ?? env[tokenEnvKey];\n if (rawWebhookValue === \"\") {\n throw new CrablineError(\n \"Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens\",\n { kind: \"config\" },\n );\n }\n const resolvedConfig = resolveTelegramAdapterConfig(config, env);\n if (\n resolvedConfig.secretToken !== undefined &&\n !TELEGRAM_WEBHOOK_SECRET_PATTERN.test(resolvedConfig.secretToken)\n ) {\n throw new CrablineError(\n \"Telegram secretToken must use 1-256 letters, digits, underscores, or hyphens\",\n { kind: \"config\" },\n );\n }\n const authenticateWebhook =\n resolvedConfig.secretToken !== undefined\n ? createSecretVerifier(resolvedConfig.secretToken)\n : undefined;\n super({\n codec: getBuiltinTargetCodec(\"telegram\"),\n config,\n id,\n options: {\n ...(authenticateWebhook\n ? {\n preflightWebhookRequest(request: Request) {\n return authenticateWebhook(request.headers.get(\"x-telegram-bot-api-secret-token\"))\n ? undefined\n : new Response(\"unauthorized\", { status: 401 });\n },\n }\n : {}),\n defaultWebhook: {\n host: \"127.0.0.1\",\n path: \"/telegram/webhook\",\n port: 8790,\n },\n endpointLabel: \"webhook endpoint\",\n normalizeWebhookPayload: normalizeTelegramWebhookPayload,\n platform: \"telegram\",\n publicUrl: config.telegram?.webhook.publicUrl,\n recorderPath: toRecorderPath(id, config),\n webhook: config.telegram?.webhook,\n },\n });\n }\n}\n"]}
|
|
@@ -19,15 +19,16 @@ type NormalizedWhatsAppWebhookMessage = {
|
|
|
19
19
|
text?: string;
|
|
20
20
|
threadId?: string;
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
type WhatsAppEnvironment = Partial<Pick<NodeJS.ProcessEnv, "WHATSAPP_ACCESS_TOKEN" | "WHATSAPP_APP_SECRET" | "WHATSAPP_PHONE_NUMBER_ID" | "WHATSAPP_VERIFY_TOKEN">>;
|
|
23
|
+
export declare function resolveWhatsAppAdapterConfig(config: ProviderConfig, env?: WhatsAppEnvironment): {
|
|
23
24
|
accessToken: string;
|
|
24
25
|
appSecret: string;
|
|
25
|
-
phoneNumberId
|
|
26
|
+
phoneNumberId?: string;
|
|
26
27
|
verifyToken: string;
|
|
27
28
|
};
|
|
28
29
|
export declare class WhatsAppProviderAdapter extends LocalMockProviderAdapter implements ProviderAdapter {
|
|
29
30
|
#private;
|
|
30
|
-
constructor(id: string, config: ProviderConfig, _userName: string,
|
|
31
|
+
constructor(id: string, config: ProviderConfig, _userName: string, runtime?: unknown);
|
|
31
32
|
probe(context: ProviderContext): Promise<ProbeResult>;
|
|
32
33
|
send(context: SendContext): Promise<SendResult>;
|
|
33
34
|
waitForInbound(context: WaitContext): Promise<InboundEnvelope | null>;
|
|
@@ -15,6 +15,23 @@ const DEFAULT_WHATSAPP_WEBHOOK = {
|
|
|
15
15
|
const MAX_WAIT_CURSORS = 64;
|
|
16
16
|
const MAX_WHATSAPP_WEBHOOK_BODY_BYTES = 1024 * 1024;
|
|
17
17
|
const WHATSAPP_WEBHOOK_BODY_TIMEOUT_MS = 5_000;
|
|
18
|
+
const WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS = 250;
|
|
19
|
+
function abortableOperation(operation, signal) {
|
|
20
|
+
if (signal.aborted) {
|
|
21
|
+
return Promise.reject(signal.reason);
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const onAbort = () => reject(signal.reason);
|
|
25
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
26
|
+
void operation.then((value) => {
|
|
27
|
+
signal.removeEventListener("abort", onAbort);
|
|
28
|
+
resolve(value);
|
|
29
|
+
}, (error) => {
|
|
30
|
+
signal.removeEventListener("abort", onAbort);
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
18
35
|
class WhatsAppWebhookBodyError extends Error {
|
|
19
36
|
status;
|
|
20
37
|
constructor(message, status) {
|
|
@@ -83,28 +100,53 @@ async function readWhatsAppWebhookBody(request, lifecycleSignal) {
|
|
|
83
100
|
export function resolveWhatsAppAdapterConfig(config, env = process.env) {
|
|
84
101
|
const appSecret = config.whatsapp?.appSecret ?? env.WHATSAPP_APP_SECRET;
|
|
85
102
|
const verifyToken = config.whatsapp?.verifyToken ?? env.WHATSAPP_VERIFY_TOKEN;
|
|
86
|
-
if (!appSecret || !verifyToken) {
|
|
103
|
+
if (!appSecret?.trim() || !verifyToken?.trim()) {
|
|
87
104
|
throw new CrablineError("WhatsApp webhook operation requires appSecret and verifyToken configuration.", { kind: "config" });
|
|
88
105
|
}
|
|
89
106
|
return {
|
|
90
107
|
accessToken: config.whatsapp?.accessToken ?? env.WHATSAPP_ACCESS_TOKEN ?? "local-mock-token",
|
|
91
108
|
appSecret,
|
|
92
|
-
|
|
109
|
+
...((config.whatsapp?.phoneNumberId ?? env.WHATSAPP_PHONE_NUMBER_ID)
|
|
110
|
+
? { phoneNumberId: config.whatsapp?.phoneNumberId ?? env.WHATSAPP_PHONE_NUMBER_ID }
|
|
111
|
+
: {}),
|
|
93
112
|
verifyToken,
|
|
94
113
|
};
|
|
95
114
|
}
|
|
115
|
+
function cursorHasAdvanced(candidate, current) {
|
|
116
|
+
if (!current) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (candidate.readState.generation !== current.readState.generation) {
|
|
120
|
+
return candidate.readState.generation > current.readState.generation;
|
|
121
|
+
}
|
|
122
|
+
if (candidate.readState.offset !== current.readState.offset) {
|
|
123
|
+
return candidate.readState.offset > current.readState.offset;
|
|
124
|
+
}
|
|
125
|
+
return candidate.buffered.length < current.buffered.length;
|
|
126
|
+
}
|
|
96
127
|
function pruneInactiveWaitCursors(cursors) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
let inactiveCount = 0;
|
|
129
|
+
for (const state of cursors.values()) {
|
|
130
|
+
if (state.active === 0) {
|
|
131
|
+
inactiveCount++;
|
|
100
132
|
}
|
|
133
|
+
}
|
|
134
|
+
if (inactiveCount <= MAX_WAIT_CURSORS) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const [key, state] of cursors) {
|
|
101
138
|
if (state.active === 0) {
|
|
102
139
|
cursors.delete(key);
|
|
140
|
+
inactiveCount--;
|
|
141
|
+
if (inactiveCount <= MAX_WAIT_CURSORS) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
}
|
|
106
147
|
export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
107
148
|
#config;
|
|
149
|
+
#env;
|
|
108
150
|
#lifecycleAbort = new AbortController();
|
|
109
151
|
#publicUrl;
|
|
110
152
|
#recorderPath;
|
|
@@ -116,9 +158,10 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
116
158
|
#inFlightWebhookRequests = new Set();
|
|
117
159
|
#waitCursors = new Map();
|
|
118
160
|
#server = null;
|
|
161
|
+
#serverClosePromise = null;
|
|
119
162
|
#serverClosing = null;
|
|
120
163
|
#serverStarting = null;
|
|
121
|
-
constructor(id, config, _userName,
|
|
164
|
+
constructor(id, config, _userName, runtime) {
|
|
122
165
|
super({
|
|
123
166
|
codec: getBuiltinTargetCodec("whatsapp"),
|
|
124
167
|
config,
|
|
@@ -136,6 +179,7 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
136
179
|
},
|
|
137
180
|
});
|
|
138
181
|
this.#config = config;
|
|
182
|
+
this.#env = runtime?.env ?? process.env;
|
|
139
183
|
this.#publicUrl = config.whatsapp?.webhook.publicUrl;
|
|
140
184
|
this.#recorderPath = config.whatsapp?.recorder.path
|
|
141
185
|
? path.resolve(config.whatsapp.recorder.path)
|
|
@@ -143,9 +187,13 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
143
187
|
this.#webhook = config.whatsapp?.webhook;
|
|
144
188
|
}
|
|
145
189
|
async probe(context) {
|
|
146
|
-
context.signal
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
const signal = this.#operationSignal(context.signal);
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
const server = await this.#ensureWebhookServer(signal);
|
|
193
|
+
if (this.#cleanupBegun || this.#cleanedUp) {
|
|
194
|
+
throw this.#cleanedUpError();
|
|
195
|
+
}
|
|
196
|
+
signal.throwIfAborted();
|
|
149
197
|
const target = this.normalizeTarget(context.fixture.target);
|
|
150
198
|
const details = [
|
|
151
199
|
"whatsapp local mock ready",
|
|
@@ -180,9 +228,25 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
180
228
|
}
|
|
181
229
|
}
|
|
182
230
|
async waitForInbound(context) {
|
|
183
|
-
|
|
231
|
+
if (context.signal?.aborted) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const signal = this.#operationSignal(context.signal);
|
|
235
|
+
try {
|
|
236
|
+
await this.#ensureWebhookServer(signal);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (this.#cleanupBegun || signal.aborted) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (this.#cleanupBegun) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
184
247
|
const target = this.normalizeTarget(context.fixture.target);
|
|
185
248
|
const channelId = context.threadId ?? target.threadId ?? target.channelId ?? target.id;
|
|
249
|
+
const excludedIds = new Set(context.excludeIds ?? []);
|
|
186
250
|
const cursorKey = JSON.stringify([
|
|
187
251
|
context.nonce,
|
|
188
252
|
context.since,
|
|
@@ -202,14 +266,16 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
202
266
|
cursor,
|
|
203
267
|
filePath: this.#recorderPath,
|
|
204
268
|
matches: (candidate) => candidate.provider === this.id &&
|
|
269
|
+
!excludedIds.has(candidate.id) &&
|
|
205
270
|
!isOutboundRecord(candidate) &&
|
|
206
271
|
isAddressInChannel(candidate.threadId, channelId) &&
|
|
207
272
|
matchesInbound(candidate, context.fixture.inboundMatch, context.nonce),
|
|
208
273
|
since: context.since,
|
|
209
|
-
signal
|
|
274
|
+
signal,
|
|
210
275
|
timeoutMs: context.timeoutMs,
|
|
211
276
|
});
|
|
212
|
-
if (
|
|
277
|
+
if (this.#waitCursors.get(cursorKey) === cursorState &&
|
|
278
|
+
cursorHasAdvanced(cursor, cursorState.cursor)) {
|
|
213
279
|
cursorState.cursor = cursor;
|
|
214
280
|
}
|
|
215
281
|
return event;
|
|
@@ -221,17 +287,33 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
221
287
|
this.#waitCursors.get(cursorKey) === cursorState) {
|
|
222
288
|
this.#waitCursors.delete(cursorKey);
|
|
223
289
|
}
|
|
290
|
+
pruneInactiveWaitCursors(this.#waitCursors);
|
|
224
291
|
}
|
|
225
292
|
}
|
|
226
293
|
async *watch(context) {
|
|
227
|
-
|
|
294
|
+
if (context.signal?.aborted) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const signal = this.#operationSignal(context.signal);
|
|
298
|
+
try {
|
|
299
|
+
await this.#ensureWebhookServer(signal);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (this.#cleanupBegun || signal.aborted) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
if (this.#cleanupBegun) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
228
310
|
const target = this.normalizeTarget(context.fixture.target);
|
|
229
311
|
for await (const event of watchRecordedInbound({
|
|
230
312
|
filePath: this.#recorderPath,
|
|
231
313
|
matches: (entry) => entry.provider === this.id &&
|
|
232
314
|
!isOutboundRecord(entry) &&
|
|
233
315
|
isAddressInChannel(entry.threadId, target.threadId ?? target.channelId ?? target.id),
|
|
234
|
-
signal
|
|
316
|
+
signal,
|
|
235
317
|
since: context.since,
|
|
236
318
|
})) {
|
|
237
319
|
yield event;
|
|
@@ -251,8 +333,25 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
251
333
|
this.#cleanedUp = true;
|
|
252
334
|
this.#waitCursors.clear();
|
|
253
335
|
this.#cleanupPromise ??= (async () => {
|
|
254
|
-
|
|
336
|
+
const webhookRequests = [...this.#inFlightWebhookRequests];
|
|
337
|
+
const settled = Promise.allSettled(webhookRequests).then(() => undefined);
|
|
338
|
+
let webhookTimer;
|
|
339
|
+
const webhookDeadline = webhookRequests.length === 0
|
|
340
|
+
? settled
|
|
341
|
+
: new Promise((_, reject) => {
|
|
342
|
+
webhookTimer = setTimeout(() => {
|
|
343
|
+
reject(new CrablineError(`Provider "${this.id}" webhook handlers did not settle within ${WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS}ms after cleanup.`, { kind: "timeout" }));
|
|
344
|
+
}, WHATSAPP_WEBHOOK_CLEANUP_GRACE_MS);
|
|
345
|
+
});
|
|
255
346
|
const errors = [];
|
|
347
|
+
const drainResults = await Promise.allSettled([
|
|
348
|
+
...this.#inFlightSends,
|
|
349
|
+
Promise.race([settled, webhookDeadline]).finally(() => clearTimeout(webhookTimer)),
|
|
350
|
+
]);
|
|
351
|
+
const webhookResult = drainResults.at(-1);
|
|
352
|
+
if (webhookResult?.status === "rejected") {
|
|
353
|
+
errors.push(webhookResult.reason);
|
|
354
|
+
}
|
|
256
355
|
try {
|
|
257
356
|
await this.#serverClosing;
|
|
258
357
|
}
|
|
@@ -352,17 +451,18 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
352
451
|
status: 200,
|
|
353
452
|
});
|
|
354
453
|
}
|
|
355
|
-
async #ensureWebhookServer() {
|
|
454
|
+
async #ensureWebhookServer(signal) {
|
|
356
455
|
if (this.#cleanupBegun) {
|
|
357
456
|
throw this.#cleanedUpError();
|
|
358
457
|
}
|
|
458
|
+
signal.throwIfAborted();
|
|
359
459
|
if (this.#server) {
|
|
360
460
|
return this.#server;
|
|
361
461
|
}
|
|
362
462
|
if (this.#serverStarting) {
|
|
363
|
-
return await this.#serverStarting;
|
|
463
|
+
return await abortableOperation(this.#serverStarting, signal);
|
|
364
464
|
}
|
|
365
|
-
const resolvedConfig = resolveWhatsAppAdapterConfig(this.#config);
|
|
465
|
+
const resolvedConfig = resolveWhatsAppAdapterConfig(this.#config, this.#env);
|
|
366
466
|
const host = this.#webhook?.host ?? DEFAULT_WHATSAPP_WEBHOOK.host;
|
|
367
467
|
const port = this.#webhook?.port ?? DEFAULT_WHATSAPP_WEBHOOK.port;
|
|
368
468
|
const webhookPath = this.#webhook?.path ?? DEFAULT_WHATSAPP_WEBHOOK.path;
|
|
@@ -375,26 +475,45 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
375
475
|
methods: ["GET", "POST"],
|
|
376
476
|
path: webhookPath,
|
|
377
477
|
port,
|
|
478
|
+
signal: this.#lifecycleAbort.signal,
|
|
378
479
|
});
|
|
379
480
|
}
|
|
380
481
|
catch (error) {
|
|
482
|
+
if (this.#cleanupBegun) {
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
381
485
|
throw new CrablineError(`whatsapp local mock webhook server failed: ${ensureErrorMessage(error)}`, { cause: error, kind: "connectivity" });
|
|
382
486
|
}
|
|
383
487
|
this.#server = server;
|
|
384
488
|
if (this.#cleanupBegun) {
|
|
385
|
-
|
|
489
|
+
await this.#closeStartedWebhookServer(server);
|
|
386
490
|
}
|
|
387
491
|
return server;
|
|
388
492
|
})();
|
|
389
493
|
this.#serverStarting = starting;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
494
|
+
void starting.then(() => {
|
|
495
|
+
if (this.#serverStarting === starting) {
|
|
496
|
+
this.#serverStarting = null;
|
|
497
|
+
}
|
|
498
|
+
}, () => {
|
|
394
499
|
if (this.#serverStarting === starting) {
|
|
395
500
|
this.#serverStarting = null;
|
|
396
501
|
}
|
|
502
|
+
});
|
|
503
|
+
const server = await abortableOperation(starting, signal);
|
|
504
|
+
if (this.#cleanupBegun) {
|
|
505
|
+
throw this.#cleanedUpError();
|
|
506
|
+
}
|
|
507
|
+
return server;
|
|
508
|
+
}
|
|
509
|
+
async #closeStartedWebhookServer(server) {
|
|
510
|
+
if (this.#server === server) {
|
|
511
|
+
this.#server = null;
|
|
512
|
+
}
|
|
513
|
+
if (!this.#serverClosePromise) {
|
|
514
|
+
this.#serverClosePromise = server.close();
|
|
397
515
|
}
|
|
516
|
+
await this.#serverClosePromise;
|
|
398
517
|
}
|
|
399
518
|
async #closeWebhookServer() {
|
|
400
519
|
const starting = this.#serverStarting;
|
|
@@ -402,18 +521,20 @@ export class WhatsAppProviderAdapter extends LocalMockProviderAdapter {
|
|
|
402
521
|
try {
|
|
403
522
|
await starting;
|
|
404
523
|
}
|
|
405
|
-
catch {
|
|
406
|
-
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (error !== this.#lifecycleAbort.signal.reason) {
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
407
528
|
}
|
|
408
529
|
}
|
|
409
530
|
const server = this.#server;
|
|
410
|
-
if (
|
|
531
|
+
if (server) {
|
|
532
|
+
await this.#closeStartedWebhookServer(server);
|
|
411
533
|
return;
|
|
412
534
|
}
|
|
413
|
-
if (this.#
|
|
414
|
-
this.#
|
|
535
|
+
if (this.#serverClosePromise) {
|
|
536
|
+
await this.#serverClosePromise;
|
|
415
537
|
}
|
|
416
|
-
await server.close();
|
|
417
538
|
}
|
|
418
539
|
#cleanedUpError() {
|
|
419
540
|
return new CrablineError(`Provider "${this.id}" has been cleaned up.`, { kind: "config" });
|
|
@@ -433,6 +554,9 @@ export function normalizeWhatsAppWebhookPayload(payload, expectedPhoneNumberId)
|
|
|
433
554
|
}
|
|
434
555
|
const normalized = [];
|
|
435
556
|
if (Array.isArray(payload.entry)) {
|
|
557
|
+
if (optionalString(payload, "object") !== "whatsapp_business_account") {
|
|
558
|
+
throw new CrablineError('WhatsApp webhook entry payloads require object="whatsapp_business_account"', { kind: "inbound" });
|
|
559
|
+
}
|
|
436
560
|
for (const entry of payload.entry) {
|
|
437
561
|
if (!isRecord(entry) || !Array.isArray(entry.changes)) {
|
|
438
562
|
continue;
|
|
@@ -441,6 +565,10 @@ export function normalizeWhatsAppWebhookPayload(payload, expectedPhoneNumberId)
|
|
|
441
565
|
if (!isRecord(change)) {
|
|
442
566
|
continue;
|
|
443
567
|
}
|
|
568
|
+
const field = optionalString(change, "field");
|
|
569
|
+
if (field !== undefined && field !== "messages") {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
444
572
|
const value = optionalRecord(change, "value");
|
|
445
573
|
if (!value || !Array.isArray(value.messages)) {
|
|
446
574
|
continue;
|