@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.
- package/.env.example.client +5 -1
- package/.env.example.gateway +16 -1
- package/CHANGELOG.md +30 -0
- package/README-ru.md +14 -0
- package/README.md +14 -0
- package/TOOLS.md +182 -10
- package/config/templates/env.both.template +15 -2
- package/config/templates/env.client.template +5 -2
- package/config/templates/env.gateway.template +15 -2
- package/dist/cli.js +28 -1
- package/dist/services/features/telegram-mcp/browser.service.js +18 -0
- package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
- package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +18 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +9 -1
- package/dist/services/features/telegram-mcp/src/app/config/env.js +119 -2
- package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
- package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +62 -19
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +410 -27
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +37 -3
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +7 -6
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +214 -37
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +186 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
- package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
- package/docs/CHAT_CONNECTOR.md +134 -0
- package/docs/STANDALONE-ru.md +13 -0
- package/docs/STANDALONE.md +13 -0
- package/package.json +5 -3
- package/packages/chrome-attach-extension/dist/background.js +572 -163
- package/packages/chrome-attach-extension/dist/manifest.json +2 -1
- package/packages/chrome-attach-extension/dist/options.js +15 -2
- package/packages/chrome-attach-extension/dist/recorder-content.js +14 -1
- package/packages/chrome-attach-extension/dist/recorder-page.js +34 -18
- package/packages/firefox-attach-extension/dist/background.js +413 -33
- package/packages/firefox-attach-extension/dist/manifest.json +0 -12
- package/packages/firefox-attach-extension/dist/options.js +14 -1
- package/packages/firefox-attach-extension/dist/recorder-content.js +14 -1
- package/packages/firefox-attach-extension/dist/recorder-page.js +34 -18
|
@@ -17,8 +17,25 @@ function sanitizeSegment(value) {
|
|
|
17
17
|
.replace(/-+$/u, "");
|
|
18
18
|
return normalized || "recording";
|
|
19
19
|
}
|
|
20
|
+
function padNumber(value, length = 2) {
|
|
21
|
+
return String(value).padStart(length, "0");
|
|
22
|
+
}
|
|
23
|
+
function formatLocalTimestamp(date) {
|
|
24
|
+
return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(date.getMilliseconds(), 3)}`;
|
|
25
|
+
}
|
|
20
26
|
function formatTimestampForDir(date) {
|
|
21
|
-
return date
|
|
27
|
+
return formatLocalTimestamp(date).replace(/[:.]/gu, "-");
|
|
28
|
+
}
|
|
29
|
+
function normalizeRecordedTimestamp(rawValue) {
|
|
30
|
+
const trimmed = rawValue?.trim();
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
return formatLocalTimestamp(new Date());
|
|
33
|
+
}
|
|
34
|
+
const parsed = new Date(trimmed);
|
|
35
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
36
|
+
return formatLocalTimestamp(new Date());
|
|
37
|
+
}
|
|
38
|
+
return formatLocalTimestamp(parsed);
|
|
22
39
|
}
|
|
23
40
|
function formatJsonLine(value) {
|
|
24
41
|
return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
|
|
@@ -85,7 +102,7 @@ class BrowserRecordingBundleWriter {
|
|
|
85
102
|
]) {
|
|
86
103
|
await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, input.session.cwd, this.config.exchange.dir, `${segment}/.keep`, Buffer.from("", "utf8"));
|
|
87
104
|
}
|
|
88
|
-
const startedAt = new Date()
|
|
105
|
+
const startedAt = formatLocalTimestamp(new Date());
|
|
89
106
|
const record = {
|
|
90
107
|
sessionId: input.sessionId,
|
|
91
108
|
backend: "firefox-attached",
|
|
@@ -150,7 +167,7 @@ class BrowserRecordingBundleWriter {
|
|
|
150
167
|
return state;
|
|
151
168
|
}
|
|
152
169
|
async appendEvent(state, event) {
|
|
153
|
-
const at = event.at
|
|
170
|
+
const at = normalizeRecordedTimestamp(event.at);
|
|
154
171
|
state.record.eventCount += 1;
|
|
155
172
|
state.record.lastEventAt = at;
|
|
156
173
|
let relPath;
|
|
@@ -255,6 +272,23 @@ class BrowserRecordingBundleWriter {
|
|
|
255
272
|
await this.writeSessionJson(state);
|
|
256
273
|
return state;
|
|
257
274
|
}
|
|
275
|
+
async finalizeExisting(input) {
|
|
276
|
+
const state = {
|
|
277
|
+
record: { ...input.record },
|
|
278
|
+
session: input.session,
|
|
279
|
+
currentPageId: undefined,
|
|
280
|
+
currentPageUrl: input.record.tabUrl,
|
|
281
|
+
pageCounter: 0,
|
|
282
|
+
snapshotCounter: 0,
|
|
283
|
+
requestCounter: 0,
|
|
284
|
+
requestArtifacts: new Map(),
|
|
285
|
+
};
|
|
286
|
+
await this.appendEvent(state, {
|
|
287
|
+
kind: "session_stopped",
|
|
288
|
+
status: "stopped",
|
|
289
|
+
});
|
|
290
|
+
return state.record;
|
|
291
|
+
}
|
|
258
292
|
async appendJsonLine(state, relativePath, value) {
|
|
259
293
|
await (0, client_1.writeXchangeRelativeFile)(this.config.terminal, state.session.cwd || "", this.config.exchange.dir, relativePath, formatJsonLine(value), { append: true });
|
|
260
294
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FirefoxAttachRegistry = void 0;
|
|
4
|
+
const localTimestamp_1 = require("../../../shared/lib/time/localTimestamp");
|
|
4
5
|
class FirefoxAttachRegistry {
|
|
5
6
|
instances = new Map();
|
|
6
7
|
setConnected(input) {
|
|
7
|
-
const now = new Date()
|
|
8
|
+
const now = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
|
|
8
9
|
const existing = this.instances.get(input.instanceId);
|
|
9
10
|
const next = {
|
|
10
11
|
instanceId: input.instanceId,
|
|
11
|
-
browser:
|
|
12
|
+
browser: input.browser,
|
|
12
13
|
extensionVersion: input.extensionVersion,
|
|
13
14
|
...(input.profileName ? { profileName: input.profileName } : {}),
|
|
14
15
|
connectedAt: existing?.connectedAt ?? now,
|
|
@@ -25,7 +26,7 @@ class FirefoxAttachRegistry {
|
|
|
25
26
|
if (!existing) {
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
existing.lastSeenAt = new Date()
|
|
29
|
+
existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
|
|
29
30
|
}
|
|
30
31
|
setTabs(instanceId, tabs) {
|
|
31
32
|
const existing = this.instances.get(instanceId);
|
|
@@ -35,7 +36,7 @@ class FirefoxAttachRegistry {
|
|
|
35
36
|
existing.tabs = tabs.map((tab) => ({ ...tab }));
|
|
36
37
|
existing.activeTab =
|
|
37
38
|
tabs.find((tab) => tab.active) ?? existing.activeTab ?? null;
|
|
38
|
-
existing.lastSeenAt = new Date()
|
|
39
|
+
existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
|
|
39
40
|
}
|
|
40
41
|
setActiveTab(instanceId, tab) {
|
|
41
42
|
const existing = this.instances.get(instanceId);
|
|
@@ -43,7 +44,7 @@ class FirefoxAttachRegistry {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
existing.activeTab = tab ? { ...tab } : null;
|
|
46
|
-
existing.lastSeenAt = new Date()
|
|
47
|
+
existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
|
|
47
48
|
}
|
|
48
49
|
updateTab(instanceId, tab) {
|
|
49
50
|
const existing = this.instances.get(instanceId);
|
|
@@ -62,7 +63,7 @@ class FirefoxAttachRegistry {
|
|
|
62
63
|
if (tab.active) {
|
|
63
64
|
existing.activeTab = { ...tab };
|
|
64
65
|
}
|
|
65
|
-
existing.lastSeenAt = new Date()
|
|
66
|
+
existing.lastSeenAt = (0, localTimestamp_1.formatLocalTimestamp)(new Date());
|
|
66
67
|
}
|
|
67
68
|
remove(instanceId) {
|
|
68
69
|
this.instances.delete(instanceId);
|
package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js
CHANGED
|
@@ -1,16 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
5
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
36
|
exports.FirefoxAttachServer = void 0;
|
|
7
37
|
const node_crypto_1 = require("node:crypto");
|
|
8
|
-
const
|
|
38
|
+
const wsModule = __importStar(require("ws"));
|
|
39
|
+
const localTimestamp_1 = require("../../../shared/lib/time/localTimestamp");
|
|
40
|
+
const bodyLimits_1 = require("../../../shared/lib/bodyLimits");
|
|
9
41
|
const browserRecordingBundle_1 = require("./browserRecordingBundle");
|
|
10
42
|
const firefoxAttachRegistry_1 = require("./firefoxAttachRegistry");
|
|
11
|
-
const
|
|
43
|
+
const types_1 = require("./types");
|
|
44
|
+
const wsLib = wsModule;
|
|
12
45
|
const WebSocketServer = wsLib.WebSocketServer;
|
|
13
|
-
const WS_OPEN = wsLib.OPEN;
|
|
46
|
+
const WS_OPEN = wsLib.WebSocket.OPEN;
|
|
47
|
+
const ATTACH_HEARTBEAT_TIMEOUT_MS = 45_000;
|
|
48
|
+
const ATTACH_HEARTBEAT_REAP_INTERVAL_MS = 15_000;
|
|
49
|
+
const ATTACH_RATE_WINDOW_MS = 1_000;
|
|
50
|
+
const ATTACH_RATE_MAX_MESSAGES = 1_000;
|
|
51
|
+
class FirefoxAttachProtocolError extends Error {
|
|
52
|
+
reason;
|
|
53
|
+
constructor(reason) {
|
|
54
|
+
super(reason);
|
|
55
|
+
this.reason = reason;
|
|
56
|
+
this.name = "FirefoxAttachProtocolError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const isAllowedAttachOrigin = (origin) => {
|
|
60
|
+
if (!origin) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return /^(?:moz|chrome)-extension:\/\/[a-z0-9-]+\/?$/iu.test(origin);
|
|
64
|
+
};
|
|
14
65
|
class FirefoxAttachServer {
|
|
15
66
|
config;
|
|
16
67
|
logger;
|
|
@@ -24,6 +75,7 @@ class FirefoxAttachServer {
|
|
|
24
75
|
pendingTabActions = new Map();
|
|
25
76
|
pendingRecordingControls = new Map();
|
|
26
77
|
activeRecordingsById = new Map();
|
|
78
|
+
heartbeatReaper = null;
|
|
27
79
|
constructor(config, logger, sessionStore, maintenanceStore) {
|
|
28
80
|
this.config = config;
|
|
29
81
|
this.logger = logger;
|
|
@@ -38,22 +90,48 @@ class FirefoxAttachServer {
|
|
|
38
90
|
if (this.wsServer) {
|
|
39
91
|
return;
|
|
40
92
|
}
|
|
41
|
-
|
|
93
|
+
const wsServer = new WebSocketServer({
|
|
42
94
|
host: this.config.browser.attach.host,
|
|
43
95
|
port: this.config.browser.attach.port,
|
|
44
96
|
path: this.config.browser.attach.path,
|
|
97
|
+
maxPayload: bodyLimits_1.MAX_BODY_SIZE_BYTES,
|
|
98
|
+
verifyClient: (info, done) => {
|
|
99
|
+
if (!isAllowedAttachOrigin(info.origin)) {
|
|
100
|
+
done(false, 403, "Forbidden");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
done(true);
|
|
104
|
+
},
|
|
45
105
|
});
|
|
46
|
-
this.wsServer
|
|
47
|
-
|
|
106
|
+
this.wsServer = wsServer;
|
|
107
|
+
wsServer.on("connection", (socket) => {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const state = {
|
|
110
|
+
socket,
|
|
111
|
+
lastSeenAt: now,
|
|
112
|
+
rateWindowStartedAt: now,
|
|
113
|
+
rateWindowMessageCount: 0,
|
|
114
|
+
protocolFailed: false,
|
|
115
|
+
};
|
|
48
116
|
this.sockets.add(state);
|
|
49
117
|
socket.on("message", (payload) => {
|
|
50
|
-
void this.handleMessage(state, payload)
|
|
118
|
+
void this.handleMessage(state, payload).catch((error) => {
|
|
119
|
+
this.handleProtocolError(state, error);
|
|
120
|
+
});
|
|
51
121
|
});
|
|
52
122
|
socket.on("close", () => {
|
|
53
123
|
if (state.instanceId) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
124
|
+
const current = this.socketsByInstanceId.get(state.instanceId);
|
|
125
|
+
if (current === state) {
|
|
126
|
+
void this.handleInstanceDisconnect(state.instanceId).catch((error) => {
|
|
127
|
+
this.logger.error("Firefox attach disconnect cleanup failed", {
|
|
128
|
+
instanceId: state.instanceId,
|
|
129
|
+
error: error instanceof Error ? error.message : String(error),
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
this.registry.remove(state.instanceId);
|
|
133
|
+
this.socketsByInstanceId.delete(state.instanceId);
|
|
134
|
+
}
|
|
57
135
|
this.logger.info("Firefox attach instance disconnected", {
|
|
58
136
|
instanceId: state.instanceId,
|
|
59
137
|
});
|
|
@@ -66,6 +144,10 @@ class FirefoxAttachServer {
|
|
|
66
144
|
});
|
|
67
145
|
});
|
|
68
146
|
});
|
|
147
|
+
this.heartbeatReaper = setInterval(() => {
|
|
148
|
+
this.reapStaleSockets();
|
|
149
|
+
}, ATTACH_HEARTBEAT_REAP_INTERVAL_MS);
|
|
150
|
+
this.heartbeatReaper.unref();
|
|
69
151
|
this.logger.info("Firefox attach WebSocket server started", {
|
|
70
152
|
host: this.config.browser.attach.host,
|
|
71
153
|
port: this.config.browser.attach.port,
|
|
@@ -76,6 +158,10 @@ class FirefoxAttachServer {
|
|
|
76
158
|
if (!this.wsServer) {
|
|
77
159
|
return;
|
|
78
160
|
}
|
|
161
|
+
if (this.heartbeatReaper) {
|
|
162
|
+
clearInterval(this.heartbeatReaper);
|
|
163
|
+
this.heartbeatReaper = null;
|
|
164
|
+
}
|
|
79
165
|
for (const state of this.sockets) {
|
|
80
166
|
try {
|
|
81
167
|
state.socket.close(1001, "server shutdown");
|
|
@@ -85,6 +171,7 @@ class FirefoxAttachServer {
|
|
|
85
171
|
}
|
|
86
172
|
}
|
|
87
173
|
this.sockets.clear();
|
|
174
|
+
this.socketsByInstanceId.clear();
|
|
88
175
|
const wsServer = this.wsServer;
|
|
89
176
|
this.wsServer = null;
|
|
90
177
|
await new Promise((resolve, reject) => {
|
|
@@ -110,6 +197,9 @@ class FirefoxAttachServer {
|
|
|
110
197
|
if (existing.instanceId !== input.instanceId) {
|
|
111
198
|
throw new Error("Recording is already active in another browser instance.");
|
|
112
199
|
}
|
|
200
|
+
if (existing.tabId !== input.tabId) {
|
|
201
|
+
throw new Error(`Browser instance '${input.instanceId}' is already recording tab '${existing.tabId}'. Stop it before starting recording for tab '${input.tabId}'.`);
|
|
202
|
+
}
|
|
113
203
|
return existing;
|
|
114
204
|
}
|
|
115
205
|
const session = await this.sessionStore.getSession(input.sessionId);
|
|
@@ -118,7 +208,7 @@ class FirefoxAttachServer {
|
|
|
118
208
|
sessionId: input.sessionId,
|
|
119
209
|
label: this.config.project.sessionLabel?.trim() || input.sessionId,
|
|
120
210
|
cwd: process.cwd(),
|
|
121
|
-
updatedAt: new Date()
|
|
211
|
+
updatedAt: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
|
|
122
212
|
};
|
|
123
213
|
if (!resolvedSession.cwd?.trim()) {
|
|
124
214
|
throw new Error("Workspace cwd is not registered for this console.");
|
|
@@ -170,19 +260,22 @@ class FirefoxAttachServer {
|
|
|
170
260
|
if (!existing) {
|
|
171
261
|
return null;
|
|
172
262
|
}
|
|
263
|
+
const socketState = this.socketsByInstanceId.get(existing.instanceId);
|
|
173
264
|
const activeState = this.activeRecordingsById.get(existing.recordingId);
|
|
174
|
-
if (
|
|
265
|
+
if (socketState) {
|
|
175
266
|
try {
|
|
176
267
|
await this.invokeRecordingControl({
|
|
177
|
-
instanceId:
|
|
178
|
-
tabId:
|
|
179
|
-
recordingId:
|
|
268
|
+
instanceId: existing.instanceId,
|
|
269
|
+
tabId: existing.tabId,
|
|
270
|
+
recordingId: existing.recordingId,
|
|
180
271
|
mode: "stop",
|
|
181
272
|
});
|
|
182
273
|
}
|
|
183
274
|
catch {
|
|
184
|
-
// Local writer still finalizes the bundle.
|
|
275
|
+
// Local writer or persisted state still finalizes the bundle.
|
|
185
276
|
}
|
|
277
|
+
}
|
|
278
|
+
if (activeState) {
|
|
186
279
|
await this.recordingWriter.appendEvent(activeState, {
|
|
187
280
|
kind: "session_stopped",
|
|
188
281
|
status: "stopped",
|
|
@@ -193,11 +286,17 @@ class FirefoxAttachServer {
|
|
|
193
286
|
return activeState.record;
|
|
194
287
|
}
|
|
195
288
|
if (existing.status !== "stopped") {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
289
|
+
const session = await this.sessionStore.getSession(existing.sessionId);
|
|
290
|
+
const stopped = session?.cwd?.trim()
|
|
291
|
+
? await this.recordingWriter.finalizeExisting({
|
|
292
|
+
session,
|
|
293
|
+
record: existing,
|
|
294
|
+
})
|
|
295
|
+
: {
|
|
296
|
+
...existing,
|
|
297
|
+
status: "stopped",
|
|
298
|
+
stoppedAt: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
|
|
299
|
+
};
|
|
201
300
|
await this.maintenanceStore.setBrowserRecording(stopped);
|
|
202
301
|
await this.broadcastRecordingState(stopped);
|
|
203
302
|
return stopped;
|
|
@@ -216,6 +315,7 @@ class FirefoxAttachServer {
|
|
|
216
315
|
reject(new Error(`Attached browser action '${input.action}' timed out for instance '${input.instanceId}'.`));
|
|
217
316
|
}, this.config.browser.timeoutMs);
|
|
218
317
|
this.pendingTabActions.set(requestId, {
|
|
318
|
+
instanceId: input.instanceId,
|
|
219
319
|
resolve,
|
|
220
320
|
reject,
|
|
221
321
|
timer,
|
|
@@ -241,19 +341,39 @@ class FirefoxAttachServer {
|
|
|
241
341
|
socket.send(JSON.stringify(payload));
|
|
242
342
|
}
|
|
243
343
|
async handleMessage(state, payload) {
|
|
344
|
+
if (state.protocolFailed) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.enforceRateLimit(state);
|
|
348
|
+
state.lastSeenAt = Date.now();
|
|
244
349
|
const raw = typeof payload === "string"
|
|
245
350
|
? payload
|
|
246
351
|
: Buffer.isBuffer(payload)
|
|
247
352
|
? payload.toString("utf8")
|
|
248
|
-
:
|
|
249
|
-
|
|
353
|
+
: payload instanceof ArrayBuffer
|
|
354
|
+
? Buffer.from(payload).toString("utf8")
|
|
355
|
+
: Array.isArray(payload) && payload.every(Buffer.isBuffer)
|
|
356
|
+
? Buffer.concat(payload).toString("utf8")
|
|
357
|
+
: String(payload ?? "");
|
|
358
|
+
let decoded;
|
|
250
359
|
try {
|
|
251
|
-
|
|
360
|
+
decoded = JSON.parse(raw);
|
|
252
361
|
}
|
|
253
362
|
catch {
|
|
254
|
-
|
|
255
|
-
|
|
363
|
+
throw new FirefoxAttachProtocolError("invalid_json");
|
|
364
|
+
}
|
|
365
|
+
const parsed = types_1.firefoxAttachInboundMessageSchema.safeParse(decoded);
|
|
366
|
+
if (!parsed.success) {
|
|
367
|
+
throw new FirefoxAttachProtocolError("invalid_message");
|
|
256
368
|
}
|
|
369
|
+
const message = parsed.data;
|
|
370
|
+
if (!state.instanceId && message.type !== "hello") {
|
|
371
|
+
throw new FirefoxAttachProtocolError("hello_required");
|
|
372
|
+
}
|
|
373
|
+
if (state.instanceId && message.type === "hello") {
|
|
374
|
+
throw new FirefoxAttachProtocolError("duplicate_hello");
|
|
375
|
+
}
|
|
376
|
+
const connectedInstanceId = state.instanceId;
|
|
257
377
|
switch (message.type) {
|
|
258
378
|
case "hello":
|
|
259
379
|
await this.handleHello(state, message);
|
|
@@ -293,10 +413,14 @@ class FirefoxAttachServer {
|
|
|
293
413
|
}
|
|
294
414
|
return;
|
|
295
415
|
case "tab_action_result":
|
|
296
|
-
|
|
416
|
+
if (connectedInstanceId) {
|
|
417
|
+
this.resolvePendingTabAction(connectedInstanceId, message);
|
|
418
|
+
}
|
|
297
419
|
return;
|
|
298
420
|
case "recording_control_result":
|
|
299
|
-
|
|
421
|
+
if (connectedInstanceId) {
|
|
422
|
+
this.resolvePendingRecordingControl(connectedInstanceId, message);
|
|
423
|
+
}
|
|
300
424
|
return;
|
|
301
425
|
case "recording_event":
|
|
302
426
|
if (state.instanceId) {
|
|
@@ -315,10 +439,18 @@ class FirefoxAttachServer {
|
|
|
315
439
|
}
|
|
316
440
|
}
|
|
317
441
|
async handleHello(state, message) {
|
|
442
|
+
const previous = this.socketsByInstanceId.get(message.instance_id);
|
|
318
443
|
state.instanceId = message.instance_id;
|
|
319
444
|
this.socketsByInstanceId.set(message.instance_id, state);
|
|
445
|
+
if (previous && previous !== state) {
|
|
446
|
+
this.logger.warn("Firefox attach instance connection replaced", {
|
|
447
|
+
instanceId: message.instance_id,
|
|
448
|
+
});
|
|
449
|
+
previous.socket.close(4001, "replaced by newer connection");
|
|
450
|
+
}
|
|
320
451
|
const instance = this.registry.setConnected({
|
|
321
452
|
instanceId: message.instance_id,
|
|
453
|
+
browser: message.browser,
|
|
322
454
|
extensionVersion: message.extension_version,
|
|
323
455
|
...(message.profile_name ? { profileName: message.profile_name } : {}),
|
|
324
456
|
capabilities: ["tabs", "active_tab", "recording"],
|
|
@@ -360,7 +492,7 @@ class FirefoxAttachServer {
|
|
|
360
492
|
backend: "firefox-attached",
|
|
361
493
|
instanceId,
|
|
362
494
|
tabId: tab.tab_id,
|
|
363
|
-
attachedAt: new Date()
|
|
495
|
+
attachedAt: (0, localTimestamp_1.formatLocalTimestamp)(new Date()),
|
|
364
496
|
...(tab.title ? { title: tab.title } : {}),
|
|
365
497
|
...(tab.url ? { url: tab.url } : {}),
|
|
366
498
|
});
|
|
@@ -372,18 +504,18 @@ class FirefoxAttachServer {
|
|
|
372
504
|
url: tab.url,
|
|
373
505
|
});
|
|
374
506
|
}
|
|
375
|
-
resolvePendingTabAction(message) {
|
|
507
|
+
resolvePendingTabAction(instanceId, message) {
|
|
376
508
|
const pending = this.pendingTabActions.get(message.request_id);
|
|
377
|
-
if (!pending) {
|
|
509
|
+
if (!pending || pending.instanceId !== instanceId) {
|
|
378
510
|
return;
|
|
379
511
|
}
|
|
380
512
|
clearTimeout(pending.timer);
|
|
381
513
|
this.pendingTabActions.delete(message.request_id);
|
|
382
514
|
pending.resolve(message);
|
|
383
515
|
}
|
|
384
|
-
resolvePendingRecordingControl(message) {
|
|
516
|
+
resolvePendingRecordingControl(instanceId, message) {
|
|
385
517
|
const pending = this.pendingRecordingControls.get(message.request_id);
|
|
386
|
-
if (!pending) {
|
|
518
|
+
if (!pending || pending.instanceId !== instanceId) {
|
|
387
519
|
return;
|
|
388
520
|
}
|
|
389
521
|
clearTimeout(pending.timer);
|
|
@@ -402,6 +534,7 @@ class FirefoxAttachServer {
|
|
|
402
534
|
reject(new Error(`Attached browser recording '${input.mode}' timed out for instance '${input.instanceId}'.`));
|
|
403
535
|
}, this.config.browser.timeoutMs);
|
|
404
536
|
this.pendingRecordingControls.set(requestId, {
|
|
537
|
+
instanceId: input.instanceId,
|
|
405
538
|
resolve,
|
|
406
539
|
reject,
|
|
407
540
|
timer,
|
|
@@ -468,7 +601,51 @@ class FirefoxAttachServer {
|
|
|
468
601
|
async broadcastRecordingState(record) {
|
|
469
602
|
const payload = this.buildRecordingStateMessage(record);
|
|
470
603
|
for (const state of this.sockets) {
|
|
471
|
-
|
|
604
|
+
if (state.instanceId) {
|
|
605
|
+
this.sendJson(state.socket, payload);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
enforceRateLimit(state) {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
if (now - state.rateWindowStartedAt >= ATTACH_RATE_WINDOW_MS) {
|
|
612
|
+
state.rateWindowStartedAt = now;
|
|
613
|
+
state.rateWindowMessageCount = 0;
|
|
614
|
+
}
|
|
615
|
+
state.rateWindowMessageCount += 1;
|
|
616
|
+
if (state.rateWindowMessageCount > ATTACH_RATE_MAX_MESSAGES) {
|
|
617
|
+
throw new FirefoxAttachProtocolError("rate_limit_exceeded");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
handleProtocolError(state, error) {
|
|
621
|
+
if (state.protocolFailed) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
state.protocolFailed = true;
|
|
625
|
+
const reason = error instanceof FirefoxAttachProtocolError
|
|
626
|
+
? error.reason
|
|
627
|
+
: "message_handler_failed";
|
|
628
|
+
this.logger.warn("Firefox attach protocol error", {
|
|
629
|
+
...(state.instanceId ? { instanceId: state.instanceId } : {}),
|
|
630
|
+
reason,
|
|
631
|
+
});
|
|
632
|
+
try {
|
|
633
|
+
state.socket.close(1008, reason);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// The socket may already have closed while the handler was awaiting I/O.
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
reapStaleSockets() {
|
|
640
|
+
const staleBefore = Date.now() - ATTACH_HEARTBEAT_TIMEOUT_MS;
|
|
641
|
+
for (const state of this.sockets) {
|
|
642
|
+
if (state.lastSeenAt >= staleBefore) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
this.logger.warn("Firefox attach socket heartbeat expired", {
|
|
646
|
+
...(state.instanceId ? { instanceId: state.instanceId } : {}),
|
|
647
|
+
});
|
|
648
|
+
state.socket.close(4002, "heartbeat timeout");
|
|
472
649
|
}
|
|
473
650
|
}
|
|
474
651
|
mapManualRecordingResult(record) {
|