@deadragdoll/tellymcp 0.0.13 → 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 (64) 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 +14 -0
  5. package/README.md +14 -0
  6. package/TOOLS.md +182 -10
  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 +28 -1
  11. package/dist/services/features/telegram-mcp/browser.service.js +18 -0
  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 +18 -0
  17. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +9 -1
  18. package/dist/services/features/telegram-mcp/src/app/config/env.js +119 -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 +62 -19
  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/browserDetachTabTool.js +28 -0
  27. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +1 -1
  28. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +1 -1
  29. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +410 -27
  30. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +37 -3
  31. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +7 -6
  32. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +214 -37
  33. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +186 -0
  34. package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
  35. package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
  36. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
  37. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
  38. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
  39. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
  40. package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
  41. package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
  42. package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
  43. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
  44. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
  45. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
  46. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
  47. package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
  48. package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
  49. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
  50. package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
  51. package/docs/CHAT_CONNECTOR.md +134 -0
  52. package/docs/STANDALONE-ru.md +13 -0
  53. package/docs/STANDALONE.md +13 -0
  54. package/package.json +5 -3
  55. package/packages/chrome-attach-extension/dist/background.js +572 -163
  56. package/packages/chrome-attach-extension/dist/manifest.json +2 -1
  57. package/packages/chrome-attach-extension/dist/options.js +15 -2
  58. package/packages/chrome-attach-extension/dist/recorder-content.js +14 -1
  59. package/packages/chrome-attach-extension/dist/recorder-page.js +34 -18
  60. package/packages/firefox-attach-extension/dist/background.js +413 -33
  61. package/packages/firefox-attach-extension/dist/manifest.json +0 -12
  62. package/packages/firefox-attach-extension/dist/options.js +14 -1
  63. package/packages/firefox-attach-extension/dist/recorder-content.js +14 -1
  64. package/packages/firefox-attach-extension/dist/recorder-page.js +34 -18
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TEMPORARY_FILE_LINK_MAX_BYTES = exports.TemporaryFileLinkStore = void 0;
7
+ exports.resolvePublicFilesBaseUrl = resolvePublicFilesBaseUrl;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_fs_1 = require("node:fs");
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_stream_1 = require("node:stream");
13
+ const promises_2 = require("node:stream/promises");
14
+ const FILE_LINK_TTL_MS = 10 * 60 * 1000;
15
+ const FILE_LINK_MAX_BYTES = 32 * 1024 * 1024;
16
+ const FILE_LINK_MAX_DOWNLOADS = 3;
17
+ const FILE_LINK_CLEANUP_INTERVAL_MS = 60 * 1000;
18
+ function sanitizeFilename(value) {
19
+ const basename = node_path_1.default.basename(value?.trim() || "file.bin");
20
+ const withoutControlChars = Array.from(basename)
21
+ .map((character) => {
22
+ const code = character.charCodeAt(0);
23
+ return code < 32 || code === 127 ? "-" : character;
24
+ })
25
+ .join("");
26
+ const safe = withoutControlChars.replace(/[/\\<>:"|?*]/gu, "-").trim();
27
+ return safe || "file.bin";
28
+ }
29
+ function decodeFilenameHeader(value) {
30
+ if (!value) {
31
+ return undefined;
32
+ }
33
+ try {
34
+ return decodeURIComponent(value);
35
+ }
36
+ catch {
37
+ return value;
38
+ }
39
+ }
40
+ function encodeContentDispositionFilename(filename) {
41
+ return encodeURIComponent(filename).replace(/[!'()*]/gu, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
42
+ }
43
+ function resolvePublicFilesBaseUrl(gatewayPublicUrl) {
44
+ if (!gatewayPublicUrl?.trim()) {
45
+ throw new Error("URL file delivery requires GATEWAY_PUBLIC_URL.");
46
+ }
47
+ const url = new URL(gatewayPublicUrl);
48
+ url.search = "";
49
+ url.hash = "";
50
+ url.pathname = url.pathname.replace(/\/+$/u, "");
51
+ if (url.pathname.endsWith("/gateway")) {
52
+ url.pathname = url.pathname.slice(0, -"/gateway".length) || "/";
53
+ }
54
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/files`.replace(/\/{2,}/gu, "/");
55
+ return url;
56
+ }
57
+ function readSingleHeader(req, name) {
58
+ const value = req.headers[name];
59
+ return Array.isArray(value) ? value[0] : value;
60
+ }
61
+ class TemporaryFileLinkStore {
62
+ logger;
63
+ recordsByUploadToken = new Map();
64
+ recordsByDownloadToken = new Map();
65
+ recordsByCacheKey = new Map();
66
+ cleanupTimer = null;
67
+ baseUrl;
68
+ rootDir;
69
+ constructor(gatewayPublicUrl, logger, rootDir = node_path_1.default.resolve(".tellymcp", "tmp", "file-links")) {
70
+ this.logger = logger;
71
+ this.baseUrl = resolvePublicFilesBaseUrl(gatewayPublicUrl);
72
+ this.rootDir = rootDir;
73
+ }
74
+ async start() {
75
+ await (0, promises_1.mkdir)(this.rootDir, { recursive: true });
76
+ const entries = await (0, promises_1.readdir)(this.rootDir).catch(() => []);
77
+ await Promise.all(entries.map((entry) => (0, promises_1.rm)(node_path_1.default.join(this.rootDir, entry), { force: true, recursive: true })));
78
+ this.cleanupTimer = setInterval(() => {
79
+ void this.cleanupExpired();
80
+ }, FILE_LINK_CLEANUP_INTERVAL_MS);
81
+ this.cleanupTimer.unref();
82
+ }
83
+ async stop() {
84
+ if (this.cleanupTimer) {
85
+ clearInterval(this.cleanupTimer);
86
+ this.cleanupTimer = null;
87
+ }
88
+ await this.cleanupExpired(true);
89
+ }
90
+ createTicket(cacheKey) {
91
+ const id = (0, node_crypto_1.randomUUID)();
92
+ const uploadToken = (0, node_crypto_1.randomBytes)(32).toString("base64url");
93
+ const downloadToken = (0, node_crypto_1.randomBytes)(32).toString("base64url");
94
+ const expiresAtMs = Date.now() + FILE_LINK_TTL_MS;
95
+ const record = {
96
+ id,
97
+ uploadToken,
98
+ downloadToken,
99
+ cacheKey,
100
+ filePath: node_path_1.default.join(this.rootDir, `${id}.bin`),
101
+ partialPath: node_path_1.default.join(this.rootDir, `${id}.upload`),
102
+ expiresAtMs,
103
+ status: "pending",
104
+ downloadCount: 0,
105
+ };
106
+ this.recordsByUploadToken.set(uploadToken, record);
107
+ this.recordsByDownloadToken.set(downloadToken, record);
108
+ this.recordsByCacheKey.set(cacheKey, record);
109
+ const uploadUrl = new URL(this.baseUrl);
110
+ uploadUrl.pathname = `${this.baseUrl.pathname}/upload/${uploadToken}`;
111
+ return {
112
+ upload_url: uploadUrl.toString(),
113
+ download_token: downloadToken,
114
+ expires_at: new Date(expiresAtMs).toISOString(),
115
+ };
116
+ }
117
+ async discard(downloadToken) {
118
+ const record = this.recordsByDownloadToken.get(downloadToken);
119
+ if (record) {
120
+ await this.deleteRecord(record);
121
+ }
122
+ }
123
+ getReadyLink(downloadToken) {
124
+ const record = this.recordsByDownloadToken.get(downloadToken);
125
+ if (!record ||
126
+ record.expiresAtMs <= Date.now() ||
127
+ record.status !== "ready") {
128
+ throw new Error("Temporary file upload did not complete.");
129
+ }
130
+ if (!record.filename ||
131
+ !record.mimetype ||
132
+ record.sizeBytes === undefined) {
133
+ throw new Error("Temporary file metadata is incomplete.");
134
+ }
135
+ const downloadUrl = new URL(this.baseUrl);
136
+ downloadUrl.pathname = `${this.baseUrl.pathname}/download/${record.downloadToken}/${encodeURIComponent(record.filename)}`;
137
+ return {
138
+ url: downloadUrl.toString(),
139
+ filename: record.filename,
140
+ mimetype: record.mimetype,
141
+ size_bytes: record.sizeBytes,
142
+ expires_at: new Date(record.expiresAtMs).toISOString(),
143
+ };
144
+ }
145
+ async readCachedBase64(cacheKey, maxBytes) {
146
+ const record = this.recordsByCacheKey.get(cacheKey);
147
+ if (!record ||
148
+ record.status !== "ready" ||
149
+ record.expiresAtMs <= Date.now() ||
150
+ !record.filename ||
151
+ !record.mimetype ||
152
+ record.sizeBytes === undefined ||
153
+ record.sizeBytes > maxBytes) {
154
+ return null;
155
+ }
156
+ const content = await (0, promises_1.readFile)(record.filePath);
157
+ return {
158
+ data: content.toString("base64"),
159
+ filename: record.filename,
160
+ mimetype: record.mimetype,
161
+ size_bytes: record.sizeBytes,
162
+ };
163
+ }
164
+ matches(pathname) {
165
+ return pathname.startsWith("/files/");
166
+ }
167
+ async handleRequest(req, res, pathname) {
168
+ if (!this.matches(pathname)) {
169
+ return false;
170
+ }
171
+ const uploadMatch = /^\/files\/upload\/([^/]+)$/u.exec(pathname);
172
+ if (uploadMatch) {
173
+ await this.handleUpload(req, res, uploadMatch[1] ?? "");
174
+ return true;
175
+ }
176
+ const downloadMatch = /^\/files\/download\/([^/]+)(?:\/[^/]*)?$/u.exec(pathname);
177
+ if (downloadMatch) {
178
+ await this.handleDownload(req, res, downloadMatch[1] ?? "");
179
+ return true;
180
+ }
181
+ res.statusCode = 404;
182
+ res.end("Not found");
183
+ return true;
184
+ }
185
+ async handleUpload(req, res, uploadToken) {
186
+ if (req.method !== "PUT") {
187
+ res.statusCode = 405;
188
+ res.end("Method not allowed");
189
+ return;
190
+ }
191
+ const record = this.recordsByUploadToken.get(uploadToken);
192
+ if (!record ||
193
+ record.expiresAtMs <= Date.now() ||
194
+ record.status !== "pending") {
195
+ res.statusCode = 404;
196
+ res.end("Upload ticket not found");
197
+ return;
198
+ }
199
+ const declaredLength = Number(readSingleHeader(req, "content-length") ?? "0");
200
+ if (!Number.isFinite(declaredLength) ||
201
+ declaredLength < 0 ||
202
+ declaredLength > FILE_LINK_MAX_BYTES) {
203
+ res.statusCode = 413;
204
+ res.end("Invalid or oversized content-length");
205
+ return;
206
+ }
207
+ let receivedBytes = 0;
208
+ const limiter = new node_stream_1.Transform({
209
+ transform(chunk, _encoding, callback) {
210
+ receivedBytes += chunk.byteLength;
211
+ if (receivedBytes > FILE_LINK_MAX_BYTES) {
212
+ callback(new Error("Temporary file exceeds the 32 MiB limit."));
213
+ return;
214
+ }
215
+ callback(null, chunk);
216
+ },
217
+ });
218
+ try {
219
+ await (0, promises_2.pipeline)(req, limiter, (0, node_fs_1.createWriteStream)(record.partialPath, { flags: "wx" }));
220
+ if (receivedBytes !== declaredLength) {
221
+ throw new Error("Uploaded file size does not match content-length.");
222
+ }
223
+ await (0, promises_1.rename)(record.partialPath, record.filePath);
224
+ record.filename = sanitizeFilename(decodeFilenameHeader(readSingleHeader(req, "x-telly-filename")));
225
+ record.mimetype =
226
+ readSingleHeader(req, "content-type")?.split(";", 1)[0]?.trim() ||
227
+ "application/octet-stream";
228
+ record.sizeBytes = receivedBytes;
229
+ record.status = "ready";
230
+ this.recordsByUploadToken.delete(uploadToken);
231
+ res.statusCode = 204;
232
+ res.end();
233
+ }
234
+ catch (error) {
235
+ await (0, promises_1.rm)(record.partialPath, { force: true });
236
+ this.logger.warn("Temporary file upload failed", {
237
+ transferId: record.id,
238
+ error: error instanceof Error ? error.message : String(error),
239
+ });
240
+ res.statusCode = 400;
241
+ res.end("Upload failed");
242
+ }
243
+ }
244
+ async handleDownload(req, res, downloadToken) {
245
+ if (req.method !== "GET" && req.method !== "HEAD") {
246
+ res.statusCode = 405;
247
+ res.end("Method not allowed");
248
+ return;
249
+ }
250
+ const record = this.recordsByDownloadToken.get(downloadToken);
251
+ if (!record ||
252
+ record.status !== "ready" ||
253
+ record.expiresAtMs <= Date.now() ||
254
+ !record.filename ||
255
+ !record.mimetype ||
256
+ record.sizeBytes === undefined ||
257
+ record.downloadCount >= FILE_LINK_MAX_DOWNLOADS) {
258
+ res.statusCode = 404;
259
+ res.end("File link expired or unavailable");
260
+ return;
261
+ }
262
+ res.statusCode = 200;
263
+ res.setHeader("content-type", record.mimetype);
264
+ res.setHeader("content-length", record.sizeBytes);
265
+ res.setHeader("content-disposition", `attachment; filename*=UTF-8''${encodeContentDispositionFilename(record.filename)}`);
266
+ res.setHeader("cache-control", "private, no-store, max-age=0");
267
+ res.setHeader("x-content-type-options", "nosniff");
268
+ if (req.method === "HEAD") {
269
+ res.end();
270
+ return;
271
+ }
272
+ record.downloadCount += 1;
273
+ try {
274
+ await (0, promises_2.pipeline)((0, node_fs_1.createReadStream)(record.filePath), res);
275
+ }
276
+ catch (error) {
277
+ record.downloadCount -= 1;
278
+ this.logger.warn("Temporary file download failed", {
279
+ transferId: record.id,
280
+ error: error instanceof Error ? error.message : String(error),
281
+ });
282
+ if (!res.writableEnded) {
283
+ res.destroy(error instanceof Error ? error : undefined);
284
+ }
285
+ }
286
+ }
287
+ async cleanupExpired(force = false) {
288
+ const now = Date.now();
289
+ const records = [...this.recordsByDownloadToken.values()];
290
+ await Promise.all(records
291
+ .filter((record) => force || record.expiresAtMs <= now)
292
+ .map((record) => this.deleteRecord(record)));
293
+ }
294
+ async deleteRecord(record) {
295
+ this.recordsByUploadToken.delete(record.uploadToken);
296
+ this.recordsByDownloadToken.delete(record.downloadToken);
297
+ if (this.recordsByCacheKey.get(record.cacheKey) === record) {
298
+ this.recordsByCacheKey.delete(record.cacheKey);
299
+ }
300
+ await Promise.all([
301
+ (0, promises_1.rm)(record.filePath, { force: true }),
302
+ (0, promises_1.rm)(record.partialPath, { force: true }),
303
+ ]);
304
+ }
305
+ }
306
+ exports.TemporaryFileLinkStore = TemporaryFileLinkStore;
307
+ exports.TEMPORARY_FILE_LINK_MAX_BYTES = FILE_LINK_MAX_BYTES;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.assertWorkspaceFilePathAllowed = assertWorkspaceFilePathAllowed;
7
+ exports.resolveWorkspaceFileMimeType = resolveWorkspaceFileMimeType;
8
+ exports.decodeWorkspaceTextContent = decodeWorkspaceTextContent;
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_util_1 = require("node:util");
11
+ const mime_types_1 = require("mime-types");
12
+ const BLOCKED_DIRECTORY_NAMES = new Set([
13
+ ".aws",
14
+ ".azure",
15
+ ".docker",
16
+ ".git",
17
+ ".gnupg",
18
+ ".kube",
19
+ ".ssh",
20
+ ".tellymcp",
21
+ ]);
22
+ const BLOCKED_FILE_NAMES = new Set([
23
+ ".dockerconfigjson",
24
+ ".git-credentials",
25
+ ".netrc",
26
+ ".npmrc",
27
+ ".pypirc",
28
+ ".yarnrc",
29
+ ".yarnrc.yml",
30
+ "_netrc",
31
+ "credentials",
32
+ "credentials.json",
33
+ "id_dsa",
34
+ "id_ecdsa",
35
+ "id_ed25519",
36
+ "id_rsa",
37
+ "secrets.json",
38
+ "service-account-key.json",
39
+ "service-account.json",
40
+ ]);
41
+ const BLOCKED_FILE_EXTENSIONS = new Set([
42
+ ".jks",
43
+ ".kdbx",
44
+ ".key",
45
+ ".keystore",
46
+ ".p12",
47
+ ".pem",
48
+ ".pfx",
49
+ ]);
50
+ const SAFE_ENV_TEMPLATE_MARKERS = new Set([
51
+ "dist",
52
+ "example",
53
+ "sample",
54
+ "template",
55
+ ]);
56
+ const SOURCE_MIME_TYPES = new Map([
57
+ [".astro", "text/plain"],
58
+ [".cts", "text/typescript"],
59
+ [".gql", "application/graphql"],
60
+ [".graphql", "application/graphql"],
61
+ [".jsx", "text/javascript"],
62
+ [".mdx", "text/markdown"],
63
+ [".mts", "text/typescript"],
64
+ [".prisma", "text/plain"],
65
+ [".svelte", "text/plain"],
66
+ [".toml", "application/toml"],
67
+ [".ts", "text/typescript"],
68
+ [".tsx", "text/typescript"],
69
+ [".vue", "text/plain"],
70
+ ]);
71
+ const SOURCE_FILE_MIME_TYPES = new Map([
72
+ ["dockerfile", "text/plain"],
73
+ ["makefile", "text/plain"],
74
+ ]);
75
+ function isBlockedEnvFile(fileName) {
76
+ if (fileName === ".env") {
77
+ return true;
78
+ }
79
+ if (!fileName.startsWith(".env.")) {
80
+ return false;
81
+ }
82
+ const suffixSegments = fileName.slice(".env.".length).split(".");
83
+ return !suffixSegments.some((segment) => SAFE_ENV_TEMPLATE_MARKERS.has(segment));
84
+ }
85
+ function assertWorkspaceFilePathAllowed(filePath) {
86
+ const normalized = filePath.replaceAll("\\", "/").toLowerCase();
87
+ const segments = normalized.split("/").filter(Boolean);
88
+ const fileName = segments.at(-1) ?? "";
89
+ if (segments.some((segment) => BLOCKED_DIRECTORY_NAMES.has(segment)) ||
90
+ BLOCKED_FILE_NAMES.has(fileName) ||
91
+ BLOCKED_FILE_EXTENSIONS.has(node_path_1.default.posix.extname(fileName)) ||
92
+ isBlockedEnvFile(fileName)) {
93
+ throw new Error("Access to sensitive workspace files is blocked.");
94
+ }
95
+ }
96
+ function resolveWorkspaceFileMimeType(fileName) {
97
+ const normalized = fileName.toLowerCase();
98
+ return (SOURCE_FILE_MIME_TYPES.get(normalized) ||
99
+ SOURCE_MIME_TYPES.get(node_path_1.default.extname(normalized)) ||
100
+ (0, mime_types_1.lookup)(fileName) ||
101
+ "application/octet-stream");
102
+ }
103
+ function decodeWorkspaceTextContent(content) {
104
+ let text;
105
+ try {
106
+ text = new node_util_1.TextDecoder("utf-8", { fatal: true }).decode(content);
107
+ }
108
+ catch {
109
+ throw new Error("type='text' requires valid UTF-8 content; use type='url' or type='base64' for binary files.");
110
+ }
111
+ if (text.includes("\0")) {
112
+ throw new Error("type='text' does not accept binary content; use type='url' or type='base64'.");
113
+ }
114
+ return text;
115
+ }
@@ -8,6 +8,7 @@ const node_path_1 = __importDefault(require("node:path"));
8
8
  const schema_1 = require("../../../entities/request/model/schema");
9
9
  const redactSecrets_1 = require("../../../shared/lib/redact-secrets/redactSecrets");
10
10
  const client_1 = require("../../../shared/integrations/terminal/client");
11
+ const bodyLimits_1 = require("../../../shared/lib/bodyLimits");
11
12
  const relay_1 = require("../../../app/webapp/relay");
12
13
  const gatewayClientAccess_1 = require("../../distributed-client/model/gatewayClientAccess");
13
14
  function mergeSavedContext(input, session) {
@@ -300,7 +301,10 @@ class NotifyService {
300
301
  if (!binding) {
301
302
  throw new Error("Gateway relay session has no active Telegram route yet.");
302
303
  }
303
- const sendResult = await this.transport.sendDocumentBufferToChat?.(binding.telegramChatId, input.fileName, Buffer.from(input.contentBase64, "base64"), input.caption);
304
+ (0, bodyLimits_1.assertStringBodySize)(input.contentBase64);
305
+ const decodedContent = Buffer.from(input.contentBase64, "base64");
306
+ (0, bodyLimits_1.assertBodySize)(decodedContent.byteLength);
307
+ const sendResult = await this.transport.sendDocumentBufferToChat?.(binding.telegramChatId, input.fileName, decodedContent, input.caption);
304
308
  if (!sendResult) {
305
309
  throw new Error("Gateway transport document proxy is unavailable.");
306
310
  }
@@ -6,6 +6,7 @@ const promises_1 = require("node:fs/promises");
6
6
  const grammy_1 = require("grammy");
7
7
  const telegramXchangeRecords_1 = require("../../lib/telegramXchangeRecords");
8
8
  const collabSemantics_1 = require("./collabSemantics");
9
+ const bodyLimits_1 = require("../../lib/bodyLimits");
9
10
  const transportFormatting_1 = require("./transportFormatting");
10
11
  const transportConstructorWiring_1 = require("./transportConstructorWiring");
11
12
  const transportUtils_1 = require("./transportUtils");
@@ -383,6 +384,8 @@ class TelegramTransport {
383
384
  }
384
385
  async sendDocumentToChat(telegramChatId, filePath, caption) {
385
386
  this.ensureTelegramEnabledFor("send Telegram documents");
387
+ const fileStats = await (0, promises_1.stat)(filePath);
388
+ (0, bodyLimits_1.assertBodySize)(fileStats.size);
386
389
  const fileBuffer = await (0, promises_1.readFile)(filePath);
387
390
  const response = await this.bot.api.sendDocument(telegramChatId, new grammy_1.InputFile(fileBuffer, (0, node_path_1.basename)(filePath)), caption?.trim()
388
391
  ? {
@@ -11,6 +11,7 @@ const xchangeRecordHints_1 = require("../../lib/xchangeRecordHints");
11
11
  const sqliteRecordStore_1 = require("../xchange/sqliteRecordStore");
12
12
  const client_1 = require("../terminal/client");
13
13
  const transportUtils_1 = require("./transportUtils");
14
+ const bodyLimits_1 = require("../../lib/bodyLimits");
14
15
  class TransportFileHandoffActions {
15
16
  host;
16
17
  constructor(host) {
@@ -316,12 +317,12 @@ class TransportFileHandoffActions {
316
317
  ...(meta?.relativePath ? { relativePath: meta.relativePath } : {}),
317
318
  ...(meta?.storageRef ? { storageRef: meta.storageRef } : {}),
318
319
  });
319
- const fileContent = await (0, client_1.readWorkspaceFile)(this.host.config.terminal, this.host.objectStore.resolveWorkspaceDir(session), localFilePath);
320
+ const fileContent = await (0, client_1.readWorkspaceFile)(this.host.config.terminal, this.host.objectStore.resolveWorkspaceDir(session), localFilePath, bodyLimits_1.MAX_BASE64_SOURCE_SIZE_BYTES);
320
321
  const handoffSummary = input.description
321
322
  .split(/\r?\n/u)
322
323
  .map((line) => line.trim())
323
324
  .find(Boolean) ?? `Передача файла: ${fileName}`;
324
- return this.host.sendPartnerNote({
325
+ const note = {
325
326
  session_id: input.sessionId,
326
327
  ...(input.targetSessionId ? { target_session_id: input.targetSessionId } : {}),
327
328
  ...(input.projectUuid ? { project_uuid: input.projectUuid } : {}),
@@ -346,7 +347,9 @@ class TransportFileHandoffActions {
346
347
  content_base64: node_buffer_1.Buffer.from(fileContent).toString("base64"),
347
348
  },
348
349
  ],
349
- });
350
+ };
351
+ (0, bodyLimits_1.assertSerializedBodySize)(note);
352
+ return this.host.sendPartnerNote(note);
350
353
  }
351
354
  }
352
355
  exports.TransportFileHandoffActions = TransportFileHandoffActions;
@@ -9,6 +9,7 @@ exports.writeXchangeRelativeFile = writeXchangeRelativeFile;
9
9
  exports.listXchangeFiles = listXchangeFiles;
10
10
  exports.deleteXchangeFile = deleteXchangeFile;
11
11
  exports.readWorkspaceFile = readWorkspaceFile;
12
+ exports.resolveWorkspaceFileForRead = resolveWorkspaceFileForRead;
12
13
  exports.isTerminalUnavailableError = isTerminalUnavailableError;
13
14
  exports.isTerminalTargetInvalidError = isTerminalTargetInvalidError;
14
15
  exports.getConfiguredTerminalShell = getConfiguredTerminalShell;
@@ -31,6 +32,7 @@ exports.subscribeForegroundTerminal = subscribeForegroundTerminal;
31
32
  exports.stopAllForegroundTerminals = stopAllForegroundTerminals;
32
33
  const promises_1 = require("node:fs/promises");
33
34
  const node_path_1 = __importDefault(require("node:path"));
35
+ const bodyLimits_1 = require("../../lib/bodyLimits");
34
36
  const ptyRegistry_1 = require("../terminal/ptyRegistry");
35
37
  const ENTER_AFTER_PASTE_DELAY_MS = 75;
36
38
  function toPtyConfig(config) {
@@ -93,9 +95,7 @@ async function allocateAvailableFilePath(dir, fileName) {
93
95
  const extension = node_path_1.default.extname(safeFileName);
94
96
  const baseName = node_path_1.default.basename(safeFileName, extension);
95
97
  for (let attempt = 0; attempt < 1000; attempt += 1) {
96
- const candidateName = attempt === 0
97
- ? safeFileName
98
- : `${baseName}--${attempt}${extension}`;
98
+ const candidateName = attempt === 0 ? safeFileName : `${baseName}--${attempt}${extension}`;
99
99
  const candidatePath = node_path_1.default.join(dir, candidateName);
100
100
  try {
101
101
  await (0, promises_1.access)(candidatePath);
@@ -172,9 +172,32 @@ async function deleteXchangeFile(config, workspaceDir, exchangeDirName, filePath
172
172
  await (0, promises_1.rm)(resolvedFilePath, { force: true });
173
173
  return true;
174
174
  }
175
- async function readWorkspaceFile(config, workspaceDir, filePath) {
175
+ async function readWorkspaceFile(config, workspaceDir, filePath, maxBytes = bodyLimits_1.MAX_BODY_SIZE_BYTES) {
176
+ const resolved = await resolveWorkspaceFileForRead(config, workspaceDir, filePath, maxBytes);
177
+ return (0, promises_1.readFile)(resolved.filePath);
178
+ }
179
+ async function resolveWorkspaceFileForRead(config, workspaceDir, filePath, maxBytes = bodyLimits_1.MAX_BODY_SIZE_BYTES) {
180
+ void config;
176
181
  const resolvedFilePath = resolvePathInsideWorkspace(workspaceDir, filePath);
177
- return (0, promises_1.readFile)(resolvedFilePath);
182
+ const [realWorkspaceDir, realFilePath] = await Promise.all([
183
+ (0, promises_1.realpath)(node_path_1.default.resolve(workspaceDir)),
184
+ (0, promises_1.realpath)(resolvedFilePath),
185
+ ]);
186
+ const relative = node_path_1.default.relative(realWorkspaceDir, realFilePath);
187
+ if (relative.startsWith("..") ||
188
+ node_path_1.default.isAbsolute(relative) ||
189
+ relative.trim() === "") {
190
+ throw new Error("File path is outside the workspace directory.");
191
+ }
192
+ const fileStats = await (0, promises_1.stat)(realFilePath);
193
+ if (!fileStats.isFile()) {
194
+ throw new Error("File path does not point to a regular file.");
195
+ }
196
+ (0, bodyLimits_1.assertBodySize)(fileStats.size, maxBytes);
197
+ return {
198
+ filePath: realFilePath,
199
+ sizeBytes: fileStats.size,
200
+ };
178
201
  }
179
202
  function isTerminalUnavailableError(error) {
180
203
  const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
@@ -202,7 +225,7 @@ function ensureTerminalTargetForSession(config, input) {
202
225
  }
203
226
  async function resolveTerminalTargetFromHint(config, hint) {
204
227
  const target = hint.terminalTarget?.trim() || (0, ptyRegistry_1.buildPtyTarget)("default");
205
- return (0, ptyRegistry_1.hasPtyTarget)(target) ? target : hint.terminalTarget?.trim() ?? null;
228
+ return (0, ptyRegistry_1.hasPtyTarget)(target) ? target : (hint.terminalTarget?.trim() ?? null);
206
229
  }
207
230
  async function getTerminalWindowHeight(config, target) {
208
231
  return (0, ptyRegistry_1.getPtyWindowHeight)(target);