@deadragdoll/tellymcp 0.0.12 → 0.0.14

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.
Files changed (83) hide show
  1. package/.env.example.client +5 -1
  2. package/.env.example.gateway +16 -1
  3. package/CHANGELOG.md +30 -0
  4. package/README-ru.md +33 -0
  5. package/README.md +33 -0
  6. package/TOOLS.md +384 -9
  7. package/config/templates/env.both.template +15 -2
  8. package/config/templates/env.client.template +5 -2
  9. package/config/templates/env.gateway.template +15 -2
  10. package/dist/cli.js +137 -2
  11. package/dist/services/features/telegram-mcp/browser.service.js +56 -1
  12. package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
  13. package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
  14. package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
  15. package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
  16. package/dist/services/features/telegram-mcp/mcp-server.service.js +30 -0
  17. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +23 -1
  18. package/dist/services/features/telegram-mcp/src/app/config/env.js +132 -2
  19. package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
  20. package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
  21. package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
  22. package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
  23. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +205 -17
  24. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
  25. package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
  26. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
  27. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
  28. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
  29. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
  30. package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
  31. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
  32. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
  33. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
  34. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
  35. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
  36. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
  37. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
  38. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +890 -23
  39. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +536 -0
  40. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +80 -0
  41. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +736 -0
  42. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +188 -0
  43. package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
  44. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
  45. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
  46. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
  47. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
  48. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
  49. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
  50. package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
  51. package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
  52. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
  53. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
  54. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
  55. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
  56. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
  57. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
  58. package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
  59. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
  60. package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
  61. package/docs/CHAT_CONNECTOR.md +134 -0
  62. package/docs/STANDALONE-ru.md +55 -6
  63. package/docs/STANDALONE.md +55 -6
  64. package/package.json +10 -5
  65. package/packages/chrome-attach-extension/dist/background.js +1735 -0
  66. package/packages/chrome-attach-extension/dist/icon.svg +6 -0
  67. package/packages/chrome-attach-extension/dist/manifest.json +37 -0
  68. package/packages/chrome-attach-extension/dist/options.html +312 -0
  69. package/packages/chrome-attach-extension/dist/options.js +606 -0
  70. package/packages/chrome-attach-extension/dist/popup.html +93 -0
  71. package/packages/chrome-attach-extension/dist/popup.js +79 -0
  72. package/packages/chrome-attach-extension/dist/recorder-content.js +96 -0
  73. package/packages/chrome-attach-extension/dist/recorder-page.js +282 -0
  74. package/packages/firefox-attach-extension/README.md +13 -0
  75. package/packages/firefox-attach-extension/dist/background.js +1622 -0
  76. package/packages/firefox-attach-extension/dist/icon.svg +6 -0
  77. package/packages/firefox-attach-extension/dist/manifest.json +44 -0
  78. package/packages/firefox-attach-extension/dist/options.html +312 -0
  79. package/packages/firefox-attach-extension/dist/options.js +540 -0
  80. package/packages/firefox-attach-extension/dist/popup.html +93 -0
  81. package/packages/firefox-attach-extension/dist/popup.js +64 -0
  82. package/packages/firefox-attach-extension/dist/recorder-content.js +90 -0
  83. package/packages/firefox-attach-extension/dist/recorder-page.js +318 -0
@@ -12,6 +12,8 @@ const transport_1 = require("../../shared/integrations/telegram/transport");
12
12
  const minioExchangeStore_1 = require("../../shared/integrations/object-storage/minioExchangeStore");
13
13
  const gatewayHttpService_1 = require("../../features/distributed-gateway/model/gatewayHttpService");
14
14
  const gatewayClientAccess_1 = require("../../features/distributed-client/model/gatewayClientAccess");
15
+ const firefoxAttachServer_1 = require("../../features/browser-attach/model/firefoxAttachServer");
16
+ const temporaryFileLinkStore_1 = require("../../features/file-content/model/temporaryFileLinkStore");
15
17
  const client_2 = require("../../shared/integrations/terminal/client");
16
18
  const ptyRegistry_1 = require("../../shared/integrations/terminal/ptyRegistry");
17
19
  async function createAppRuntime(input) {
@@ -33,6 +35,7 @@ async function createAppRuntime(input) {
33
35
  httpPort: config.mcp.httpPort,
34
36
  httpPath: config.mcp.httpPath,
35
37
  bearerAuthEnabled: Boolean(config.mcp.bearerToken),
38
+ oauthConnectorEnabled: Boolean(config.oauth),
36
39
  enableDebugRoutes: config.mcp.enableDebugRoutes,
37
40
  enablePruneRoute: config.mcp.enablePruneRoute,
38
41
  },
@@ -55,7 +58,6 @@ async function createAppRuntime(input) {
55
58
  initDataTtlSeconds: config.webapp.initDataTtlSeconds,
56
59
  sessionTtlSeconds: config.webapp.sessionTtlSeconds,
57
60
  launchMode: config.webapp.launchMode,
58
- pollIntervalMs: config.webapp.pollIntervalMs,
59
61
  actionCooldownMs: config.webapp.actionCooldownMs,
60
62
  },
61
63
  terminal: {
@@ -63,6 +65,15 @@ async function createAppRuntime(input) {
63
65
  nudgeDebounceSeconds: config.terminal.nudgeDebounceSeconds,
64
66
  nudgeCooldownSeconds: config.terminal.nudgeCooldownSeconds,
65
67
  },
68
+ browser: {
69
+ enabled: config.browser.enabled,
70
+ headless: config.browser.headless,
71
+ devtools: config.browser.devtools,
72
+ attachEnabled: config.browser.attach.enabled,
73
+ attachHost: config.browser.attach.host,
74
+ attachPort: config.browser.attach.port,
75
+ attachPath: config.browser.attach.path,
76
+ },
66
77
  telegram: {
67
78
  webhookEnabled: config.telegram.webhook.enabled,
68
79
  webhookPath: config.telegram.webhook.path,
@@ -200,6 +211,13 @@ async function createAppRuntime(input) {
200
211
  await telegramTransport.sendStartupNotifications();
201
212
  logger.info("Startup Telegram notifications completed");
202
213
  const gatewayHttpService = new gatewayHttpService_1.GatewayHttpService(config, input.callBroker);
214
+ const temporaryFileLinkStore = config.distributed.mode !== "client" &&
215
+ config.distributed.gatewayPublicUrl?.trim()
216
+ ? new temporaryFileLinkStore_1.TemporaryFileLinkStore(config.distributed.gatewayPublicUrl, logger)
217
+ : null;
218
+ await temporaryFileLinkStore?.start();
219
+ const firefoxAttachServer = new firefoxAttachServer_1.FirefoxAttachServer(config, logger, stateStore, stateStore);
220
+ await firefoxAttachServer.start();
203
221
  return {
204
222
  callBroker: input.callBroker,
205
223
  config,
@@ -217,8 +235,12 @@ async function createAppRuntime(input) {
217
235
  projectIdentityResolver,
218
236
  webAppLaunchRegistry,
219
237
  gatewayHttpService,
238
+ firefoxAttachServer,
239
+ temporaryFileLinkStore,
220
240
  shutdown: async () => {
221
241
  logger.info("Shutdown started");
242
+ await temporaryFileLinkStore?.stop();
243
+ await firefoxAttachServer.stop();
222
244
  await telegramTransport.stop();
223
245
  (0, ptyRegistry_1.stopAllPtyTargets)();
224
246
  redis.disconnect();
@@ -93,6 +93,22 @@ const envSchema = z.object({
93
93
  MCP_HTTP_PORT: z.coerce.number().int().positive().default(8787),
94
94
  MCP_HTTP_PATH: z.string().min(1).default("/mcp"),
95
95
  MCP_HTTP_BEARER_TOKEN: optionalNonEmptyString,
96
+ TELLYMCP_PUBLIC_URL: optionalUrlString,
97
+ TELLYMCP_OAUTH_ISSUER: optionalUrlString,
98
+ TELLYMCP_OAUTH_AUDIENCE: optionalUrlString,
99
+ TELLYMCP_MAGIC_TOKEN: optionalNonEmptyString,
100
+ TELLYMCP_MAGIC_TOKEN_HASH: optionalNonEmptyString,
101
+ TELLYMCP_OAUTH_CLIENT_ID: optionalNonEmptyString,
102
+ TELLYMCP_OAUTH_CLIENT_SECRET: optionalNonEmptyString,
103
+ TELLYMCP_ALLOWED_REDIRECT_URIS: optionalNonEmptyString,
104
+ TELLYMCP_OAUTH_PRIVATE_KEY_PEM: optionalNonEmptyString,
105
+ TELLYMCP_AUTH_CODE_TTL_SECONDS: z.coerce
106
+ .number()
107
+ .int()
108
+ .positive()
109
+ .default(300),
110
+ TELLYMCP_OAUTH_SCOPES: z.string().min(1).default("tellymcp:read tellymcp:write"),
111
+ TELLYMCP_OAUTH_KEY_ID: z.string().min(1).default("tellymcp-oauth"),
96
112
  MCP_HTTP_ENABLE_DEBUG_ROUTES: z
97
113
  .string()
98
114
  .optional()
@@ -145,7 +161,6 @@ const envSchema = z.object({
145
161
  .enum(["default", "expand", "fullscreen"])
146
162
  .default("fullscreen"),
147
163
  WEBAPP_VISIBLE_SCREENS: z.coerce.number().int().positive().default(2),
148
- WEBAPP_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(2000),
149
164
  WEBAPP_ACTION_COOLDOWN_MS: z.coerce.number().int().nonnegative().default(150),
150
165
  MCP_XCHANGE_DIR: z.string().min(1).default(".mcp-xchange"),
151
166
  TERMINAL_SHELL: z.string().min(1).default(process.env.SHELL || "bash"),
@@ -211,6 +226,13 @@ const envSchema = z.object({
211
226
  BROWSER_EXECUTABLE_PATH: optionalNonEmptyString,
212
227
  BROWSER_CHANNEL: z.preprocess(emptyStringToUndefined, z.enum(["chrome", "chromium", "msedge"]).optional()),
213
228
  BROWSER_SLOW_MO_MS: z.coerce.number().int().nonnegative().default(0),
229
+ BROWSER_ATTACH_ENABLED: z
230
+ .string()
231
+ .optional()
232
+ .transform((value) => value === "true"),
233
+ BROWSER_ATTACH_WS_HOST: z.string().min(1).default("127.0.0.1"),
234
+ BROWSER_ATTACH_WS_PORT: z.coerce.number().int().positive().default(9999),
235
+ BROWSER_ATTACH_WS_PATH: z.string().min(1).default("/browser-attach/ws"),
214
236
  PROXY_USE: z.preprocess(emptyStringToUndefined, z.enum(["http", "socks5"]).optional()),
215
237
  HTTP_PROXY: optionalNonEmptyString,
216
238
  SOCKS5_PROXY: optionalNonEmptyString,
@@ -239,6 +261,16 @@ function loadConfig() {
239
261
  !parsed.TELEGRAM_BOT_TOKEN?.trim()) {
240
262
  throw new Error("TELEGRAM_BOT_TOKEN is required for gateway and both distributed modes.");
241
263
  }
264
+ const gatewayServerEnabled = parsed.DISTRIBUTED_MODE === "gateway" ||
265
+ parsed.DISTRIBUTED_MODE === "both";
266
+ const gatewayClientConfigured = parsed.DISTRIBUTED_MODE === "client" &&
267
+ Boolean(parsed.GATEWAY_PUBLIC_URL || parsed.GATEWAY_WS_URL);
268
+ if ((gatewayServerEnabled || gatewayClientConfigured) &&
269
+ !parsed.GATEWAY_AUTH_TOKEN?.trim()) {
270
+ throw new Error(gatewayServerEnabled
271
+ ? "GATEWAY_AUTH_TOKEN is required for gateway and both distributed modes."
272
+ : "GATEWAY_AUTH_TOKEN is required when a client connects to a gateway.");
273
+ }
242
274
  if (parsed.ADMIN_TOKEN?.trim() && !parsed.TELEGRAM_BOT_TOKEN?.trim()) {
243
275
  throw new Error("ADMIN_TOKEN requires TELEGRAM_BOT_TOKEN because admin mode runs through the gateway bot.");
244
276
  }
@@ -250,6 +282,69 @@ function loadConfig() {
250
282
  throw new Error("TELEGRAM_WEBHOOK_ENABLED=true requires TELEGRAM_WEBHOOK_SECRET.");
251
283
  }
252
284
  }
285
+ const oauthConfigured = Boolean(parsed.TELLYMCP_PUBLIC_URL ||
286
+ parsed.TELLYMCP_OAUTH_ISSUER ||
287
+ parsed.TELLYMCP_OAUTH_AUDIENCE ||
288
+ parsed.TELLYMCP_MAGIC_TOKEN ||
289
+ parsed.TELLYMCP_MAGIC_TOKEN_HASH ||
290
+ parsed.TELLYMCP_OAUTH_CLIENT_ID ||
291
+ parsed.TELLYMCP_OAUTH_CLIENT_SECRET ||
292
+ parsed.TELLYMCP_ALLOWED_REDIRECT_URIS ||
293
+ parsed.TELLYMCP_OAUTH_PRIVATE_KEY_PEM);
294
+ if (oauthConfigured && !parsed.TELLYMCP_PUBLIC_URL) {
295
+ throw new Error("TELLYMCP_PUBLIC_URL is required when OAuth connector settings are configured.");
296
+ }
297
+ if (oauthConfigured &&
298
+ !parsed.TELLYMCP_MAGIC_TOKEN &&
299
+ !parsed.TELLYMCP_MAGIC_TOKEN_HASH) {
300
+ throw new Error("TELLYMCP_MAGIC_TOKEN or TELLYMCP_MAGIC_TOKEN_HASH is required when OAuth is configured.");
301
+ }
302
+ if (parsed.TELLYMCP_MAGIC_TOKEN && parsed.TELLYMCP_MAGIC_TOKEN_HASH) {
303
+ throw new Error("Configure only one of TELLYMCP_MAGIC_TOKEN and TELLYMCP_MAGIC_TOKEN_HASH.");
304
+ }
305
+ if (parsed.TELLYMCP_MAGIC_TOKEN_HASH &&
306
+ !/^sha256:[a-f0-9]{64}$/iu.test(parsed.TELLYMCP_MAGIC_TOKEN_HASH)) {
307
+ throw new Error("TELLYMCP_MAGIC_TOKEN_HASH must use the format sha256:<64 hex characters>.");
308
+ }
309
+ if (parsed.TELLYMCP_OAUTH_CLIENT_SECRET && !parsed.TELLYMCP_OAUTH_CLIENT_ID) {
310
+ throw new Error("TELLYMCP_OAUTH_CLIENT_SECRET requires TELLYMCP_OAUTH_CLIENT_ID.");
311
+ }
312
+ for (const [name, value] of [
313
+ ["TELLYMCP_PUBLIC_URL", parsed.TELLYMCP_PUBLIC_URL],
314
+ ["TELLYMCP_OAUTH_ISSUER", parsed.TELLYMCP_OAUTH_ISSUER],
315
+ ["TELLYMCP_OAUTH_AUDIENCE", parsed.TELLYMCP_OAUTH_AUDIENCE],
316
+ ]) {
317
+ if (!value) {
318
+ continue;
319
+ }
320
+ const url = new URL(value);
321
+ if (url.search || url.hash) {
322
+ throw new Error(`${name} must not contain a query string or fragment.`);
323
+ }
324
+ }
325
+ const oauthScopes = parsed.TELLYMCP_OAUTH_SCOPES
326
+ .split(/[\s,]+/u)
327
+ .map((scope) => scope.trim())
328
+ .filter(Boolean);
329
+ if (oauthConfigured && oauthScopes.length === 0) {
330
+ throw new Error("TELLYMCP_OAUTH_SCOPES must contain at least one scope.");
331
+ }
332
+ const allowedRedirectUris = parsed.TELLYMCP_ALLOWED_REDIRECT_URIS
333
+ ? parsed.TELLYMCP_ALLOWED_REDIRECT_URIS.split(",")
334
+ .map((redirectUri) => redirectUri.trim())
335
+ .filter(Boolean)
336
+ : [];
337
+ for (const redirectUri of allowedRedirectUris) {
338
+ try {
339
+ const parsedRedirectUri = new URL(redirectUri);
340
+ if (parsedRedirectUri.hash) {
341
+ throw new Error("fragment");
342
+ }
343
+ }
344
+ catch {
345
+ throw new Error(`TELLYMCP_ALLOWED_REDIRECT_URIS contains an invalid URL: ${redirectUri}`);
346
+ }
347
+ }
253
348
  const telegramProxy = parsed.PROXY_USE === "http"
254
349
  ? parsed.HTTP_PROXY
255
350
  ? {
@@ -321,6 +416,36 @@ function loadConfig() {
321
416
  enableDebugRoutes: parsed.MCP_HTTP_ENABLE_DEBUG_ROUTES,
322
417
  enablePruneRoute: parsed.MCP_HTTP_ENABLE_PRUNE_ROUTE,
323
418
  },
419
+ ...(oauthConfigured && parsed.TELLYMCP_PUBLIC_URL
420
+ ? {
421
+ oauth: {
422
+ publicUrl: parsed.TELLYMCP_PUBLIC_URL.replace(/\/+$/u, ""),
423
+ issuer: (parsed.TELLYMCP_OAUTH_ISSUER ?? parsed.TELLYMCP_PUBLIC_URL).replace(/\/+$/u, ""),
424
+ audience: (parsed.TELLYMCP_OAUTH_AUDIENCE ?? parsed.TELLYMCP_PUBLIC_URL).replace(/\/+$/u, ""),
425
+ ...(parsed.TELLYMCP_MAGIC_TOKEN
426
+ ? { magicToken: parsed.TELLYMCP_MAGIC_TOKEN }
427
+ : {}),
428
+ ...(parsed.TELLYMCP_MAGIC_TOKEN_HASH
429
+ ? { magicTokenHash: parsed.TELLYMCP_MAGIC_TOKEN_HASH }
430
+ : {}),
431
+ ...(parsed.TELLYMCP_OAUTH_CLIENT_ID
432
+ ? { clientId: parsed.TELLYMCP_OAUTH_CLIENT_ID }
433
+ : {}),
434
+ ...(parsed.TELLYMCP_OAUTH_CLIENT_SECRET
435
+ ? { clientSecret: parsed.TELLYMCP_OAUTH_CLIENT_SECRET }
436
+ : {}),
437
+ allowedRedirectUris,
438
+ ...(parsed.TELLYMCP_OAUTH_PRIVATE_KEY_PEM
439
+ ? {
440
+ privateKeyPem: parsed.TELLYMCP_OAUTH_PRIVATE_KEY_PEM.replace(/\\n/gu, "\n"),
441
+ }
442
+ : {}),
443
+ authCodeTtlSeconds: parsed.TELLYMCP_AUTH_CODE_TTL_SECONDS,
444
+ scopes: oauthScopes,
445
+ keyId: parsed.TELLYMCP_OAUTH_KEY_ID,
446
+ },
447
+ }
448
+ : {}),
324
449
  distributed: {
325
450
  mode: parsed.DISTRIBUTED_MODE,
326
451
  ...(parsed.GATEWAY_PUBLIC_URL
@@ -379,7 +504,6 @@ function loadConfig() {
379
504
  sessionTtlSeconds: parsed.WEBAPP_SESSION_TTL_SECONDS,
380
505
  launchMode: parsed.WEBAPP_LAUNCH_MODE,
381
506
  visibleScreens: parsed.WEBAPP_VISIBLE_SCREENS,
382
- pollIntervalMs: parsed.WEBAPP_POLL_INTERVAL_MS,
383
507
  actionCooldownMs: parsed.WEBAPP_ACTION_COOLDOWN_MS,
384
508
  },
385
509
  exchange: {
@@ -417,6 +541,12 @@ function loadConfig() {
417
541
  : {}),
418
542
  ...(parsed.BROWSER_CHANNEL ? { channel: parsed.BROWSER_CHANNEL } : {}),
419
543
  slowMoMs: parsed.BROWSER_SLOW_MO_MS,
544
+ attach: {
545
+ enabled: parsed.BROWSER_ATTACH_ENABLED,
546
+ host: parsed.BROWSER_ATTACH_WS_HOST,
547
+ port: parsed.BROWSER_ATTACH_WS_PORT,
548
+ path: parsed.BROWSER_ATTACH_WS_PATH,
549
+ },
420
550
  },
421
551
  project: {
422
552
  ...(parsed.PROJECT_NAME ? { name: parsed.PROJECT_NAME } : {}),
@@ -3,15 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveMcpHttpAuthorization = resolveMcpHttpAuthorization;
7
+ exports.isMcpHttpRequestAuthorized = isMcpHttpRequestAuthorized;
6
8
  exports.createMcpHttpHandler = createMcpHttpHandler;
7
9
  const node_crypto_1 = require("node:crypto");
8
10
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
9
11
  const ws_1 = __importDefault(require("ws"));
12
+ const oauthFacade_1 = require("./oauthFacade");
10
13
  const auth_1 = require("./webapp/auth");
11
14
  const relay_1 = require("./webapp/relay");
12
15
  const assets_1 = require("./webapp/assets");
13
16
  const client_1 = require("../shared/integrations/terminal/client");
14
17
  const terminal_1 = require("./webapp/terminal");
18
+ const bodyLimits_1 = require("../shared/lib/bodyLimits");
15
19
  function formatTerminalHttpError(error, fallback) {
16
20
  if ((0, terminal_1.isTerminalUnavailableError)(error)) {
17
21
  return "terminal runtime is unavailable";
@@ -32,6 +36,13 @@ function isInitializeRequest(body) {
32
36
  const method = Reflect.get(body, "method");
33
37
  return method === "initialize";
34
38
  }
39
+ function readMcpRpcMethod(body) {
40
+ if (!body || typeof body !== "object") {
41
+ return undefined;
42
+ }
43
+ const method = Reflect.get(body, "method");
44
+ return typeof method === "string" ? method : undefined;
45
+ }
35
46
  function readHeader(req, headerName) {
36
47
  const value = req.headers[headerName];
37
48
  if (Array.isArray(value)) {
@@ -44,24 +55,21 @@ async function readJsonBody(req) {
44
55
  if (knownParams &&
45
56
  typeof knownParams === "object" &&
46
57
  Object.keys(knownParams).length > 0) {
58
+ (0, bodyLimits_1.assertSerializedBodySize)(knownParams);
47
59
  return knownParams;
48
60
  }
49
61
  const knownBody = req.body;
50
62
  if (knownBody !== undefined) {
63
+ (0, bodyLimits_1.assertSerializedBodySize)(knownBody);
51
64
  return knownBody;
52
65
  }
53
- const chunks = [];
54
- for await (const chunk of req) {
55
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
56
- }
57
- if (chunks.length === 0) {
58
- return undefined;
59
- }
60
- const raw = Buffer.concat(chunks).toString("utf8").trim();
61
- if (!raw) {
62
- return undefined;
66
+ return (0, bodyLimits_1.readLimitedJsonBody)(req);
67
+ }
68
+ function ignoreInvalidJsonBody(error) {
69
+ if ((0, bodyLimits_1.isBodySizeLimitError)(error)) {
70
+ throw error;
63
71
  }
64
- return JSON.parse(raw);
72
+ return undefined;
65
73
  }
66
74
  function writeJson(res, statusCode, payload) {
67
75
  const body = JSON.stringify(payload);
@@ -103,6 +111,19 @@ function readBearerToken(req) {
103
111
  }
104
112
  return header.slice("Bearer ".length).trim() || null;
105
113
  }
114
+ function resolveMcpHttpAuthorization(req, internalBearerToken, oauthFacade) {
115
+ if (!internalBearerToken && !oauthFacade) {
116
+ return "public";
117
+ }
118
+ if (internalBearerToken && isAuthorized(req, internalBearerToken)) {
119
+ return "internal_bearer";
120
+ }
121
+ const token = readBearerToken(req);
122
+ return token && oauthFacade?.verifyAccessToken(token) ? "oauth" : null;
123
+ }
124
+ function isMcpHttpRequestAuthorized(req, internalBearerToken, oauthFacade) {
125
+ return resolveMcpHttpAuthorization(req, internalBearerToken, oauthFacade) !== null;
126
+ }
106
127
  function isDuplicateSseStreamError(error) {
107
128
  const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
108
129
  return message.includes("Only one SSE stream is allowed per session");
@@ -143,7 +164,10 @@ function isRelayTerminalUnavailableMessage(error) {
143
164
  function createMcpHttpHandler(runtime, input) {
144
165
  const transports = new Map();
145
166
  const webAppSessions = new auth_1.WebAppSessionRegistry();
146
- const liveWsServer = new WebSocketServer({ noServer: true });
167
+ const liveWsServer = new WebSocketServer({
168
+ noServer: true,
169
+ maxPayload: bodyLimits_1.MAX_BODY_SIZE_BYTES,
170
+ });
147
171
  const webAppBasePath = runtime.config.webapp.basePath.replace(/\/+$/u, "") || "/webapp";
148
172
  const rootPrefix = normalizeBasePath(process.env.ROOT_PREFIX || "/api");
149
173
  const publicWebAppBasePath = rootPrefix === "/"
@@ -153,6 +177,13 @@ function createMcpHttpHandler(runtime, input) {
153
177
  const webAppLivePrefix = `${webAppBasePath}/live/`;
154
178
  const telegramWebhookPath = runtime.config.telegram.webhook.path.replace(/\/+$/u, "") ||
155
179
  "/telegram/webhook";
180
+ const mcpHttpPath = runtime.config.mcp.httpPath.replace(/\/+$/u, "") || "/mcp";
181
+ const oauthFacade = runtime.config.oauth
182
+ ? (0, oauthFacade_1.createOAuthFacade)(runtime.config.oauth, mcpHttpPath, runtime.logger)
183
+ : null;
184
+ if (oauthFacade?.ephemeralSigningKey) {
185
+ runtime.logger.warn("OAuth connector is using an ephemeral signing key; access tokens will stop working after restart");
186
+ }
156
187
  const closeSessionEntry = async (entry) => {
157
188
  await entry.close();
158
189
  };
@@ -220,6 +251,11 @@ function createMcpHttpHandler(runtime, input) {
220
251
  socket.on("close", () => {
221
252
  void cleanup();
222
253
  });
254
+ socket.on("error", (error) => {
255
+ runtime.logger.warn("Telegram WebApp live WS socket error", {
256
+ error: error instanceof Error ? error.message : String(error),
257
+ });
258
+ });
223
259
  socket.on("message", (raw) => {
224
260
  void (async () => {
225
261
  const parsed = raw && typeof raw === "string"
@@ -288,6 +324,15 @@ function createMcpHttpHandler(runtime, input) {
288
324
  }
289
325
  if (resolved.session?.terminalTarget && (0, client_1.isStreamableTerminalTarget)(resolved.session.terminalTarget)) {
290
326
  (0, client_1.resizeForegroundTerminal)(resolved.session.terminalTarget, Math.max(20, Math.min(400, Math.round(cols))), Math.max(5, Math.min(200, Math.round(rows))));
327
+ return;
328
+ }
329
+ if (resolved.relayTarget) {
330
+ await input.getGatewaySocketService?.()?.requestLiveRelay?.({
331
+ clientUuid: resolved.relayTarget.clientUuid,
332
+ localSessionId: resolved.relayTarget.localSessionId,
333
+ requestType: "resize",
334
+ payload: { cols, rows },
335
+ });
291
336
  }
292
337
  }
293
338
  })().catch((error) => {
@@ -356,11 +401,39 @@ function createMcpHttpHandler(runtime, input) {
356
401
  socket.close(1011, "Live stream bootstrap failed");
357
402
  });
358
403
  });
359
- const handleRequest = async (req, res, pathname) => {
404
+ const handleRequestInternal = async (req, res, pathname) => {
360
405
  const method = req.method ?? "GET";
361
406
  const normalizedPathname = normalizePrefixedPathname(pathname);
362
407
  const requestUrl = new URL(req.url ?? normalizedPathname, `http://gateway.local`);
363
408
  requestUrl.pathname = normalizedPathname;
409
+ if (runtime.temporaryFileLinkStore &&
410
+ (await runtime.temporaryFileLinkStore.handleRequest(req, res, requestUrl.pathname))) {
411
+ return;
412
+ }
413
+ if (oauthFacade) {
414
+ const authorizationHeader = readHeader(req, "authorization");
415
+ const connectorRequestContext = {
416
+ method,
417
+ rawPath: pathname,
418
+ path: requestUrl.pathname,
419
+ queryParameters: [...requestUrl.searchParams.keys()],
420
+ userAgent: readHeader(req, "user-agent"),
421
+ accept: readHeader(req, "accept"),
422
+ contentType: readHeader(req, "content-type"),
423
+ authorizationHeaderPresent: Boolean(authorizationHeader),
424
+ authorizationScheme: authorizationHeader?.split(/\s+/u, 1)[0],
425
+ mcpSessionId: readHeader(req, "mcp-session-id"),
426
+ remoteAddress: req.socket.remoteAddress,
427
+ };
428
+ runtime.logger.info("OAuth connector HTTP request received", connectorRequestContext);
429
+ res.once("finish", () => {
430
+ runtime.logger.info("OAuth connector HTTP response completed", {
431
+ ...connectorRequestContext,
432
+ statusCode: res.statusCode,
433
+ responseContentType: String(res.getHeader("content-type") ?? ""),
434
+ });
435
+ });
436
+ }
364
437
  if (requestUrl.pathname === "/healthz") {
365
438
  writeJson(res, 200, {
366
439
  ok: true,
@@ -369,6 +442,10 @@ function createMcpHttpHandler(runtime, input) {
369
442
  });
370
443
  return;
371
444
  }
445
+ if (oauthFacade &&
446
+ (await oauthFacade.handleRequest(req, res, requestUrl.pathname))) {
447
+ return;
448
+ }
372
449
  if (runtime.config.telegram.webhook.enabled &&
373
450
  requestUrl.pathname === telegramWebhookPath) {
374
451
  if (method !== "POST") {
@@ -457,7 +534,7 @@ function createMcpHttpHandler(runtime, input) {
457
534
  method,
458
535
  path: requestUrl.pathname,
459
536
  });
460
- const body = await readJsonBody(req).catch(() => undefined);
537
+ const body = await readJsonBody(req).catch(ignoreInvalidJsonBody);
461
538
  let sessionId = body &&
462
539
  typeof body === "object" &&
463
540
  typeof Reflect.get(body, "sessionId") === "string"
@@ -529,7 +606,6 @@ function createMcpHttpHandler(runtime, input) {
529
606
  session_id: relayBootstrap.session_id,
530
607
  session_label: relayBootstrap.session_label,
531
608
  terminal_target: relayBootstrap.terminal_target,
532
- poll_interval_ms: relayBootstrap.poll_interval_ms,
533
609
  expires_at: new Date(record.expiresAtMs).toISOString(),
534
610
  });
535
611
  if (trustedTelegramUserId !== null) {
@@ -569,17 +645,6 @@ function createMcpHttpHandler(runtime, input) {
569
645
  }
570
646
  try {
571
647
  const validated = (0, auth_1.validateTelegramWebAppInitData)(initDataRaw, initDataUnsafe, requireTelegramBotToken(runtime, "validate Telegram WebApp bootstrap"), runtime.config.webapp.initDataTtlSeconds);
572
- runtime.logger.info("Telegram WebApp initData validation debug", {
573
- sessionId: sessionId || null,
574
- telegramUserId: validated.user.id,
575
- providedHash: validated.validationDebug.providedHash,
576
- officialRawMatches: validated.validationDebug.officialRaw.matches,
577
- officialRawCheckString: validated.validationDebug.officialRaw.checkString,
578
- officialRawComputedHash: validated.validationDebug.officialRaw.computedHash,
579
- userFieldsMatches: validated.validationDebug.userFields?.matches ?? null,
580
- userFieldsCheckString: validated.validationDebug.userFields?.checkString ?? null,
581
- userFieldsComputedHash: validated.validationDebug.userFields?.computedHash ?? null,
582
- });
583
648
  if (!sessionId) {
584
649
  sessionId =
585
650
  (await runtime.bindingStore.getActiveSessionIdForTelegramUser(validated.user.id)) ?? "";
@@ -615,7 +680,6 @@ function createMcpHttpHandler(runtime, input) {
615
680
  session_id: sessionId,
616
681
  session_label: session?.label ?? null,
617
682
  terminal_target: Boolean(session?.terminalTarget),
618
- poll_interval_ms: runtime.config.webapp.pollIntervalMs,
619
683
  expires_at: new Date(record.expiresAtMs).toISOString(),
620
684
  });
621
685
  if (launchRecord?.telegramChatId !== undefined &&
@@ -640,7 +704,6 @@ function createMcpHttpHandler(runtime, input) {
640
704
  runtime.logger.warn("Telegram WebApp bootstrap rejected", {
641
705
  sessionId,
642
706
  initDataLength: initDataRaw.length,
643
- initDataPreview: initDataRaw.slice(0, 160),
644
707
  hasUnsafeUser: Boolean(initDataUnsafe?.user) &&
645
708
  typeof initDataUnsafe?.user?.id === "number",
646
709
  error: error instanceof Error
@@ -651,74 +714,6 @@ function createMcpHttpHandler(runtime, input) {
651
714
  }
652
715
  return;
653
716
  }
654
- if (requestUrl.pathname === `${webAppBasePath}/api/view`) {
655
- if (method !== "GET") {
656
- writeText(res, 405, "Method not allowed");
657
- return;
658
- }
659
- const token = readBearerToken(req);
660
- const webAppSession = token ? webAppSessions.get(token) : null;
661
- if (!webAppSession) {
662
- writeText(res, 401, "Unauthorized");
663
- return;
664
- }
665
- const relayTarget = (0, relay_1.parseLiveRelaySessionId)(webAppSession.sessionId);
666
- if (relayTarget) {
667
- try {
668
- const result = await runtime.gatewayHttpService.requestLiveRelayView({
669
- clientUuid: relayTarget.clientUuid,
670
- localSessionId: relayTarget.localSessionId,
671
- });
672
- writeJson(res, 200, result);
673
- }
674
- catch (error) {
675
- runtime.logger.error("Telegram WebApp relay visible buffer capture failed", {
676
- sessionId: webAppSession.sessionId,
677
- clientUuid: relayTarget.clientUuid,
678
- localSessionId: relayTarget.localSessionId,
679
- error: error instanceof Error
680
- ? (error.stack ?? error.message)
681
- : String(error),
682
- });
683
- writeText(res, isRelayTerminalUnavailableMessage(error) ? 503 : 500, error instanceof Error ? error.message : "Failed to capture relay terminal buffer");
684
- }
685
- return;
686
- }
687
- const binding = await runtime.bindingStore.getBinding(webAppSession.sessionId);
688
- if (!binding ||
689
- binding.telegramUserId !== webAppSession.telegramUserId) {
690
- writeText(res, 403, "Session binding is no longer valid");
691
- return;
692
- }
693
- const session = await runtime.sessionStore.getSession(webAppSession.sessionId);
694
- if (!session?.terminalTarget) {
695
- writeText(res, 409, "terminal target is not configured for this session");
696
- return;
697
- }
698
- try {
699
- const terminalSize = await (0, terminal_1.getTerminalWindowSize)(runtime.config.terminal, session.terminalTarget);
700
- const content = await (0, terminal_1.captureVisibleTerminal)(runtime.config.terminal, session.terminalTarget, runtime.config.terminal.captureLines, runtime.config.webapp.visibleScreens);
701
- const ansi = await (0, terminal_1.captureVisibleTerminalAnsi)(runtime.config.terminal, session.terminalTarget, runtime.config.terminal.captureLines, runtime.config.webapp.visibleScreens);
702
- writeJson(res, 200, {
703
- session_id: session.sessionId,
704
- session_label: session.label ?? null,
705
- captured_at: new Date().toISOString(),
706
- content,
707
- ansi,
708
- ...(terminalSize ? terminalSize : {}),
709
- });
710
- }
711
- catch (error) {
712
- runtime.logger.error("Telegram WebApp visible buffer capture failed", {
713
- sessionId: webAppSession.sessionId,
714
- error: error instanceof Error
715
- ? (error.stack ?? error.message)
716
- : String(error),
717
- });
718
- writeText(res, (0, terminal_1.isTerminalUnavailableError)(error) ? 503 : 500, formatTerminalHttpError(error, "Failed to capture visible terminal buffer"));
719
- }
720
- return;
721
- }
722
717
  if (requestUrl.pathname === `${webAppBasePath}/api/action`) {
723
718
  if (method !== "POST") {
724
719
  writeText(res, 405, "Method not allowed");
@@ -730,7 +725,7 @@ function createMcpHttpHandler(runtime, input) {
730
725
  writeText(res, 401, "Unauthorized");
731
726
  return;
732
727
  }
733
- const body = await readJsonBody(req).catch(() => undefined);
728
+ const body = await readJsonBody(req).catch(ignoreInvalidJsonBody);
734
729
  const action = body &&
735
730
  typeof body === "object" &&
736
731
  typeof Reflect.get(body, "action") === "string"
@@ -921,16 +916,22 @@ function createMcpHttpHandler(runtime, input) {
921
916
  });
922
917
  return;
923
918
  }
924
- if (requestUrl.pathname !== runtime.config.mcp.httpPath) {
919
+ if (requestUrl.pathname !== mcpHttpPath &&
920
+ requestUrl.pathname !== `${mcpHttpPath}/`) {
925
921
  writeText(res, 404, "Not found");
926
922
  return;
927
923
  }
928
- if (!isAuthorized(req, runtime.config.mcp.bearerToken)) {
924
+ const authorizationMode = resolveMcpHttpAuthorization(req, runtime.config.mcp.bearerToken, oauthFacade);
925
+ if (!authorizationMode) {
926
+ const authorizationHeader = readHeader(req, "authorization");
929
927
  runtime.logger.warn("Unauthorized MCP HTTP request rejected", {
930
928
  method,
931
929
  path: requestUrl.pathname,
932
930
  remoteAddress: req.socket.remoteAddress,
931
+ authorizationHeaderPresent: Boolean(authorizationHeader),
932
+ authorizationScheme: authorizationHeader?.split(/\s+/u, 1)[0],
933
933
  });
934
+ oauthFacade?.writeMcpChallenge(res);
934
935
  writeText(res, 401, "Unauthorized");
935
936
  return;
936
937
  }
@@ -938,12 +939,33 @@ function createMcpHttpHandler(runtime, input) {
938
939
  const parsedBody = method === "POST" || method === "DELETE"
939
940
  ? await readJsonBody(req)
940
941
  : undefined;
942
+ const rpcMethod = readMcpRpcMethod(parsedBody);
941
943
  runtime.logger.debug("MCP HTTP request received", {
942
944
  method,
943
945
  path: requestUrl.pathname,
944
946
  sessionId,
947
+ authorizationMode,
948
+ rpcMethod,
945
949
  hasBody: parsedBody !== undefined,
946
950
  });
951
+ if (rpcMethod === "initialize" || rpcMethod === "tools/list") {
952
+ runtime.logger.info("Chat connector MCP handshake request received", {
953
+ rpcMethod,
954
+ authorizationMode,
955
+ sessionId,
956
+ userAgent: readHeader(req, "user-agent"),
957
+ });
958
+ res.once("finish", () => {
959
+ runtime.logger.info("Chat connector MCP handshake response completed", {
960
+ rpcMethod,
961
+ authorizationMode,
962
+ statusCode: res.statusCode,
963
+ sessionId: readHeader(req, "mcp-session-id") ??
964
+ (String(res.getHeader("mcp-session-id") ?? "") || undefined),
965
+ contentType: String(res.getHeader("content-type") ?? ""),
966
+ });
967
+ });
968
+ }
947
969
  try {
948
970
  if (method === "POST") {
949
971
  if (sessionId) {
@@ -1090,6 +1112,24 @@ function createMcpHttpHandler(runtime, input) {
1090
1112
  }
1091
1113
  }
1092
1114
  };
1115
+ const handleRequest = async (req, res, pathname) => {
1116
+ try {
1117
+ await handleRequestInternal(req, res, pathname);
1118
+ }
1119
+ catch (error) {
1120
+ if (!(0, bodyLimits_1.isBodySizeLimitError)(error)) {
1121
+ throw error;
1122
+ }
1123
+ if (!res.headersSent) {
1124
+ writeJson(res, 413, {
1125
+ error: `Request body exceeds the ${bodyLimits_1.MAX_BODY_SIZE} MiB limit`,
1126
+ });
1127
+ }
1128
+ else if (!res.writableEnded) {
1129
+ res.end();
1130
+ }
1131
+ }
1132
+ };
1093
1133
  let shuttingDown = false;
1094
1134
  const handleUpgrade = async (req, socket, head, pathname) => {
1095
1135
  const normalizedPathname = normalizePrefixedPathname(pathname);