@mastra/telegram 0.1.0 → 0.1.1-alpha.0

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/index.js CHANGED
@@ -1,680 +1,727 @@
1
- // src/telegram-provider.ts
2
- import { randomUUID, timingSafeEqual } from "crypto";
1
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "crypto";
3
2
  import { AgentChannels, resolveWaitUntil } from "@mastra/core/channels";
4
3
  import { InMemoryChannelsStorage } from "@mastra/core/storage";
5
- import { createTelegramAdapter } from "@chat-adapter/telegram";
6
-
7
- // src/telegram-client.ts
8
- import { randomBytes } from "crypto";
9
-
10
- // src/types.ts
11
- var TELEGRAM_API_BASE_URL = "https://api.telegram.org";
12
- var DEFAULT_ALLOWED_UPDATES = [
13
- "message",
14
- "edited_message",
15
- "channel_post",
16
- "edited_channel_post",
17
- "callback_query",
18
- "message_reaction"
4
+ import { TelegramAdapter, createTelegramAdapter, createTelegramAdapter as createTelegramAdapter$1 } from "@chat-adapter/telegram";
5
+ //#region src/types.ts
6
+ /** Default Telegram Bot API origin. */
7
+ const TELEGRAM_API_BASE_URL = "https://api.telegram.org";
8
+ /**
9
+ * Default update types requested from Telegram. `message_reaction` must be
10
+ * listed explicitly (Telegram omits it otherwise).
11
+ */
12
+ const DEFAULT_ALLOWED_UPDATES = [
13
+ "message",
14
+ "edited_message",
15
+ "channel_post",
16
+ "edited_channel_post",
17
+ "callback_query",
18
+ "message_reaction"
19
19
  ];
20
- var BOTFATHER_DEEP_LINK = "https://t.me/botfather";
21
-
22
- // src/telegram-client.ts
20
+ /**
21
+ * Deep link that opens BotFather so an operator can create a new bot with
22
+ * `/newbot`. Telegram has no OAuth: the resulting BotFather token is pasted
23
+ * back into {@link TelegramProvider.connect} to finish the installation.
24
+ */
25
+ const BOTFATHER_DEEP_LINK = "https://t.me/botfather";
26
+ //#endregion
27
+ //#region src/telegram-client.ts
28
+ /**
29
+ * Call a Bot API method. Sends a `GET` when `payload` is omitted and a JSON
30
+ * `POST` otherwise. Throws when the transport fails or the API returns
31
+ * `ok: false`.
32
+ */
23
33
  async function botApiRequest(botToken, method, apiBaseUrl, payload) {
24
- const init = payload === void 0 ? void 0 : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) };
25
- let response;
26
- try {
27
- response = await fetch(`${apiBaseUrl}/bot${botToken}/${method}`, {
28
- ...init,
29
- signal: AbortSignal.timeout(1e4)
30
- });
31
- } catch (cause) {
32
- throw Object.assign(new Error(`Telegram ${method} request failed`, { cause }), {
33
- isTransportError: true
34
- });
35
- }
36
- const body = await response.json().catch(() => null);
37
- if (!response.ok || !body?.ok) {
38
- const detail = body?.description ?? `HTTP ${response.status}`;
39
- throw new Error(`Telegram ${method} failed: ${detail}`);
40
- }
41
- return body.result;
34
+ const init = payload === void 0 ? void 0 : {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify(payload)
38
+ };
39
+ let response;
40
+ try {
41
+ response = await fetch(`${apiBaseUrl}/bot${botToken}/${method}`, {
42
+ ...init,
43
+ signal: AbortSignal.timeout(1e4)
44
+ });
45
+ } catch (cause) {
46
+ throw Object.assign(new Error(`Telegram ${method} request failed`, { cause }), { isTransportError: true });
47
+ }
48
+ const body = await response.json().catch(() => null);
49
+ if (!response.ok || !body?.ok) {
50
+ const detail = body?.description ?? `HTTP ${response.status}`;
51
+ throw new Error(`Telegram ${method} failed: ${detail}`);
52
+ }
53
+ return body.result;
42
54
  }
55
+ /**
56
+ * Validate a bot token via `getMe` and resolve the bot's identity. Throws if
57
+ * the token is rejected or the returned user is not a bot.
58
+ *
59
+ * @see https://core.telegram.org/bots/api#getme
60
+ */
43
61
  async function getMe(botToken, apiBaseUrl = TELEGRAM_API_BASE_URL) {
44
- let result;
45
- try {
46
- result = await botApiRequest(botToken, "getMe", apiBaseUrl);
47
- } catch (cause) {
48
- if (cause instanceof Error && cause.isTransportError) {
49
- throw cause;
50
- }
51
- throw new Error(`Telegram rejected the bot token: ${cause instanceof Error ? cause.message : String(cause)}`, {
52
- cause
53
- });
54
- }
55
- if (!result?.is_bot) {
56
- throw new Error("Telegram getMe returned a non-bot user; expected a BotFather token");
57
- }
58
- return result;
62
+ let result;
63
+ try {
64
+ result = await botApiRequest(botToken, "getMe", apiBaseUrl);
65
+ } catch (cause) {
66
+ if (cause instanceof Error && cause.isTransportError) throw cause;
67
+ throw new Error(`Telegram rejected the bot token: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
68
+ }
69
+ if (!result?.is_bot) throw new Error("Telegram getMe returned a non-bot user; expected a BotFather token");
70
+ return result;
59
71
  }
72
+ /**
73
+ * Register a per-bot webhook. Setting a webhook disables `getUpdates`
74
+ * (long-polling) for that bot — the two transports are mutually exclusive.
75
+ *
76
+ * @see https://core.telegram.org/bots/api#setwebhook
77
+ */
60
78
  async function setWebhook(botToken, options, apiBaseUrl = TELEGRAM_API_BASE_URL) {
61
- await botApiRequest(botToken, "setWebhook", apiBaseUrl, {
62
- url: options.url,
63
- secret_token: options.secretToken,
64
- allowed_updates: options.allowedUpdates,
65
- drop_pending_updates: options.dropPendingUpdates
66
- });
79
+ await botApiRequest(botToken, "setWebhook", apiBaseUrl, {
80
+ url: options.url,
81
+ secret_token: options.secretToken,
82
+ allowed_updates: options.allowedUpdates,
83
+ drop_pending_updates: options.dropPendingUpdates
84
+ });
67
85
  }
86
+ /**
87
+ * Remove a bot's webhook. Required before switching a bot to long-polling
88
+ * (`getUpdates` fails while a webhook is set).
89
+ *
90
+ * @see https://core.telegram.org/bots/api#deletewebhook
91
+ */
68
92
  async function deleteWebhook(botToken, dropPendingUpdates = false, apiBaseUrl = TELEGRAM_API_BASE_URL) {
69
- await botApiRequest(botToken, "deleteWebhook", apiBaseUrl, {
70
- drop_pending_updates: dropPendingUpdates
71
- });
93
+ await botApiRequest(botToken, "deleteWebhook", apiBaseUrl, { drop_pending_updates: dropPendingUpdates });
72
94
  }
95
+ /**
96
+ * Publish the bot's command list for a scope.
97
+ *
98
+ * @see https://core.telegram.org/bots/api#setmycommands
99
+ */
73
100
  async function setMyCommands(botToken, options, apiBaseUrl = TELEGRAM_API_BASE_URL) {
74
- await botApiRequest(botToken, "setMyCommands", apiBaseUrl, {
75
- commands: options.commands,
76
- scope: options.scope,
77
- language_code: options.languageCode
78
- });
101
+ await botApiRequest(botToken, "setMyCommands", apiBaseUrl, {
102
+ commands: options.commands,
103
+ scope: options.scope,
104
+ language_code: options.languageCode
105
+ });
79
106
  }
107
+ /**
108
+ * Generate a webhook secret token within Telegram's `setWebhook` constraint:
109
+ * 1-256 chars from `[A-Za-z0-9_-]`. base64url of 32 random bytes yields 43
110
+ * such chars.
111
+ *
112
+ * @see https://core.telegram.org/bots/api#setwebhook
113
+ */
80
114
  function generateSecretToken() {
81
- return randomBytes(32).toString("base64url");
115
+ return randomBytes(32).toString("base64url");
82
116
  }
83
-
84
- // src/commands.ts
85
- var DEFAULT_COMMANDS = [
86
- { command: "start", description: "Start a conversation" },
87
- { command: "help", description: "Show what this bot can do" },
88
- { command: "settings", description: "Manage your preferences" }
117
+ //#endregion
118
+ //#region src/commands.ts
119
+ /**
120
+ * Conventional command seed registered when a connect provides none.
121
+ * @see https://core.telegram.org/bots/features#commands
122
+ */
123
+ const DEFAULT_COMMANDS = [
124
+ {
125
+ command: "start",
126
+ description: "Start a conversation"
127
+ },
128
+ {
129
+ command: "help",
130
+ description: "Show what this bot can do"
131
+ },
132
+ {
133
+ command: "settings",
134
+ description: "Manage your preferences"
135
+ }
89
136
  ];
137
+ /**
138
+ * Map user-supplied commands (agent capabilities) to Telegram `BotCommand[]`,
139
+ * enforcing the Bot API constraints: `command` is lowercased, stripped of a
140
+ * leading slash, reduced to `[a-z0-9_]`, and clamped to 1-32 chars;
141
+ * `description` defaults to `Run /<command>` and is clamped to 256 chars.
142
+ * Empty or duplicate command names are dropped.
143
+ */
90
144
  function normalizeCommands(raw) {
91
- if (!raw) return [];
92
- const seen = /* @__PURE__ */ new Set();
93
- const commands = [];
94
- for (const item of raw) {
95
- const input = typeof item === "string" ? { command: item } : item;
96
- const command = input.command.replace(/^\//, "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 32);
97
- if (!command || seen.has(command)) continue;
98
- seen.add(command);
99
- const description = (input.description?.trim() || `Run /${command}`).slice(0, 256);
100
- commands.push({ command, description });
101
- }
102
- return commands;
145
+ if (!raw) return [];
146
+ const seen = /* @__PURE__ */ new Set();
147
+ const commands = [];
148
+ for (const item of raw) {
149
+ const input = typeof item === "string" ? { command: item } : item;
150
+ const command = input.command.replace(/^\//, "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 32);
151
+ if (!command || seen.has(command)) continue;
152
+ seen.add(command);
153
+ const description = (input.description?.trim() || `Run /${command}`).slice(0, 256);
154
+ commands.push({
155
+ command,
156
+ description
157
+ });
158
+ }
159
+ return commands;
103
160
  }
104
-
105
- // src/crypto.ts
106
- import { createCipheriv, createDecipheriv, hkdfSync, randomBytes as randomBytes2 } from "crypto";
107
- var ALGO_PREFIX = "aes-256-gcm-hkdf";
108
- var HKDF_INFO = "mastra-telegram-encryption";
161
+ //#endregion
162
+ //#region src/crypto.ts
163
+ /**
164
+ * Opt-in AES-256-GCM encryption for installation secrets at rest, with
165
+ * HKDF-SHA256 key derivation (mirrors `@mastra/slack`'s `crypto.ts`). Each value
166
+ * gets a fresh random 16-byte salt + 12-byte IV; the salt travels in the
167
+ * ciphertext, so the same passphrase never derives the same key twice. The
168
+ * algorithm prefix lets plaintext and encrypted values coexist during migration,
169
+ * so {@link decrypt} can no-op on plaintext.
170
+ *
171
+ * Format: `aes-256-gcm-hkdf:base64(salt):base64(iv):base64(authTag):base64(ciphertext)`
172
+ */
173
+ const ALGO_PREFIX = "aes-256-gcm-hkdf";
174
+ const HKDF_INFO = "mastra-telegram-encryption";
109
175
  function deriveKey(passphrase, salt) {
110
- return Buffer.from(hkdfSync("sha256", passphrase, salt, HKDF_INFO, 32));
176
+ return Buffer.from(hkdfSync("sha256", passphrase, salt, HKDF_INFO, 32));
111
177
  }
178
+ /** Whether a stored value was produced by {@link encrypt}. */
112
179
  function isEncrypted(value) {
113
- return value.startsWith(`${ALGO_PREFIX}:`);
180
+ return value.startsWith(`${ALGO_PREFIX}:`);
114
181
  }
182
+ /** Encrypt a UTF-8 string with a per-value random salt + IV. */
115
183
  function encrypt(plaintext, passphrase) {
116
- const salt = randomBytes2(16);
117
- const iv = randomBytes2(12);
118
- const cipher = createCipheriv("aes-256-gcm", deriveKey(passphrase, salt), iv);
119
- const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
120
- const tag = cipher.getAuthTag();
121
- return `${ALGO_PREFIX}:${salt.toString("base64")}:${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
184
+ const salt = randomBytes(16);
185
+ const iv = randomBytes(12);
186
+ const cipher = createCipheriv("aes-256-gcm", deriveKey(passphrase, salt), iv);
187
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
188
+ const tag = cipher.getAuthTag();
189
+ return `${ALGO_PREFIX}:${salt.toString("base64")}:${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
122
190
  }
191
+ /** Decrypt a value from {@link encrypt}. Plaintext (unprefixed) is returned unchanged. */
123
192
  function decrypt(value, passphrase) {
124
- if (!isEncrypted(value)) return value;
125
- const [, saltB64, ivB64, tagB64, ctB64] = value.split(":");
126
- if (!saltB64 || !ivB64 || !tagB64 || ctB64 === void 0) {
127
- throw new Error("Invalid ciphertext payload");
128
- }
129
- const decipher = createDecipheriv(
130
- "aes-256-gcm",
131
- deriveKey(passphrase, Buffer.from(saltB64, "base64")),
132
- Buffer.from(ivB64, "base64")
133
- );
134
- decipher.setAuthTag(Buffer.from(tagB64, "base64"));
135
- return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
193
+ if (!isEncrypted(value)) return value;
194
+ const [, saltB64, ivB64, tagB64, ctB64] = value.split(":");
195
+ if (!saltB64 || !ivB64 || !tagB64 || ctB64 === void 0) throw new Error("Invalid ciphertext payload");
196
+ const decipher = createDecipheriv("aes-256-gcm", deriveKey(passphrase, Buffer.from(saltB64, "base64")), Buffer.from(ivB64, "base64"));
197
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
198
+ return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
136
199
  }
137
-
138
- // src/install-store.ts
139
- var PLATFORM = "telegram";
200
+ //#endregion
201
+ //#region src/install-store.ts
202
+ /** Platform identifier used for every stored record and route. */
203
+ const PLATFORM = "telegram";
204
+ /**
205
+ * Persistence for Telegram bot installations, layered over the platform-agnostic
206
+ * `ChannelsStorage` (the same store `@mastra/slack` uses). Installations are
207
+ * keyed by agent — one bot = one agent — and the per-bot secret fields live in
208
+ * the record's `data` blob. When an `encryptionKey` is supplied, `botToken` and
209
+ * `secretToken` are AES-256-GCM encrypted at rest.
210
+ */
140
211
  var TelegramInstallStore = class {
141
- constructor(storage, encryptionKey) {
142
- this.storage = storage;
143
- this.encryptionKey = encryptionKey;
144
- }
145
- storage;
146
- encryptionKey;
147
- /** The active or pending installation for an agent, if any. */
148
- async getByAgent(agentId) {
149
- const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
150
- return record ? this.#fromRecord(record) : null;
151
- }
152
- /** Look up an installation by the routing id in its webhook path (M1 dispatch). */
153
- async getByWebhookId(webhookId) {
154
- const record = await this.storage.getInstallationByWebhookId(webhookId);
155
- return record && record.platform === PLATFORM ? this.#fromRecord(record) : null;
156
- }
157
- /** Insert or replace an installation. */
158
- async save(installation) {
159
- await this.storage.saveInstallation(this.#toRecord(installation));
160
- }
161
- /** All Telegram installations (active and pending). */
162
- async list() {
163
- const records = await this.storage.listInstallations(PLATFORM);
164
- return records.map((r) => this.#fromRecord(r));
165
- }
166
- /** Remove an agent's installation, if present. */
167
- async deleteByAgent(agentId) {
168
- const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
169
- if (record) await this.storage.deleteInstallation(record.id);
170
- }
171
- #enc(value) {
172
- return value && this.encryptionKey ? encrypt(value, this.encryptionKey) : value;
173
- }
174
- #dec(value) {
175
- if (!value) return value;
176
- if (!this.encryptionKey) {
177
- if (isEncrypted(value)) {
178
- throw new Error(
179
- "Telegram installation secrets are encrypted at rest, but no encryption key is configured. Set `encryptionKey` on TelegramProvider or MASTRA_ENCRYPTION_KEY."
180
- );
181
- }
182
- return value;
183
- }
184
- return decrypt(value, this.encryptionKey);
185
- }
186
- #toRecord(install) {
187
- const data = {
188
- botToken: this.#enc(install.botToken),
189
- secretToken: this.#enc(install.secretToken),
190
- username: install.username,
191
- webhookUrl: install.webhookUrl,
192
- commands: install.commands
193
- };
194
- return {
195
- id: install.id,
196
- platform: PLATFORM,
197
- agentId: install.agentId,
198
- status: install.status,
199
- webhookId: install.webhookId,
200
- data,
201
- createdAt: install.installedAt,
202
- updatedAt: /* @__PURE__ */ new Date()
203
- };
204
- }
205
- #fromRecord(record) {
206
- const data = record.data ?? {};
207
- return {
208
- id: record.id,
209
- agentId: record.agentId,
210
- webhookId: record.webhookId ?? "",
211
- status: record.status === "active" ? "active" : "pending",
212
- botToken: this.#dec(data.botToken),
213
- secretToken: this.#dec(data.secretToken),
214
- username: data.username,
215
- webhookUrl: data.webhookUrl,
216
- commands: data.commands,
217
- installedAt: record.createdAt
218
- };
219
- }
212
+ storage;
213
+ encryptionKey;
214
+ constructor(storage, encryptionKey) {
215
+ this.storage = storage;
216
+ this.encryptionKey = encryptionKey;
217
+ }
218
+ /** The active or pending installation for an agent, if any. */
219
+ async getByAgent(agentId) {
220
+ const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
221
+ return record ? this.#fromRecord(record) : null;
222
+ }
223
+ /** Look up an installation by the routing id in its webhook path (M1 dispatch). */
224
+ async getByWebhookId(webhookId) {
225
+ const record = await this.storage.getInstallationByWebhookId(webhookId);
226
+ return record && record.platform === "telegram" ? this.#fromRecord(record) : null;
227
+ }
228
+ /** Insert or replace an installation. */
229
+ async save(installation) {
230
+ await this.storage.saveInstallation(this.#toRecord(installation));
231
+ }
232
+ /** All Telegram installations (active and pending). */
233
+ async list() {
234
+ return (await this.storage.listInstallations(PLATFORM)).map((r) => this.#fromRecord(r));
235
+ }
236
+ /** Remove an agent's installation, if present. */
237
+ async deleteByAgent(agentId) {
238
+ const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
239
+ if (record) await this.storage.deleteInstallation(record.id);
240
+ }
241
+ #enc(value) {
242
+ return value && this.encryptionKey ? encrypt(value, this.encryptionKey) : value;
243
+ }
244
+ #dec(value) {
245
+ if (!value) return value;
246
+ if (!this.encryptionKey) {
247
+ if (isEncrypted(value)) throw new Error("Telegram installation secrets are encrypted at rest, but no encryption key is configured. Set `encryptionKey` on TelegramProvider or MASTRA_ENCRYPTION_KEY.");
248
+ return value;
249
+ }
250
+ return decrypt(value, this.encryptionKey);
251
+ }
252
+ #toRecord(install) {
253
+ const data = {
254
+ botToken: this.#enc(install.botToken),
255
+ secretToken: this.#enc(install.secretToken),
256
+ username: install.username,
257
+ webhookUrl: install.webhookUrl,
258
+ commands: install.commands
259
+ };
260
+ return {
261
+ id: install.id,
262
+ platform: PLATFORM,
263
+ agentId: install.agentId,
264
+ status: install.status,
265
+ webhookId: install.webhookId,
266
+ data,
267
+ createdAt: install.installedAt,
268
+ updatedAt: /* @__PURE__ */ new Date()
269
+ };
270
+ }
271
+ #fromRecord(record) {
272
+ const data = record.data ?? {};
273
+ return {
274
+ id: record.id,
275
+ agentId: record.agentId,
276
+ webhookId: record.webhookId ?? "",
277
+ status: record.status === "active" ? "active" : "pending",
278
+ botToken: this.#dec(data.botToken),
279
+ secretToken: this.#dec(data.secretToken),
280
+ username: data.username,
281
+ webhookUrl: data.webhookUrl,
282
+ commands: data.commands,
283
+ installedAt: record.createdAt
284
+ };
285
+ }
220
286
  };
287
+ /** Project an installation to its public, secret-free info for the editor UI. */
221
288
  function toInstallationInfo(install) {
222
- return {
223
- id: install.id,
224
- platform: PLATFORM,
225
- agentId: install.agentId,
226
- status: install.status,
227
- displayName: install.username,
228
- installedAt: install.installedAt
229
- };
289
+ return {
290
+ id: install.id,
291
+ platform: PLATFORM,
292
+ agentId: install.agentId,
293
+ status: install.status,
294
+ displayName: install.username,
295
+ installedAt: install.installedAt
296
+ };
230
297
  }
231
-
232
- // src/telegram-provider.ts
298
+ //#endregion
299
+ //#region src/telegram-provider.ts
300
+ /**
301
+ * Resolve the per-adapter streaming/typing config the provider applies to the
302
+ * Telegram entry in `AgentChannels.adapters`. This is the wrapper's stream
303
+ * binding: enabling `streaming` runs the adapter's post-and-edit
304
+ * (`editMessageText`) chunking loop, and `typingStatus` keeps a `sendChatAction`
305
+ * indicator alive — both default on.
306
+ */
233
307
  function resolveTelegramAdapterConfig(config) {
234
- return {
235
- streaming: config.streaming ?? true,
236
- typingStatus: config.typingStatus ?? true
237
- };
308
+ return {
309
+ streaming: config.streaming ?? true,
310
+ typingStatus: config.typingStatus ?? true
311
+ };
238
312
  }
239
- var SECRET_HEADER = "x-telegram-bot-api-secret-token";
313
+ /** Header Telegram echoes the per-bot secret on for every webhook POST. */
314
+ const SECRET_HEADER = "x-telegram-bot-api-secret-token";
315
+ /**
316
+ * Telegram channel provider for Mastra — a {@link ChannelProvider} over
317
+ * `@chat-adapter/telegram`. The adapter handles the Bot API transport (webhook
318
+ * parse, send/edit, typing, rich messages); this provider adds the
319
+ * install/lifecycle layer.
320
+ *
321
+ * Implemented:
322
+ * - **`mastra-telegram-i2g.2`** — multi-token install store (one bot = one
323
+ * agent), `connect()`/`disconnect()`, `getMe` token ingestion.
324
+ * - **`mastra-telegram-i2g.3`** — per-bot `setWebhook` lifecycle,
325
+ * `X-Telegram-Bot-Api-Secret-Token` verification, webhook⇄polling exclusion,
326
+ * and a mounted POST route that delegates to `AgentChannels.handleWebhookEvent`.
327
+ *
328
+ * Later: `setMyCommands` + streaming (`mastra-telegram-i2g.4`).
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * const telegram = new TelegramProvider({ baseUrl: 'https://my-app.example.com' })
333
+ * const mastra = new Mastra({ agents: { myAgent }, channels: { telegram } })
334
+ * await telegram.connect('my-agent', { botToken: '123456:ABC-...' }) // → { type: 'immediate' }
335
+ * ```
336
+ */
240
337
  var TelegramProvider = class {
241
- id = PLATFORM;
242
- #config;
243
- #mastra;
244
- #store;
245
- /** Live adapters, keyed by installation id. */
246
- #adapters = /* @__PURE__ */ new Map();
247
- /** Cached sync view of whether any active bot is registered (for {@link getInfo}). */
248
- #configured = false;
249
- #initPromise = null;
250
- constructor(config = {}) {
251
- this.#config = config;
252
- }
253
- /**
254
- * Called by Mastra when this channel is registered.
255
- * @internal
256
- */
257
- __attach(mastra) {
258
- if (this.#mastra && this.#mastra !== mastra) {
259
- this.#initPromise = null;
260
- this.#store = void 0;
261
- this.#adapters.clear();
262
- this.#configured = false;
263
- }
264
- this.#mastra = mastra;
265
- }
266
- /**
267
- * Per-bot webhook route. A single POST endpoint keyed by an opaque
268
- * `webhookId`; the per-bot secret is verified from the request header, never
269
- * carried in the URL. Auto-initializes on first hit (mirrors `@mastra/slack`).
270
- */
271
- getRoutes() {
272
- const self = this;
273
- const withInit = (handler) => {
274
- return async ({ mastra }) => {
275
- self.#mastra = mastra;
276
- await self.#autoInitialize();
277
- return handler.bind(self);
278
- };
279
- };
280
- return [
281
- {
282
- path: `/${PLATFORM}/events/:webhookId`,
283
- method: "POST",
284
- requiresAuth: false,
285
- createHandler: withInit(this.#handleWebhook)
286
- }
287
- ];
288
- }
289
- /** Discovery metadata for the editor UI. */
290
- getInfo() {
291
- return {
292
- id: this.id,
293
- name: "Telegram",
294
- isConfigured: this.#configured,
295
- connectOptionsSchema: {
296
- type: "object",
297
- properties: {
298
- botToken: {
299
- type: "string",
300
- description: "BotFather bot token. Omit to receive a BotFather deep link instead."
301
- },
302
- name: {
303
- type: "string",
304
- description: "Display name for the bot (defaults to the bot's @username)."
305
- }
306
- }
307
- }
308
- };
309
- }
310
- /**
311
- * Restore installations from storage: rebuild an adapter per active bot and
312
- * inject `AgentChannels` so the agent can receive events immediately.
313
- * Idempotent. Does not re-register webhooks (they persist server-side across
314
- * restarts); reconnect an agent if its `baseUrl` changed.
315
- */
316
- async initialize() {
317
- if (this.#initPromise) return this.#initPromise;
318
- this.#initPromise = this.#doInitialize();
319
- try {
320
- await this.#initPromise;
321
- } catch (err) {
322
- this.#initPromise = null;
323
- throw err;
324
- }
325
- }
326
- async #doInitialize() {
327
- const store = await this.#getStore();
328
- const active = (await store.list()).filter((i) => i.status === "active");
329
- this.#configured = active.length > 0;
330
- for (const installation of active) {
331
- try {
332
- await this.#activateInstallation(installation);
333
- } catch (err) {
334
- console.error(`[Telegram] Failed to restore installation "${installation.id}":`, err);
335
- }
336
- }
337
- }
338
- /**
339
- * Update runtime provider settings. Telegram has no global auth credential to
340
- * clear (per-bot tokens are managed via {@link connect}/{@link disconnect}),
341
- * so `null` is a no-op; an object merges `apiBaseUrl`/`baseUrl` overrides.
342
- */
343
- async configure(credentials) {
344
- if (credentials === null) return;
345
- const apiBaseUrlChanged = credentials.apiBaseUrl !== void 0 && credentials.apiBaseUrl !== this.#config.apiBaseUrl;
346
- this.#config = { ...this.#config, ...credentials };
347
- if (!apiBaseUrlChanged) return;
348
- const wasInitialized = this.#initPromise !== null;
349
- for (const adapter of this.#adapters.values()) {
350
- try {
351
- await adapter.stopPolling();
352
- } catch (err) {
353
- console.warn("[Telegram] Failed to stop polling while reconfiguring:", err);
354
- }
355
- }
356
- this.#adapters.clear();
357
- this.#initPromise = null;
358
- if (wasInitialized) await this.initialize();
359
- }
360
- /**
361
- * Connect an agent to a Telegram bot.
362
- *
363
- * - With `options.botToken`: validate via `getMe`, mint a per-bot webhook
364
- * secret, persist the installation, register the transport (webhook or
365
- * polling), and return `{ type: 'immediate' }`.
366
- * - Without a token: persist a pending installation and return
367
- * `{ type: 'deep_link' }` pointing at BotFather.
368
- */
369
- async connect(agentId, options = {}) {
370
- const store = await this.#getStore();
371
- const existing = await store.getByAgent(agentId);
372
- if (existing?.status === "active") {
373
- throw new Error(`Agent "${agentId}" is already connected to Telegram. Disconnect first to reconnect.`);
374
- }
375
- if (!options.botToken) {
376
- const installationId2 = existing?.id ?? randomUUID();
377
- await store.save({
378
- id: installationId2,
379
- agentId,
380
- webhookId: existing?.webhookId ?? randomUUID(),
381
- status: "pending",
382
- installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
383
- });
384
- return { type: "deep_link", url: BOTFATHER_DEEP_LINK, installationId: installationId2 };
385
- }
386
- const me = await getMe(options.botToken, this.#apiBaseUrl());
387
- const installationId = existing?.id ?? randomUUID();
388
- const webhookId = existing?.webhookId ?? randomUUID();
389
- const baseUrl = this.#getBaseUrl();
390
- const mode = this.#resolveMode(baseUrl);
391
- if (mode === "webhook" && !baseUrl) {
392
- throw new Error(
393
- 'TelegramProvider needs a baseUrl to register a webhook. Set `baseUrl`, configure the Mastra server, or use `mode: "polling"`.'
394
- );
395
- }
396
- const webhookUrl = mode === "webhook" ? `${baseUrl}/${PLATFORM}/events/${webhookId}` : void 0;
397
- const commands = normalizeCommands(options.commands ?? this.#config.commands ?? DEFAULT_COMMANDS);
398
- const installation = {
399
- id: installationId,
400
- agentId,
401
- webhookId,
402
- status: "active",
403
- botToken: options.botToken,
404
- secretToken: generateSecretToken(),
405
- username: options.name ?? me.username ?? me.first_name,
406
- webhookUrl,
407
- commands: commands.length ? commands : void 0,
408
- installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
409
- };
410
- await this.#registerTransport(installation, mode);
411
- await this.#registerCommands(installation);
412
- await store.save(installation);
413
- await this.#activateInstallation(installation);
414
- this.#configured = true;
415
- await this.#config.onInstall?.(installation);
416
- return { type: "immediate", installationId };
417
- }
418
- /** Disconnect an agent from Telegram, removing its webhook and installation. */
419
- async disconnect(agentId) {
420
- const store = await this.#getStore();
421
- const existing = await store.getByAgent(agentId);
422
- if (!existing) {
423
- throw new Error(`No Telegram installation found for agent "${agentId}"`);
424
- }
425
- const adapter = this.#adapters.get(existing.id);
426
- if (adapter) {
427
- try {
428
- await adapter.stopPolling();
429
- } catch (err) {
430
- console.warn(`[Telegram] Failed to stop polling for agent "${agentId}":`, err);
431
- }
432
- }
433
- if (existing.botToken) {
434
- try {
435
- await deleteWebhook(existing.botToken, true, this.#apiBaseUrl());
436
- } catch (err) {
437
- console.warn(`[Telegram] Failed to delete webhook for agent "${agentId}":`, err);
438
- }
439
- }
440
- this.#adapters.delete(existing.id);
441
- await store.deleteByAgent(agentId);
442
- this.#configured = (await store.list()).some((i) => i.status === "active");
443
- }
444
- /** List installations (public info only — no tokens or secrets). */
445
- async listInstallations() {
446
- const store = await this.#getStore();
447
- const installations = await store.list();
448
- return installations.map(toInstallationInfo);
449
- }
450
- /**
451
- * Get the full installation for an agent (includes the bot token / secret).
452
- * Returns `null` if the agent has no Telegram installation. Mirrors
453
- * `SlackProvider.getInstallation`.
454
- */
455
- async getInstallation(agentId) {
456
- const store = await this.#getStore();
457
- return await store.getByAgent(agentId) ?? null;
458
- }
459
- /**
460
- * Whether at least one bot is actively registered. Mirrors
461
- * `SlackProvider.isConfigured` (Telegram has no global credential to check —
462
- * "configured" means an active installation exists).
463
- */
464
- isConfigured() {
465
- return this.#configured;
466
- }
467
- /**
468
- * Get the live `TelegramAdapter` for an installation id, if one is active.
469
- * Used for message formatting/posting. Mirrors `SlackProvider.getAdapter`.
470
- */
471
- getAdapter(installationId) {
472
- return this.#adapters.get(installationId);
473
- }
474
- // ===========================================================================
475
- // Webhook handling
476
- // ===========================================================================
477
- async #handleWebhook(c) {
478
- const webhookId = c.req.param("webhookId");
479
- if (!webhookId) return c.json({ ok: false, error: "Missing webhookId" }, 400);
480
- const store = await this.#getStore();
481
- const installation = await store.getByWebhookId(webhookId);
482
- if (!installation || installation.status !== "active") {
483
- return c.json({ ok: false, error: "Unknown webhook" }, 404);
484
- }
485
- const provided = c.req.header(SECRET_HEADER);
486
- if (!secretMatches(provided, installation.secretToken)) {
487
- return c.json({ ok: false, error: "Invalid secret token" }, 401);
488
- }
489
- const agent = this.#resolveAgent(installation.agentId);
490
- if (!agent || !this.#mastra) {
491
- return c.json({ ok: true });
492
- }
493
- const adapter = this.#getOrCreateAdapter(installation);
494
- let channels = agent.getChannels();
495
- if (!channels || channels.adapters[PLATFORM] !== adapter) {
496
- channels = this.#createAgentChannels(agent, adapter);
497
- await channels.initialize(this.#mastra);
498
- }
499
- const waitUntil = this.#config.waitUntil ?? resolveWaitUntil(c);
500
- try {
501
- return await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : void 0);
502
- } catch (err) {
503
- console.error("[Telegram] Error delegating to AgentChannels:", err);
504
- return c.json({ ok: true });
505
- }
506
- }
507
- // ===========================================================================
508
- // Internals
509
- // ===========================================================================
510
- #apiBaseUrl() {
511
- return this.#config.apiBaseUrl ?? TELEGRAM_API_BASE_URL;
512
- }
513
- #resolveMode(baseUrl) {
514
- const mode = this.#config.mode ?? "auto";
515
- if (mode === "auto") return baseUrl ? "webhook" : "polling";
516
- return mode;
517
- }
518
- /** Register (or clear) the receive transport for a bot, enforcing the exclusion. */
519
- async #registerTransport(installation, mode) {
520
- if (!installation.botToken) return;
521
- if (mode === "webhook" && installation.webhookUrl && installation.secretToken) {
522
- await setWebhook(
523
- installation.botToken,
524
- {
525
- url: installation.webhookUrl,
526
- secretToken: installation.secretToken,
527
- allowedUpdates: this.#config.allowedUpdates ?? [...DEFAULT_ALLOWED_UPDATES],
528
- dropPendingUpdates: true
529
- },
530
- this.#apiBaseUrl()
531
- );
532
- } else {
533
- await deleteWebhook(installation.botToken, true, this.#apiBaseUrl());
534
- }
535
- }
536
- /** Publish the bot's command list (best-effort — a failure won't block connect). */
537
- async #registerCommands(installation) {
538
- if (!installation.botToken || !installation.commands?.length) return;
539
- try {
540
- await setMyCommands(
541
- installation.botToken,
542
- { commands: installation.commands, scope: this.#config.commandScope },
543
- this.#apiBaseUrl()
544
- );
545
- } catch (err) {
546
- console.warn(`[Telegram] Failed to register commands for agent "${installation.agentId}":`, err);
547
- }
548
- }
549
- #getOrCreateAdapter(installation) {
550
- const existing = this.#adapters.get(installation.id);
551
- if (existing) return existing;
552
- const adapter = createTelegramAdapter({
553
- botToken: installation.botToken,
554
- secretToken: installation.secretToken,
555
- userName: installation.username,
556
- apiBaseUrl: this.#apiBaseUrl(),
557
- mode: installation.webhookUrl ? "webhook" : this.#config.mode ?? "auto",
558
- ...this.#config.logger !== void 0 ? { logger: this.#config.logger } : {},
559
- ...this.#config.longPolling !== void 0 ? { longPolling: this.#config.longPolling } : {}
560
- });
561
- this.#adapters.set(installation.id, adapter);
562
- return adapter;
563
- }
564
- /** Rebuild the adapter and inject AgentChannels for an active installation. */
565
- async #activateInstallation(installation) {
566
- const agent = this.#resolveAgent(installation.agentId);
567
- const adapter = this.#getOrCreateAdapter(installation);
568
- if (agent && this.#mastra) {
569
- const channels = this.#createAgentChannels(agent, adapter);
570
- await channels.initialize(this.#mastra);
571
- }
572
- }
573
- /**
574
- * Create AgentChannels for an agent with the Telegram adapter, preserving any
575
- * adapters/config the agent author already configured (mirrors `@mastra/slack`).
576
- */
577
- #createAgentChannels(agent, adapter) {
578
- const existing = agent.getChannels();
579
- const existingConfig = existing?.channelConfig;
580
- const cfg = this.#config;
581
- const entry = {
582
- adapter,
583
- ...resolveTelegramAdapterConfig(cfg),
584
- // Telegram has no Block Kit; default tool rendering to plain text so
585
- // 'cards'/'grouped'/'timeline' don't degrade to fallback text unexpectedly.
586
- toolDisplay: cfg.toolDisplay ?? "text",
587
- ...cfg.cors !== void 0 ? { cors: cfg.cors } : {},
588
- ...cfg.formatError !== void 0 ? { formatError: cfg.formatError } : {}
589
- };
590
- const channels = new AgentChannels({
591
- ...existingConfig,
592
- adapters: { ...existingConfig?.adapters, [PLATFORM]: entry },
593
- userName: agent.name,
594
- handlers: cfg.handlers ?? existingConfig?.handlers,
595
- inlineMedia: cfg.inlineMedia ?? existingConfig?.inlineMedia,
596
- inlineLinks: cfg.inlineLinks ?? existingConfig?.inlineLinks,
597
- state: cfg.state ?? existingConfig?.state,
598
- threadContext: cfg.threadContext ?? existingConfig?.threadContext,
599
- chatOptions: cfg.chatOptions ?? existingConfig?.chatOptions,
600
- tools: cfg.tools ?? existingConfig?.tools,
601
- resolveResourceId: cfg.resolveResourceId ?? existingConfig?.resolveResourceId,
602
- waitUntil: cfg.waitUntil ?? existingConfig?.waitUntil,
603
- resolveWaitUntil: cfg.resolveWaitUntil ?? existingConfig?.resolveWaitUntil
604
- });
605
- agent.setChannels(channels);
606
- return channels;
607
- }
608
- async #autoInitialize() {
609
- if (!this.#mastra) return;
610
- await this.initialize();
611
- }
612
- #resolveAgent(agentId) {
613
- try {
614
- return this.#mastra?.getAgentById(agentId);
615
- } catch {
616
- return void 0;
617
- }
618
- }
619
- async #getStore() {
620
- if (this.#store) return this.#store;
621
- const encryptionKey = this.#config.encryptionKey ?? process.env.MASTRA_ENCRYPTION_KEY;
622
- this.#store = new TelegramInstallStore(await this.#resolveStorage(), encryptionKey);
623
- return this.#store;
624
- }
625
- async #resolveStorage() {
626
- if (this.#config.storage) return this.#config.storage;
627
- const mastraStore = this.#mastra?.getStorage();
628
- if (mastraStore) {
629
- try {
630
- await mastraStore.init();
631
- const channels = await mastraStore.getStore("channels");
632
- if (channels) return channels;
633
- } catch {
634
- }
635
- }
636
- return new InMemoryChannelsStorage();
637
- }
638
- #getBaseUrl() {
639
- if (this.#config.baseUrl) return stripTrailingSlash(this.#config.baseUrl);
640
- const server = this.#mastra?.getServer();
641
- if (!server) return void 0;
642
- const protocol = server.studioProtocol ?? "http";
643
- const host = server.studioHost ?? server.host ?? "localhost";
644
- const port = server.studioPort ?? server.port ?? (Number(process.env.PORT) || 4111);
645
- const includePort = !(protocol === "https" && port === 443 || protocol === "http" && port === 80);
646
- return includePort ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
647
- }
338
+ id = PLATFORM;
339
+ #config;
340
+ #mastra;
341
+ #store;
342
+ /** Live adapters, keyed by installation id. */
343
+ #adapters = /* @__PURE__ */ new Map();
344
+ /** Cached sync view of whether any active bot is registered (for {@link getInfo}). */
345
+ #configured = false;
346
+ #initPromise = null;
347
+ constructor(config = {}) {
348
+ this.#config = config;
349
+ }
350
+ /**
351
+ * Called by Mastra when this channel is registered.
352
+ * @internal
353
+ */
354
+ __attach(mastra) {
355
+ if (this.#mastra && this.#mastra !== mastra) {
356
+ this.#initPromise = null;
357
+ this.#store = void 0;
358
+ this.#adapters.clear();
359
+ this.#configured = false;
360
+ }
361
+ this.#mastra = mastra;
362
+ }
363
+ /**
364
+ * Per-bot webhook route. A single POST endpoint keyed by an opaque
365
+ * `webhookId`; the per-bot secret is verified from the request header, never
366
+ * carried in the URL. Auto-initializes on first hit (mirrors `@mastra/slack`).
367
+ */
368
+ getRoutes() {
369
+ const self = this;
370
+ const withInit = (handler) => {
371
+ return async ({ mastra }) => {
372
+ self.#mastra = mastra;
373
+ await self.#autoInitialize();
374
+ return handler.bind(self);
375
+ };
376
+ };
377
+ return [{
378
+ path: `/${PLATFORM}/events/:webhookId`,
379
+ method: "POST",
380
+ requiresAuth: false,
381
+ createHandler: withInit(this.#handleWebhook)
382
+ }];
383
+ }
384
+ /** Discovery metadata for the editor UI. */
385
+ getInfo() {
386
+ return {
387
+ id: this.id,
388
+ name: "Telegram",
389
+ isConfigured: this.#configured,
390
+ connectOptionsSchema: {
391
+ type: "object",
392
+ properties: {
393
+ botToken: {
394
+ type: "string",
395
+ description: "BotFather bot token. Omit to receive a BotFather deep link instead."
396
+ },
397
+ name: {
398
+ type: "string",
399
+ description: "Display name for the bot (defaults to the bot's @username)."
400
+ }
401
+ }
402
+ }
403
+ };
404
+ }
405
+ /**
406
+ * Restore installations from storage: rebuild an adapter per active bot and
407
+ * inject `AgentChannels` so the agent can receive events immediately.
408
+ * Idempotent. Does not re-register webhooks (they persist server-side across
409
+ * restarts); reconnect an agent if its `baseUrl` changed.
410
+ */
411
+ async initialize() {
412
+ if (this.#initPromise) return this.#initPromise;
413
+ this.#initPromise = this.#doInitialize();
414
+ try {
415
+ await this.#initPromise;
416
+ } catch (err) {
417
+ this.#initPromise = null;
418
+ throw err;
419
+ }
420
+ }
421
+ async #doInitialize() {
422
+ const active = (await (await this.#getStore()).list()).filter((i) => i.status === "active");
423
+ this.#configured = active.length > 0;
424
+ for (const installation of active) try {
425
+ await this.#activateInstallation(installation);
426
+ } catch (err) {
427
+ console.error(`[Telegram] Failed to restore installation "${installation.id}":`, err);
428
+ }
429
+ }
430
+ /**
431
+ * Update runtime provider settings. Telegram has no global auth credential to
432
+ * clear (per-bot tokens are managed via {@link connect}/{@link disconnect}),
433
+ * so `null` is a no-op; an object merges `apiBaseUrl`/`baseUrl` overrides.
434
+ */
435
+ async configure(credentials) {
436
+ if (credentials === null) return;
437
+ const apiBaseUrlChanged = credentials.apiBaseUrl !== void 0 && credentials.apiBaseUrl !== this.#config.apiBaseUrl;
438
+ this.#config = {
439
+ ...this.#config,
440
+ ...credentials
441
+ };
442
+ if (!apiBaseUrlChanged) return;
443
+ const wasInitialized = this.#initPromise !== null;
444
+ for (const adapter of this.#adapters.values()) try {
445
+ await adapter.stopPolling();
446
+ } catch (err) {
447
+ console.warn("[Telegram] Failed to stop polling while reconfiguring:", err);
448
+ }
449
+ this.#adapters.clear();
450
+ this.#initPromise = null;
451
+ if (wasInitialized) await this.initialize();
452
+ }
453
+ /**
454
+ * Connect an agent to a Telegram bot.
455
+ *
456
+ * - With `options.botToken`: validate via `getMe`, mint a per-bot webhook
457
+ * secret, persist the installation, register the transport (webhook or
458
+ * polling), and return `{ type: 'immediate' }`.
459
+ * - Without a token: persist a pending installation and return
460
+ * `{ type: 'deep_link' }` pointing at BotFather.
461
+ */
462
+ async connect(agentId, options = {}) {
463
+ const store = await this.#getStore();
464
+ const existing = await store.getByAgent(agentId);
465
+ if (existing?.status === "active") throw new Error(`Agent "${agentId}" is already connected to Telegram. Disconnect first to reconnect.`);
466
+ if (!options.botToken) {
467
+ const installationId = existing?.id ?? randomUUID();
468
+ await store.save({
469
+ id: installationId,
470
+ agentId,
471
+ webhookId: existing?.webhookId ?? randomUUID(),
472
+ status: "pending",
473
+ installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
474
+ });
475
+ return {
476
+ type: "deep_link",
477
+ url: BOTFATHER_DEEP_LINK,
478
+ installationId
479
+ };
480
+ }
481
+ const me = await getMe(options.botToken, this.#apiBaseUrl());
482
+ const installationId = existing?.id ?? randomUUID();
483
+ const webhookId = existing?.webhookId ?? randomUUID();
484
+ const baseUrl = this.#getBaseUrl();
485
+ const mode = this.#resolveMode(baseUrl);
486
+ if (mode === "webhook" && !baseUrl) throw new Error("TelegramProvider needs a baseUrl to register a webhook. Set `baseUrl`, configure the Mastra server, or use `mode: \"polling\"`.");
487
+ const webhookUrl = mode === "webhook" ? `${baseUrl}/${PLATFORM}/events/${webhookId}` : void 0;
488
+ const commands = normalizeCommands(options.commands ?? this.#config.commands ?? DEFAULT_COMMANDS);
489
+ const installation = {
490
+ id: installationId,
491
+ agentId,
492
+ webhookId,
493
+ status: "active",
494
+ botToken: options.botToken,
495
+ secretToken: generateSecretToken(),
496
+ username: options.name ?? me.username ?? me.first_name,
497
+ webhookUrl,
498
+ commands: commands.length ? commands : void 0,
499
+ installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
500
+ };
501
+ await this.#registerTransport(installation, mode);
502
+ await this.#registerCommands(installation);
503
+ await store.save(installation);
504
+ await this.#activateInstallation(installation);
505
+ this.#configured = true;
506
+ await this.#config.onInstall?.(installation);
507
+ return {
508
+ type: "immediate",
509
+ installationId
510
+ };
511
+ }
512
+ /** Disconnect an agent from Telegram, removing its webhook and installation. */
513
+ async disconnect(agentId) {
514
+ const store = await this.#getStore();
515
+ const existing = await store.getByAgent(agentId);
516
+ if (!existing) throw new Error(`No Telegram installation found for agent "${agentId}"`);
517
+ const adapter = this.#adapters.get(existing.id);
518
+ if (adapter) try {
519
+ await adapter.stopPolling();
520
+ } catch (err) {
521
+ console.warn(`[Telegram] Failed to stop polling for agent "${agentId}":`, err);
522
+ }
523
+ if (existing.botToken) try {
524
+ await deleteWebhook(existing.botToken, true, this.#apiBaseUrl());
525
+ } catch (err) {
526
+ console.warn(`[Telegram] Failed to delete webhook for agent "${agentId}":`, err);
527
+ }
528
+ this.#adapters.delete(existing.id);
529
+ await store.deleteByAgent(agentId);
530
+ this.#configured = (await store.list()).some((i) => i.status === "active");
531
+ }
532
+ /** List installations (public info only — no tokens or secrets). */
533
+ async listInstallations() {
534
+ return (await (await this.#getStore()).list()).map(toInstallationInfo);
535
+ }
536
+ /**
537
+ * Get the full installation for an agent (includes the bot token / secret).
538
+ * Returns `null` if the agent has no Telegram installation. Mirrors
539
+ * `SlackProvider.getInstallation`.
540
+ */
541
+ async getInstallation(agentId) {
542
+ return await (await this.#getStore()).getByAgent(agentId) ?? null;
543
+ }
544
+ /**
545
+ * Whether at least one bot is actively registered. Mirrors
546
+ * `SlackProvider.isConfigured` (Telegram has no global credential to check —
547
+ * "configured" means an active installation exists).
548
+ */
549
+ isConfigured() {
550
+ return this.#configured;
551
+ }
552
+ /**
553
+ * Get the live `TelegramAdapter` for an installation id, if one is active.
554
+ * Used for message formatting/posting. Mirrors `SlackProvider.getAdapter`.
555
+ */
556
+ getAdapter(installationId) {
557
+ return this.#adapters.get(installationId);
558
+ }
559
+ async #handleWebhook(c) {
560
+ const webhookId = c.req.param("webhookId");
561
+ if (!webhookId) return c.json({
562
+ ok: false,
563
+ error: "Missing webhookId"
564
+ }, 400);
565
+ const installation = await (await this.#getStore()).getByWebhookId(webhookId);
566
+ if (!installation || installation.status !== "active") return c.json({
567
+ ok: false,
568
+ error: "Unknown webhook"
569
+ }, 404);
570
+ if (!secretMatches(c.req.header(SECRET_HEADER), installation.secretToken)) return c.json({
571
+ ok: false,
572
+ error: "Invalid secret token"
573
+ }, 401);
574
+ const agent = this.#resolveAgent(installation.agentId);
575
+ if (!agent || !this.#mastra) return c.json({ ok: true });
576
+ const adapter = this.#getOrCreateAdapter(installation);
577
+ let channels = agent.getChannels();
578
+ if (!channels || channels.adapters["telegram"] !== adapter) {
579
+ channels = this.#createAgentChannels(agent, adapter);
580
+ await channels.initialize(this.#mastra);
581
+ }
582
+ const waitUntil = this.#config.waitUntil ?? resolveWaitUntil(c);
583
+ try {
584
+ return await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : void 0);
585
+ } catch (err) {
586
+ console.error("[Telegram] Error delegating to AgentChannels:", err);
587
+ return c.json({ ok: true });
588
+ }
589
+ }
590
+ #apiBaseUrl() {
591
+ return this.#config.apiBaseUrl ?? "https://api.telegram.org";
592
+ }
593
+ #resolveMode(baseUrl) {
594
+ const mode = this.#config.mode ?? "auto";
595
+ if (mode === "auto") return baseUrl ? "webhook" : "polling";
596
+ return mode;
597
+ }
598
+ /** Register (or clear) the receive transport for a bot, enforcing the exclusion. */
599
+ async #registerTransport(installation, mode) {
600
+ if (!installation.botToken) return;
601
+ if (mode === "webhook" && installation.webhookUrl && installation.secretToken) await setWebhook(installation.botToken, {
602
+ url: installation.webhookUrl,
603
+ secretToken: installation.secretToken,
604
+ allowedUpdates: this.#config.allowedUpdates ?? [...DEFAULT_ALLOWED_UPDATES],
605
+ dropPendingUpdates: true
606
+ }, this.#apiBaseUrl());
607
+ else await deleteWebhook(installation.botToken, true, this.#apiBaseUrl());
608
+ }
609
+ /** Publish the bot's command list (best-effort — a failure won't block connect). */
610
+ async #registerCommands(installation) {
611
+ if (!installation.botToken || !installation.commands?.length) return;
612
+ try {
613
+ await setMyCommands(installation.botToken, {
614
+ commands: installation.commands,
615
+ scope: this.#config.commandScope
616
+ }, this.#apiBaseUrl());
617
+ } catch (err) {
618
+ console.warn(`[Telegram] Failed to register commands for agent "${installation.agentId}":`, err);
619
+ }
620
+ }
621
+ #getOrCreateAdapter(installation) {
622
+ const existing = this.#adapters.get(installation.id);
623
+ if (existing) return existing;
624
+ const adapter = createTelegramAdapter$1({
625
+ botToken: installation.botToken,
626
+ secretToken: installation.secretToken,
627
+ userName: installation.username,
628
+ apiBaseUrl: this.#apiBaseUrl(),
629
+ mode: installation.webhookUrl ? "webhook" : this.#config.mode ?? "auto",
630
+ ...this.#config.logger !== void 0 ? { logger: this.#config.logger } : {},
631
+ ...this.#config.longPolling !== void 0 ? { longPolling: this.#config.longPolling } : {}
632
+ });
633
+ this.#adapters.set(installation.id, adapter);
634
+ return adapter;
635
+ }
636
+ /** Rebuild the adapter and inject AgentChannels for an active installation. */
637
+ async #activateInstallation(installation) {
638
+ const agent = this.#resolveAgent(installation.agentId);
639
+ const adapter = this.#getOrCreateAdapter(installation);
640
+ if (agent && this.#mastra) await this.#createAgentChannels(agent, adapter).initialize(this.#mastra);
641
+ }
642
+ /**
643
+ * Create AgentChannels for an agent with the Telegram adapter, preserving any
644
+ * adapters/config the agent author already configured (mirrors `@mastra/slack`).
645
+ */
646
+ #createAgentChannels(agent, adapter) {
647
+ const existingConfig = agent.getChannels()?.channelConfig;
648
+ const cfg = this.#config;
649
+ const entry = {
650
+ adapter,
651
+ ...resolveTelegramAdapterConfig(cfg),
652
+ toolDisplay: cfg.toolDisplay ?? "text",
653
+ ...cfg.cors !== void 0 ? { cors: cfg.cors } : {},
654
+ ...cfg.formatError !== void 0 ? { formatError: cfg.formatError } : {}
655
+ };
656
+ const channels = new AgentChannels({
657
+ ...existingConfig,
658
+ adapters: {
659
+ ...existingConfig?.adapters,
660
+ [PLATFORM]: entry
661
+ },
662
+ userName: agent.name,
663
+ handlers: cfg.handlers ?? existingConfig?.handlers,
664
+ inlineMedia: cfg.inlineMedia ?? existingConfig?.inlineMedia,
665
+ inlineLinks: cfg.inlineLinks ?? existingConfig?.inlineLinks,
666
+ state: cfg.state ?? existingConfig?.state,
667
+ threadContext: cfg.threadContext ?? existingConfig?.threadContext,
668
+ chatOptions: cfg.chatOptions ?? existingConfig?.chatOptions,
669
+ tools: cfg.tools ?? existingConfig?.tools,
670
+ resolveResourceId: cfg.resolveResourceId ?? existingConfig?.resolveResourceId,
671
+ waitUntil: cfg.waitUntil ?? existingConfig?.waitUntil,
672
+ resolveWaitUntil: cfg.resolveWaitUntil ?? existingConfig?.resolveWaitUntil
673
+ });
674
+ agent.setChannels(channels);
675
+ return channels;
676
+ }
677
+ async #autoInitialize() {
678
+ if (!this.#mastra) return;
679
+ await this.initialize();
680
+ }
681
+ #resolveAgent(agentId) {
682
+ try {
683
+ return this.#mastra?.getAgentById(agentId);
684
+ } catch {
685
+ return;
686
+ }
687
+ }
688
+ async #getStore() {
689
+ if (this.#store) return this.#store;
690
+ const encryptionKey = this.#config.encryptionKey ?? process.env.MASTRA_ENCRYPTION_KEY;
691
+ this.#store = new TelegramInstallStore(await this.#resolveStorage(), encryptionKey);
692
+ return this.#store;
693
+ }
694
+ async #resolveStorage() {
695
+ if (this.#config.storage) return this.#config.storage;
696
+ const mastraStore = this.#mastra?.getStorage();
697
+ if (mastraStore) try {
698
+ await mastraStore.init();
699
+ const channels = await mastraStore.getStore("channels");
700
+ if (channels) return channels;
701
+ } catch {}
702
+ return new InMemoryChannelsStorage();
703
+ }
704
+ #getBaseUrl() {
705
+ if (this.#config.baseUrl) return stripTrailingSlash(this.#config.baseUrl);
706
+ const server = this.#mastra?.getServer();
707
+ if (!server) return void 0;
708
+ const protocol = server.studioProtocol ?? "http";
709
+ const host = server.studioHost ?? server.host ?? "localhost";
710
+ const port = server.studioPort ?? server.port ?? (Number(process.env.PORT) || 4111);
711
+ return !(protocol === "https" && port === 443 || protocol === "http" && port === 80) ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
712
+ }
648
713
  };
714
+ /** Constant-time comparison of the webhook secret header. */
649
715
  function secretMatches(provided, expected) {
650
- if (!provided || !expected) return false;
651
- const a = Buffer.from(provided);
652
- const b = Buffer.from(expected);
653
- return a.length === b.length && timingSafeEqual(a, b);
716
+ if (!provided || !expected) return false;
717
+ const a = Buffer.from(provided);
718
+ const b = Buffer.from(expected);
719
+ return a.length === b.length && timingSafeEqual(a, b);
654
720
  }
655
721
  function stripTrailingSlash(url) {
656
- return url.endsWith("/") ? url.slice(0, -1) : url;
722
+ return url.endsWith("/") ? url.slice(0, -1) : url;
657
723
  }
724
+ //#endregion
725
+ export { BOTFATHER_DEEP_LINK, DEFAULT_ALLOWED_UPDATES, DEFAULT_COMMANDS, PLATFORM, TELEGRAM_API_BASE_URL, TelegramAdapter, TelegramInstallStore, TelegramProvider, createTelegramAdapter, deleteWebhook, generateSecretToken, getMe, normalizeCommands, resolveTelegramAdapterConfig, setMyCommands, setWebhook, toInstallationInfo };
658
726
 
659
- // src/index.ts
660
- import { createTelegramAdapter as createTelegramAdapter2, TelegramAdapter } from "@chat-adapter/telegram";
661
- export {
662
- BOTFATHER_DEEP_LINK,
663
- DEFAULT_ALLOWED_UPDATES,
664
- DEFAULT_COMMANDS,
665
- PLATFORM,
666
- TELEGRAM_API_BASE_URL,
667
- TelegramAdapter,
668
- TelegramInstallStore,
669
- TelegramProvider,
670
- createTelegramAdapter2 as createTelegramAdapter,
671
- deleteWebhook,
672
- generateSecretToken,
673
- getMe,
674
- normalizeCommands,
675
- resolveTelegramAdapterConfig,
676
- setMyCommands,
677
- setWebhook,
678
- toInstallationInfo
679
- };
680
727
  //# sourceMappingURL=index.js.map