@nubemclaw/channel-telegram 1.2.2

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.
@@ -0,0 +1,37 @@
1
+ import type { ChannelLogger } from "@nubemclaw/channel-sdk";
2
+ export interface TelegramTransportOptions {
3
+ /**
4
+ * Explicit proxy URL (`http://...` / `https://...`). Defaults to
5
+ * `NUBEMCLAW_PROXY_URL` env var. When set, every dispatcher routes
6
+ * through this proxy via `undici.ProxyAgent`.
7
+ */
8
+ readonly proxyUrl?: string;
9
+ /** Custom logger; defaults to a silent one (transport doesn't own loggers). */
10
+ readonly logger?: ChannelLogger;
11
+ /** Test seam: override the env var for proxy resolution. */
12
+ readonly envVars?: NodeJS.ProcessEnv;
13
+ }
14
+ export interface TelegramTransport {
15
+ /**
16
+ * `fetch`-shaped callable usable directly as `new Bot(token,
17
+ * { client: { fetcher } }).` Routes through the primary dispatcher
18
+ * with automatic IPv4 fallback on persistent network failures.
19
+ */
20
+ fetch: typeof globalThis.fetch;
21
+ /**
22
+ * Destroys every dispatcher this transport owns. Safe to call
23
+ * multiple times; subsequent calls resolve immediately. The runner
24
+ * MUST call this when stopping the channel — otherwise undici keeps
25
+ * keep-alive sockets open indefinitely.
26
+ */
27
+ close(): Promise<void>;
28
+ }
29
+ export declare const createTelegramTransport: (opts?: TelegramTransportOptions) => TelegramTransport;
30
+ /**
31
+ * Sentinel constant identifying the Telegram API host. Exported for
32
+ * tests that want to assert the transport is being aimed at the right
33
+ * origin (the wrapped fetch does not enforce this — `globalThis.fetch`
34
+ * resolves the URL the caller supplies).
35
+ */
36
+ export declare const TELEGRAM_HOST = "api.telegram.org";
37
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AA+I5D,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACtC;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B;;;;;OAKG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiBD,eAAO,MAAM,uBAAuB,GAAI,OAAM,wBAA6B,KAAG,iBA2J7E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,qBAAwB,CAAC"}
@@ -0,0 +1,284 @@
1
+ import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
2
+ /**
3
+ * Telegram operational transport (Phase 13, task #144 v2).
4
+ *
5
+ * Port of the operational hardening from OpenClaw
6
+ * `extensions/telegram/src/fetch.ts:862`. Telegram long-polling keeps
7
+ * connections to `api.telegram.org` hot for hours; the default global
8
+ * `fetch`/undici behavior is wrong for this access pattern in three
9
+ * concrete ways the OpenClaw codebase documented through real
10
+ * production incidents (openclaw#68128 is cited verbatim in
11
+ * fetch.ts:37-39):
12
+ *
13
+ * 1. **HTTP/2 ALPN stalls long-polling.** Undici 8 enables HTTP/2
14
+ * negotiation by default. The Telegram Bot API serves both /1.1
15
+ * and /2, but the /2 stream behavior interacts badly with the
16
+ * long-poll on Windows/IPv6 networks — connections hang past the
17
+ * `getUpdates(timeout=N)` budget. `allowH2: false` pins HTTP/1.1.
18
+ *
19
+ * 2. **Unbounded connection pool leaks sockets.** The default Agent
20
+ * has no per-origin connection limit and an effectively
21
+ * unbounded `keepAliveTimeout`. Long polling rotates dispatchers
22
+ * slowly; the leak grows monotonically until file descriptors
23
+ * exhaust. Pinning `connections: 10`, `keepAliveTimeout: 30s` and
24
+ * `keepAliveMaxTimeout: 600s` keeps the pool bounded.
25
+ *
26
+ * 3. **No way to release sockets on shutdown.** A polling session
27
+ * that stops without destroying its dispatcher leaves keep-alive
28
+ * sockets open. The exported `close()` calls `destroy()` on
29
+ * every owned dispatcher so the runner can swap or drop the
30
+ * transport cleanly.
31
+ *
32
+ * Two additional concerns the transport handles:
33
+ *
34
+ * - **IPv4 fallback with sticky attempts**: some operator networks
35
+ * (notably IPv6-only with broken NAT64) cannot reach
36
+ * `api.telegram.org` over IPv6. OpenClaw observed enough of this
37
+ * to ship sticky fallback to `family: 4`. We port the same
38
+ * behavior with a simpler health model: after N consecutive
39
+ * failures the transport flips to the IPv4-pinned dispatcher.
40
+ *
41
+ * - **Proxy support**: `NUBEMCLAW_PROXY_URL` env var or constructor
42
+ * param mounts an undici `ProxyAgent`. Corporate networks behind
43
+ * egress proxies need this — there is no built-in env support in
44
+ * grammy's default `globalThis.fetch`.
45
+ *
46
+ * What this implementation deliberately omits vs OpenClaw fetch.ts:
47
+ *
48
+ * - **DNS pinning to hardcoded Telegram IPs** (the `149.154.167.220`
49
+ * fallback list). Telegram rotates IPs on its own schedule; a
50
+ * hardcoded list is a hostile maintenance burden and we have no
51
+ * evidence it solved an incident outside OpenClaw's specific
52
+ * deployment. If we see DNS failures in v3 we revisit.
53
+ * - **HTTP exchange capture** (the `captureHttpExchange` calls).
54
+ * That is a debug-only feature wired to OpenClaw's internal
55
+ * observability; not portable to v3 today.
56
+ *
57
+ * The transport returns a `fetch` callable that grammy's `Bot`
58
+ * constructor accepts via `new Bot(token, { client: { fetcher } })`.
59
+ */
60
+ const TELEGRAM_API_HOSTNAME = "api.telegram.org";
61
+ const POOL_KEEP_ALIVE_TIMEOUT_MS = 30_000;
62
+ const POOL_KEEP_ALIVE_MAX_TIMEOUT_MS = 600_000;
63
+ const POOL_CONNECTIONS_PER_ORIGIN = 10;
64
+ const POOL_PIPELINING = 1;
65
+ const FALLBACK_FAILURE_THRESHOLD = 5;
66
+ const FALLBACK_COOLDOWN_INITIAL_MS = 10_000;
67
+ const FALLBACK_COOLDOWN_MAX_MS = 60_000;
68
+ const FALLBACK_NETWORK_ERROR_CODES = new Set([
69
+ "ETIMEDOUT",
70
+ "ENETUNREACH",
71
+ "EHOSTUNREACH",
72
+ "UND_ERR_CONNECT_TIMEOUT",
73
+ "UND_ERR_SOCKET",
74
+ ]);
75
+ const poolOptions = () => ({
76
+ allowH2: false,
77
+ keepAliveTimeout: POOL_KEEP_ALIVE_TIMEOUT_MS,
78
+ keepAliveMaxTimeout: POOL_KEEP_ALIVE_MAX_TIMEOUT_MS,
79
+ connections: POOL_CONNECTIONS_PER_ORIGIN,
80
+ pipelining: POOL_PIPELINING,
81
+ });
82
+ const collectErrorCodes = (err) => {
83
+ const codes = new Set();
84
+ const queue = [err];
85
+ const seen = new Set();
86
+ let i = 0;
87
+ while (i < queue.length) {
88
+ const current = queue[i++];
89
+ if (current === null || current === undefined || seen.has(current))
90
+ continue;
91
+ seen.add(current);
92
+ if (typeof current !== "object")
93
+ continue;
94
+ const code = current.code;
95
+ if (typeof code === "string" && code.trim() !== "") {
96
+ codes.add(code.trim().toUpperCase());
97
+ }
98
+ const cause = current.cause;
99
+ if (cause !== undefined)
100
+ queue.push(cause);
101
+ const nested = current.errors;
102
+ if (Array.isArray(nested))
103
+ for (const n of nested)
104
+ queue.push(n);
105
+ }
106
+ return codes;
107
+ };
108
+ const shouldFallback = (err) => {
109
+ if (err === null || err === undefined)
110
+ return false;
111
+ const codes = collectErrorCodes(err);
112
+ for (const c of FALLBACK_NETWORK_ERROR_CODES) {
113
+ if (codes.has(c))
114
+ return true;
115
+ }
116
+ const message = err !== null && typeof err === "object" && "message" in err
117
+ ? String(err.message).toLowerCase()
118
+ : "";
119
+ return message.includes("fetch failed") && codes.size === 0;
120
+ };
121
+ const silentLogger = {
122
+ info: () => { },
123
+ warn: () => { },
124
+ error: () => { },
125
+ debug: () => { },
126
+ };
127
+ const resolveProxyUrl = (opts) => {
128
+ const explicit = opts.proxyUrl?.trim();
129
+ if (explicit !== undefined && explicit !== "")
130
+ return explicit;
131
+ const envVars = opts.envVars ?? process.env;
132
+ const env = envVars["NUBEMCLAW_PROXY_URL"]?.trim();
133
+ return env !== undefined && env !== "" ? env : undefined;
134
+ };
135
+ export const createTelegramTransport = (opts = {}) => {
136
+ const logger = opts.logger ?? silentLogger;
137
+ const proxyUrl = resolveProxyUrl(opts);
138
+ const owned = new Set();
139
+ const buildPrimary = () => {
140
+ if (proxyUrl !== undefined) {
141
+ return new ProxyAgent({ uri: proxyUrl, ...poolOptions() });
142
+ }
143
+ return new Agent({ ...poolOptions() });
144
+ };
145
+ const buildFallbackIpv4 = () => {
146
+ // Force IPv4. ProxyAgent doesn't take a connect.family override
147
+ // directly via construction — when a proxy is in play we keep the
148
+ // dispatcher identical to primary because the IPv4 vs IPv6 decision
149
+ // is made by the proxy itself. Without a proxy we pin family: 4.
150
+ if (proxyUrl !== undefined) {
151
+ return new ProxyAgent({ uri: proxyUrl, ...poolOptions() });
152
+ }
153
+ return new Agent({
154
+ ...poolOptions(),
155
+ connect: { family: 4 },
156
+ });
157
+ };
158
+ // Lazy dispatcher instantiation per attempt so we only pay the cost
159
+ // when the attempt is actually exercised.
160
+ let primaryDispatcher = null;
161
+ let fallbackDispatcher = null;
162
+ const getPrimary = () => {
163
+ if (primaryDispatcher === null) {
164
+ primaryDispatcher = buildPrimary();
165
+ owned.add(primaryDispatcher);
166
+ }
167
+ return primaryDispatcher;
168
+ };
169
+ const getFallback = () => {
170
+ if (fallbackDispatcher === null) {
171
+ fallbackDispatcher = buildFallbackIpv4();
172
+ owned.add(fallbackDispatcher);
173
+ }
174
+ return fallbackDispatcher;
175
+ };
176
+ const attempts = [
177
+ { kind: "primary", create: getPrimary },
178
+ { kind: "fallback-ipv4", create: getFallback },
179
+ ];
180
+ const health = attempts.map(() => ({
181
+ consecutiveFailures: 0,
182
+ cooldownMs: FALLBACK_COOLDOWN_INITIAL_MS,
183
+ unhealthyUntilMs: 0,
184
+ }));
185
+ let stickyIndex = 0;
186
+ const recordSuccess = (attemptIndex) => {
187
+ const h = health[attemptIndex];
188
+ if (h === undefined)
189
+ return;
190
+ h.consecutiveFailures = 0;
191
+ h.cooldownMs = FALLBACK_COOLDOWN_INITIAL_MS;
192
+ h.unhealthyUntilMs = 0;
193
+ if (attemptIndex < stickyIndex) {
194
+ logger.debug({ from: stickyIndex, to: attemptIndex }, "telegram transport: recovered to lower-cost attempt");
195
+ stickyIndex = attemptIndex;
196
+ }
197
+ };
198
+ const recordFailure = (attemptIndex, err) => {
199
+ if (!shouldFallback(err))
200
+ return;
201
+ const h = health[attemptIndex];
202
+ if (h === undefined)
203
+ return;
204
+ h.consecutiveFailures += 1;
205
+ if (h.consecutiveFailures < FALLBACK_FAILURE_THRESHOLD)
206
+ return;
207
+ const cooldownMs = Math.min(FALLBACK_COOLDOWN_MAX_MS, h.cooldownMs);
208
+ h.consecutiveFailures = 0;
209
+ h.cooldownMs = Math.min(FALLBACK_COOLDOWN_MAX_MS, cooldownMs * 2);
210
+ h.unhealthyUntilMs = Date.now() + cooldownMs;
211
+ logger.warn({ attempt: attempts[attemptIndex]?.kind, cooldownMs }, "telegram transport: attempt marked temporarily unhealthy");
212
+ };
213
+ const promoteSticky = () => {
214
+ if (stickyIndex < attempts.length - 1) {
215
+ stickyIndex += 1;
216
+ logger.warn({ newAttempt: attempts[stickyIndex]?.kind }, "telegram transport: promoting to fallback attempt");
217
+ }
218
+ };
219
+ const wrappedFetch = async (input, init) => {
220
+ let lastErr;
221
+ for (let attemptIndex = stickyIndex; attemptIndex < attempts.length; attemptIndex += 1) {
222
+ const attempt = attempts[attemptIndex];
223
+ if (attempt === undefined)
224
+ break;
225
+ const h = health[attemptIndex];
226
+ if (h !== undefined && h.unhealthyUntilMs > Date.now()) {
227
+ // Skip this attempt while it cools down.
228
+ lastErr = new Error(`telegram transport: attempt '${attempt.kind}' cooling down`);
229
+ continue;
230
+ }
231
+ try {
232
+ const dispatcher = attempt.create();
233
+ // Cast to RequestInit + dispatcher property — undici's RequestInit
234
+ // extends the global with `dispatcher`, but the global `fetch`
235
+ // type doesn't know that.
236
+ const initWithDispatcher = {
237
+ ...init,
238
+ dispatcher,
239
+ };
240
+ const res = await undiciFetch(input, initWithDispatcher);
241
+ recordSuccess(attemptIndex);
242
+ return res;
243
+ }
244
+ catch (cause) {
245
+ lastErr = cause;
246
+ if (!shouldFallback(cause))
247
+ throw cause;
248
+ recordFailure(attemptIndex, cause);
249
+ if (attemptIndex === stickyIndex && attemptIndex < attempts.length - 1) {
250
+ promoteSticky();
251
+ }
252
+ }
253
+ }
254
+ throw lastErr;
255
+ };
256
+ let closed = false;
257
+ const close = async () => {
258
+ if (closed)
259
+ return;
260
+ closed = true;
261
+ const toDestroy = [...owned];
262
+ owned.clear();
263
+ await Promise.all(toDestroy.map(async (d) => {
264
+ try {
265
+ await d.destroy();
266
+ }
267
+ catch {
268
+ // Already destroyed — ignore.
269
+ }
270
+ }));
271
+ };
272
+ return {
273
+ fetch: wrappedFetch,
274
+ close,
275
+ };
276
+ };
277
+ /**
278
+ * Sentinel constant identifying the Telegram API host. Exported for
279
+ * tests that want to assert the transport is being aimed at the right
280
+ * origin (the wrapped fetch does not enforce this — `globalThis.fetch`
281
+ * resolves the URL the caller supplies).
282
+ */
283
+ export const TELEGRAM_HOST = TELEGRAM_API_HOSTNAME;
284
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@nubemclaw/channel-telegram",
3
+ "version": "1.2.2",
4
+ "description": "NubemClaw v3 — Telegram channel wrapping grammy (polling + webhook + setup).",
5
+ "license": "UNLICENSED",
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@grammyjs/transformer-throttler": "1.2.1",
21
+ "grammy": "1.42.0",
22
+ "undici": "8.2.0",
23
+ "zod": "^3.23.8",
24
+ "@nubemclaw/channel-sdk": "1.2.2",
25
+ "@nubemclaw/core": "1.2.2"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -b",
29
+ "clean": "tsc -b --clean && rm -rf dist .cache"
30
+ }
31
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Telegram Bot API response envelope (Phase 13, ADR-0020 §4 — boundary
5
+ * validation on untrusted input). Every Bot API response wraps either
6
+ * a successful `result` or a structured failure with `error_code` +
7
+ * `description`. We validate the envelope at this boundary so the rest
8
+ * of the channel package consumes typed payloads, never raw JSON.
9
+ *
10
+ * Scope: F13 covers only `message` updates with `text`. Photos, voice,
11
+ * documents, stickers, edited messages, callback queries — all reach
12
+ * Telegram but are silently ignored by the polling adapter via
13
+ * `allowed_updates: ["message"]`. The schemas below model exactly what
14
+ * we need to forward through the agent and no more — narrower payloads
15
+ * = smaller attack surface for parser bugs and easier to evolve.
16
+ *
17
+ * `passthrough()` is deliberate on `Message` and `User`: Telegram adds
18
+ * fields between minor versions; we tolerate unknown keys (do not
19
+ * reject) so a Bot API minor bump does not break the channel, but we
20
+ * never propagate them outside this boundary.
21
+ */
22
+
23
+ export const TelegramUserSchema = z
24
+ .object({
25
+ id: z.number().int(),
26
+ is_bot: z.boolean(),
27
+ first_name: z.string().optional(),
28
+ username: z.string().optional(),
29
+ language_code: z.string().optional(),
30
+ })
31
+ .passthrough();
32
+ export type TelegramUser = z.infer<typeof TelegramUserSchema>;
33
+
34
+ export const TelegramChatSchema = z
35
+ .object({
36
+ id: z.number().int(),
37
+ type: z.enum(["private", "group", "supergroup", "channel"]),
38
+ title: z.string().optional(),
39
+ username: z.string().optional(),
40
+ })
41
+ .passthrough();
42
+ export type TelegramChat = z.infer<typeof TelegramChatSchema>;
43
+
44
+ export const TelegramMessageSchema = z
45
+ .object({
46
+ message_id: z.number().int(),
47
+ date: z.number().int(),
48
+ chat: TelegramChatSchema,
49
+ from: TelegramUserSchema.optional(),
50
+ text: z.string().optional(),
51
+ })
52
+ .passthrough();
53
+ export type TelegramMessage = z.infer<typeof TelegramMessageSchema>;
54
+
55
+ /**
56
+ * Update wraps either a new `message` or fields F13 deliberately
57
+ * ignores (edited_message, callback_query, etc.). `allowed_updates`
58
+ * on `getUpdates`/`setWebhook` filters those at Telegram's side, so
59
+ * in practice only `update_id + message` reach us; the schema models
60
+ * the F13 happy path and tolerates extra keys via passthrough.
61
+ */
62
+ export const TelegramUpdateSchema = z
63
+ .object({
64
+ update_id: z.number().int(),
65
+ message: TelegramMessageSchema.optional(),
66
+ })
67
+ .passthrough();
68
+ export type TelegramUpdate = z.infer<typeof TelegramUpdateSchema>;
69
+
70
+ export const TelegramOkEnvelope = <T extends z.ZodTypeAny>(result: T) =>
71
+ z.object({ ok: z.literal(true), result });
72
+
73
+ export const TelegramErrEnvelope = z.object({
74
+ ok: z.literal(false),
75
+ error_code: z.number().int(),
76
+ description: z.string(),
77
+ parameters: z
78
+ .object({
79
+ retry_after: z.number().int().optional(),
80
+ migrate_to_chat_id: z.number().int().optional(),
81
+ })
82
+ .partial()
83
+ .passthrough()
84
+ .optional(),
85
+ });
86
+ export type TelegramErrResponse = z.infer<typeof TelegramErrEnvelope>;
87
+
88
+ /** Convenience type alias for "the API succeeded and gave us this T". */
89
+ export type TelegramOk<T> = { readonly ok: true; readonly result: T };