@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.
- package/dist/api/client.d.ts +64 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +108 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/schemas.d.ts +593 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +74 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/channel.d.ts +59 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +246 -0
- package/dist/channel.js.map +1 -0
- package/dist/config.d.ts +100 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/setup.d.ts +24 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +104 -0
- package/dist/setup.js.map +1 -0
- package/dist/transport.d.ts +37 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +284 -0
- package/dist/transport.js.map +1 -0
- package/package.json +31 -0
- package/src/api/schemas.ts +89 -0
- package/src/channel.test.ts +287 -0
- package/src/channel.ts +345 -0
- package/src/config.ts +53 -0
- package/src/index.ts +10 -0
- package/src/setup.test.ts +169 -0
- package/src/setup.ts +139 -0
- package/src/transport.test.ts +67 -0
- package/src/transport.ts +351 -0
|
@@ -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 };
|