Skip to content

Commit b798609

Browse files
committed
Telegram: add webhookCertPath and skip stale-socket in webhook mode (#39303)
1 parent 9e1de97 commit b798609

File tree

9 files changed

+64
-4
lines changed

9 files changed

+64
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ Docs: https://docs.openclaw.ai
231231
- Daemon/systemd fresh-install probe: check for OpenClaw's managed user unit before running `systemctl --user is-enabled`, so first-time Linux installs no longer fail on generic missing-unit probe errors. (#38819) Thanks @adaHubble.
232232
- Gateway/Windows restart supervision: relaunch task-managed gateways through Scheduled Task with quoted helper-script command paths, distinguish restart-capable supervisors per platform, and stop orphaned Windows gateway children during self-restart. (#38825) Thanks @obviyus.
233233
- Telegram/native topic command routing: resolve forum-topic native commands through the same conversation route as inbound messages so topic `agentId` overrides and bound topic sessions target the active session instead of the default topic-parent session. (#38871) Thanks @obviyus.
234+
- Gateway/Telegram webhook certificate: add `webhookCertPath` configuration option to allow uploading self-signed certificates during webhook registration, preventing SSL verification failures after health-monitor restarts. Fixes #39303.
235+
- Gateway/Health monitor webhook mode: skip `stale-socket` detection for any channel operating in webhook mode, as there is no persistent outgoing socket to go stale. Fixes #39303.
234236

235237
## 2026.3.2
236238

extensions/telegram/src/channel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
506506
webhookPath: account.config.webhookPath,
507507
webhookHost: account.config.webhookHost,
508508
webhookPort: account.config.webhookPort,
509+
webhookCertPath: account.config.webhookCertPath,
509510
});
510511
},
511512
logoutAccount: async ({ accountId, cfg }) => {

src/config/types.telegram.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ export type TelegramAccountConfig = {
140140
webhookHost?: string;
141141
/** Local webhook listener bind port (default: 8787). */
142142
webhookPort?: number;
143+
/** Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. */
144+
webhookCertPath?: string;
143145
/** Per-action tool gating (default: true for all). */
144146
actions?: TelegramActionConfig;
145147
/** Telegram thread/conversation binding overrides. */

src/config/zod-schema.providers-core.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ export const TelegramAccountSchemaBase = z
221221
.describe(
222222
"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.",
223223
),
224+
webhookCertPath: z
225+
.string()
226+
.optional()
227+
.describe(
228+
"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).",
229+
),
224230
actions: z
225231
.object({
226232
reactions: z.boolean().optional(),

src/gateway/channel-health-policy.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ describe("evaluateChannelHealth", () => {
143143
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
144144
});
145145

146+
it("skips stale-socket detection for channels in webhook mode", () => {
147+
const evaluation = evaluateChannelHealth(
148+
{
149+
running: true,
150+
connected: true,
151+
enabled: true,
152+
configured: true,
153+
lastStartAt: 0,
154+
lastEventAt: 0,
155+
mode: "webhook",
156+
},
157+
{
158+
channelId: "discord",
159+
now: 100_000,
160+
channelConnectGraceMs: 10_000,
161+
staleEventThresholdMs: 30_000,
162+
},
163+
);
164+
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
165+
});
166+
146167
it("does not flag stale sockets for channels without event tracking", () => {
147168
const evaluation = evaluateChannelHealth(
148169
{

src/gateway/channel-health-policy.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type ChannelHealthSnapshot = {
1212
lastEventAt?: number | null;
1313
lastStartAt?: number | null;
1414
reconnectAttempts?: number;
15+
mode?: string;
1516
};
1617

1718
export type ChannelHealthEvaluationReason =
@@ -100,11 +101,13 @@ export function evaluateChannelHealth(
100101
if (snapshot.connected === false) {
101102
return { healthy: false, reason: "disconnected" };
102103
}
103-
// Skip stale-socket check for Telegram (long-polling mode). Each polling request
104-
// acts as a heartbeat, so the half-dead WebSocket scenario this check is designed
105-
// to catch does not apply to Telegram's long-polling architecture.
104+
// Skip stale-socket check for Telegram (long-polling mode) and any channel
105+
// explicitly operating in webhook mode. In these cases, there is no persistent
106+
// outgoing socket that can go half-dead, so the lack of incoming events
107+
// does not necessarily indicate a connection failure.
106108
if (
107109
policy.channelId !== "telegram" &&
110+
snapshot.mode !== "webhook" &&
108111
snapshot.connected === true &&
109112
snapshot.lastEventAt != null
110113
) {

src/telegram/monitor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type MonitorTelegramOpts = {
3030
webhookHost?: string;
3131
proxyFetch?: typeof fetch;
3232
webhookUrl?: string;
33+
webhookCertPath?: string;
3334
};
3435

3536
export function createTelegramRunnerOptions(cfg: OpenClawConfig): RunOptions<unknown> {
@@ -172,6 +173,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
172173
fetch: proxyFetch,
173174
abortSignal: opts.abortSignal,
174175
publicUrl: opts.webhookUrl,
176+
webhookCertPath: opts.webhookCertPath,
175177
});
176178
await waitForAbortSignal(opts.abortSignal);
177179
return;

src/telegram/webhook.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,27 @@ describe("startTelegramWebhook", () => {
353353
);
354354
});
355355

356+
it("registers webhook with certificate when webhookCertPath is provided", async () => {
357+
setWebhookSpy.mockClear();
358+
await withStartedWebhook(
359+
{
360+
secret: TELEGRAM_SECRET,
361+
path: TELEGRAM_WEBHOOK_PATH,
362+
webhookCertPath: "/path/to/cert.pem",
363+
},
364+
async () => {
365+
expect(setWebhookSpy).toHaveBeenCalledWith(
366+
expect.any(String),
367+
expect.objectContaining({
368+
certificate: expect.objectContaining({
369+
fileData: "/path/to/cert.pem",
370+
}),
371+
}),
372+
);
373+
},
374+
);
375+
});
376+
356377
it("invokes webhook handler on matching path", async () => {
357378
handlerSpy.mockClear();
358379
createTelegramBotSpy.mockClear();

src/telegram/webhook.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createServer } from "node:http";
2-
import { webhookCallback } from "grammy";
2+
import { InputFile, webhookCallback } from "grammy";
33
import type { OpenClawConfig } from "../config/config.js";
44
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
55
import { formatErrorMessage } from "../infra/errors.js";
@@ -87,6 +87,7 @@ export async function startTelegramWebhook(opts: {
8787
abortSignal?: AbortSignal;
8888
healthPath?: string;
8989
publicUrl?: string;
90+
webhookCertPath?: string;
9091
}) {
9192
const path = opts.path ?? "/telegram-webhook";
9293
const healthPath = opts.healthPath ?? "/healthz";
@@ -241,6 +242,7 @@ export async function startTelegramWebhook(opts: {
241242
bot.api.setWebhook(publicUrl, {
242243
secret_token: secret,
243244
allowed_updates: resolveTelegramAllowedUpdates(),
245+
certificate: opts.webhookCertPath ? new InputFile(opts.webhookCertPath) : undefined,
244246
}),
245247
});
246248
} catch (err) {

0 commit comments

Comments
 (0)