@mearl/cloud-server 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/dist/cli.js +178 -47
- package/dist/index.d.ts +6 -1
- package/dist/index.js +177 -46
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ mearl-cloud-server [command] [options]
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
**子命令:**
|
|
24
|
+
|
|
24
25
|
- `start` - 后台启动 server(默认,不写也等价于 `start`)
|
|
25
26
|
- `stop` - 停止后台 server
|
|
26
27
|
- `restart` - 重启后台 server
|
|
@@ -28,6 +29,7 @@ mearl-cloud-server [command] [options]
|
|
|
28
29
|
- `logs` - 打印 server 日志末尾
|
|
29
30
|
|
|
30
31
|
**选项:**
|
|
32
|
+
|
|
31
33
|
- `--foreground` / `-f` - 前台运行(不守护化,适合 systemd / Docker / PM2)
|
|
32
34
|
- `--port <port>` - 服务器端口(默认 8080)
|
|
33
35
|
- `--path <path>` - WebSocket 路径(默认 /ws)
|
|
@@ -36,6 +38,7 @@ mearl-cloud-server [command] [options]
|
|
|
36
38
|
- `--max-connections <n>` - 最大连接数(默认 100)
|
|
37
39
|
|
|
38
40
|
**示例:**
|
|
41
|
+
|
|
39
42
|
```bash
|
|
40
43
|
# 后台启动(自动生成 token),打印连接命令后立即返回
|
|
41
44
|
mearl-cloud-server
|
|
@@ -55,6 +58,7 @@ mearl-cloud-server --foreground
|
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
后台启动后会打印供本地机器使用的连接命令,例如:
|
|
61
|
+
|
|
58
62
|
```
|
|
59
63
|
[CloudServer] Started in background (pid 12345)
|
|
60
64
|
[CloudServer] Logs: ~/.mearl/cloud-server.log
|
|
@@ -64,16 +68,19 @@ mearl-cloud-server --foreground
|
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
运行时文件位于 `~/.mearl/`:
|
|
71
|
+
|
|
67
72
|
- `cloud-server.json` - 连接配置(含 server URL、pid、端口等),cloud-client 自动读取
|
|
68
73
|
- `cloud-server.log` - 后台进程日志(`logs` 子命令读取,或 `tail -f` 跟踪);超过 5MB 会在下次启动时滚动为 `cloud-server.log.1`
|
|
69
74
|
|
|
70
75
|
> 守护进程的启停、状态、日志滚动等生命周期逻辑由共享包 [`@mearl/daemon-core`](../../daemon-core) 提供(与 cloud-connector 复用同一套实现)。
|
|
71
76
|
|
|
72
77
|
**关于 token:**
|
|
78
|
+
|
|
73
79
|
- 未指定 `--token` 时会自动生成。`restart` 会**沿用当前 token**,因此之前打印的连接命令重启后仍然有效。
|
|
74
80
|
- `stop` 后再 `start` 属于全新启动,会重新生成 token;若希望连接命令长期固定,请显式传入 `--token` 或设置 `MEARL_TOKEN`。
|
|
75
81
|
|
|
76
82
|
**环境变量:**
|
|
83
|
+
|
|
77
84
|
- `MEARL_TOKEN` - 鉴权密钥(覆盖 --token 选项)
|
|
78
85
|
- `CLOUD_SERVER_HOST` - 公网主机名(默认自动获取)
|
|
79
86
|
|
|
@@ -125,21 +132,32 @@ Server 接受两种类型的连接:
|
|
|
125
132
|
1. **Agent 连接** - 发起请求的云端客户端
|
|
126
133
|
2. **Connector 连接** - 转发请求到 native-host 的本地连接器
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
两类客户端建连后会分别发送 `agent_hello` 和 `connector_hello`。Connector 会注册
|
|
136
|
+
稳定 ID、名称和版本;同一 ID 重连时,新连接会替换旧连接。
|
|
137
|
+
|
|
138
|
+
只有一个 Connector 在线时,Server 自动选择它。多个 Connector 同时在线时,Agent
|
|
139
|
+
先调用 `connector_list` 获取清单,再在请求中传 `connector`;未指定目标时
|
|
140
|
+
Server 会拒绝请求,避免把浏览器操作发到错误机器。
|
|
129
141
|
|
|
130
142
|
## 消息协议
|
|
131
143
|
|
|
132
144
|
### 请求格式(Agent → Server)
|
|
145
|
+
|
|
133
146
|
```json
|
|
134
147
|
{
|
|
135
148
|
"id": "unique-id",
|
|
136
149
|
"action": "get_requests",
|
|
137
150
|
"data": { "count": 5 },
|
|
151
|
+
"connector": "work-mac",
|
|
138
152
|
"version": "1.30.0"
|
|
139
153
|
}
|
|
140
154
|
```
|
|
141
155
|
|
|
156
|
+
`connector` 接受 connector ID 或可唯一匹配的名称。`connector_list` 是
|
|
157
|
+
Server 内置命令,不会转发到浏览器侧。
|
|
158
|
+
|
|
142
159
|
### 响应格式(Server → Agent)
|
|
160
|
+
|
|
143
161
|
```json
|
|
144
162
|
{
|
|
145
163
|
"id": "unique-id",
|
|
@@ -150,6 +168,7 @@ Server 会自动识别连接类型,并将 Agent 的请求转发到可用的 Co
|
|
|
150
168
|
```
|
|
151
169
|
|
|
152
170
|
### 心跳格式
|
|
171
|
+
|
|
153
172
|
```json
|
|
154
173
|
{ "type": "ping", "timestamp": 1234567890 }
|
|
155
174
|
{ "type": "pong", "timestamp": 1234567890 }
|
|
@@ -211,4 +230,4 @@ pm2 start mearl-cloud-server --name mtop-cloud-server -- --foreground --port 808
|
|
|
211
230
|
- 设置合理的 `maxConnections` 限制资源占用
|
|
212
231
|
- 启用 `token` 鉴权保护服务安全
|
|
213
232
|
- 在沙箱环境中启动时,会自动检测并使用平台分配的公网地址
|
|
214
|
-
- 如果沙箱端口映射 API 不可用,会回退到使用本地 IP 地址
|
|
233
|
+
- 如果沙箱端口映射 API 不可用,会回退到使用本地 IP 地址
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,8 @@ import { createDaemon, readRecord, removeRecord } from "@mearl/daemon-core";
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
import WebSocket, { WebSocketServer } from "ws";
|
|
9
9
|
import { createServer as createHttpServer } from "node:http";
|
|
10
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
11
12
|
import { networkInterfaces } from "node:os";
|
|
12
13
|
|
|
13
14
|
// src/paths.ts
|
|
@@ -19,9 +20,12 @@ var LOG_FILE = join(CONFIG_DIR, "cloud-server.log");
|
|
|
19
20
|
|
|
20
21
|
// src/index.ts
|
|
21
22
|
import {
|
|
23
|
+
isAgentHelloMessage,
|
|
24
|
+
isConnectorHelloMessage,
|
|
22
25
|
isHeartbeatMessage,
|
|
23
26
|
isCloudMessage,
|
|
24
27
|
isCloudResponse,
|
|
28
|
+
resolveCloudConnectorSelector,
|
|
25
29
|
MAX_BUFFER_SIZE,
|
|
26
30
|
DEFAULT_HEARTBEAT_TIMEOUT,
|
|
27
31
|
DEFAULT_REQUEST_TIMEOUT
|
|
@@ -45,6 +49,7 @@ var DEFAULT_PATH = "/ws";
|
|
|
45
49
|
var DEFAULT_MAX_CONNECTIONS = 100;
|
|
46
50
|
var DEFAULT_MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
|
|
47
51
|
var RESPONSE_GRACE_MS = 5e3;
|
|
52
|
+
var LEGACY_CONNECTOR_HELLO_GRACE_MS = 1e3;
|
|
48
53
|
var CloudServer = class {
|
|
49
54
|
options;
|
|
50
55
|
connectorWss = null;
|
|
@@ -75,17 +80,12 @@ var CloudServer = class {
|
|
|
75
80
|
return this.options.token;
|
|
76
81
|
}
|
|
77
82
|
generateRandomToken() {
|
|
78
|
-
|
|
79
|
-
let token = "";
|
|
80
|
-
for (let i = 0; i < 32; i++) {
|
|
81
|
-
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
82
|
-
}
|
|
83
|
-
return token;
|
|
83
|
+
return randomBytes(24).toString("base64url");
|
|
84
84
|
}
|
|
85
85
|
async start() {
|
|
86
|
+
const token = this.getToken();
|
|
86
87
|
await this.createListener(this.options.agentPort, "127.0.0.1", true);
|
|
87
88
|
await this.createListener(this.options.port, "0.0.0.0", false);
|
|
88
|
-
const token = this.getToken();
|
|
89
89
|
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;
|
|
90
90
|
let publicWsUrl;
|
|
91
91
|
const sandboxUrl = await this.getSandboxPublicUrl(this.options.port);
|
|
@@ -104,6 +104,11 @@ var CloudServer = class {
|
|
|
104
104
|
const publicHost = process.env.CLOUD_SERVER_HOST || this.getPublicIP();
|
|
105
105
|
publicWsUrl = `ws://${publicHost}:${this.options.port}${this.options.path}?token=${token}`;
|
|
106
106
|
}
|
|
107
|
+
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
|
|
108
|
+
console.warn(
|
|
109
|
+
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
107
112
|
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
108
113
|
console.log(
|
|
109
114
|
`[CloudServer] Connector listener on 0.0.0.0:${this.options.port} (network); agent listener on 127.0.0.1:${this.options.agentPort} (local only)`
|
|
@@ -176,6 +181,9 @@ var CloudServer = class {
|
|
|
176
181
|
ws.close();
|
|
177
182
|
}
|
|
178
183
|
this.connections.clear();
|
|
184
|
+
for (const pending of this.pendingRequests.values()) {
|
|
185
|
+
clearTimeout(pending.timer);
|
|
186
|
+
}
|
|
179
187
|
this.pendingRequests.clear();
|
|
180
188
|
this.agentWss?.close();
|
|
181
189
|
this.connectorWss?.close();
|
|
@@ -216,7 +224,8 @@ var CloudServer = class {
|
|
|
216
224
|
ws,
|
|
217
225
|
id: connId,
|
|
218
226
|
lastHeartbeat: Date.now(),
|
|
219
|
-
|
|
227
|
+
connectedAt: Date.now(),
|
|
228
|
+
role: fromLocal ? "agent" : "pending",
|
|
220
229
|
fromLocal,
|
|
221
230
|
messageBuffer: ""
|
|
222
231
|
};
|
|
@@ -240,7 +249,7 @@ var CloudServer = class {
|
|
|
240
249
|
if (!line.trim()) continue;
|
|
241
250
|
try {
|
|
242
251
|
const message = JSON.parse(line);
|
|
243
|
-
this.handleMessage(ws, message
|
|
252
|
+
this.handleMessage(ws, message);
|
|
244
253
|
} catch (error) {
|
|
245
254
|
console.error(`[CloudServer] Failed to parse message from ${connId}:`, line, error);
|
|
246
255
|
}
|
|
@@ -255,7 +264,7 @@ var CloudServer = class {
|
|
|
255
264
|
console.error(`[CloudServer] Connection error for ${connId}:`, error.message);
|
|
256
265
|
});
|
|
257
266
|
}
|
|
258
|
-
handleMessage(ws, message
|
|
267
|
+
handleMessage(ws, message) {
|
|
259
268
|
const conn = this.connections.get(ws);
|
|
260
269
|
if (!conn) return;
|
|
261
270
|
if (isHeartbeatMessage(message)) {
|
|
@@ -269,20 +278,61 @@ var CloudServer = class {
|
|
|
269
278
|
}
|
|
270
279
|
return;
|
|
271
280
|
}
|
|
281
|
+
if (isAgentHelloMessage(message)) {
|
|
282
|
+
this.handleAgentHello(ws);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (isConnectorHelloMessage(message)) {
|
|
286
|
+
this.handleConnectorHello(ws, message);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
272
289
|
if (isCloudResponse(message)) {
|
|
273
|
-
this.handleResponse(ws, message
|
|
290
|
+
this.handleResponse(ws, message);
|
|
274
291
|
return;
|
|
275
292
|
}
|
|
276
293
|
if (isCloudMessage(message)) {
|
|
277
|
-
this.handleRequest(ws, message
|
|
294
|
+
this.handleRequest(ws, message);
|
|
278
295
|
return;
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
|
-
|
|
282
|
-
const { id, action, data } = message;
|
|
298
|
+
handleAgentHello(ws) {
|
|
283
299
|
const conn = this.connections.get(ws);
|
|
284
300
|
if (!conn) return;
|
|
285
301
|
if (!conn.fromLocal && !this.options.allowRemoteAgent) {
|
|
302
|
+
ws.close(1008, "Agent must be local");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
conn.role = "agent";
|
|
306
|
+
delete conn.connector;
|
|
307
|
+
}
|
|
308
|
+
handleConnectorHello(ws, message) {
|
|
309
|
+
const conn = this.connections.get(ws);
|
|
310
|
+
if (!conn || conn.fromLocal) {
|
|
311
|
+
ws.close(1008, "Connector must use the connector listener");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const [otherWs, other] of this.connections) {
|
|
315
|
+
if (otherWs !== ws && other.role === "connector" && other.connector?.connectorId === message.connectorId) {
|
|
316
|
+
this.connections.delete(otherWs);
|
|
317
|
+
this.rejectPendingForWs(otherWs);
|
|
318
|
+
otherWs.close(1e3, "Connector replaced by a newer connection");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
conn.role = "connector";
|
|
322
|
+
conn.connector = {
|
|
323
|
+
connectorId: message.connectorId,
|
|
324
|
+
name: message.name,
|
|
325
|
+
...message.version ? { version: message.version } : {}
|
|
326
|
+
};
|
|
327
|
+
console.log(
|
|
328
|
+
`[CloudServer] Connector registered: ${message.name} (${message.connectorId}, v${message.version ?? "unknown"})`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
handleRequest(ws, message) {
|
|
332
|
+
const { id, action, data } = message;
|
|
333
|
+
const conn = this.connections.get(ws);
|
|
334
|
+
if (!conn) return;
|
|
335
|
+
if (conn.role !== "agent") {
|
|
286
336
|
console.warn(
|
|
287
337
|
`[CloudServer] Rejected agent request from non-local connection ${conn.id} (action: ${action})`
|
|
288
338
|
);
|
|
@@ -290,62 +340,138 @@ var CloudServer = class {
|
|
|
290
340
|
JSON.stringify({
|
|
291
341
|
id,
|
|
292
342
|
success: false,
|
|
293
|
-
error: "Agent requests are only accepted on the local interface (127.0.0.1). Run cloud-client on the same machine as cloud-server, or start the server with --allow-remote-agent to override."
|
|
343
|
+
error: this.options.allowRemoteAgent && !conn.fromLocal ? "Remote agents must identify themselves with an agent_hello message before issuing commands." : "Agent requests are only accepted on the local interface (127.0.0.1). Run cloud-client on the same machine as cloud-server, or start the server with --allow-remote-agent to override."
|
|
294
344
|
}) + "\n"
|
|
295
345
|
);
|
|
296
|
-
ws.close(1008, "Agent must be local");
|
|
297
346
|
return;
|
|
298
347
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
348
|
+
if (action === "connector_list") {
|
|
349
|
+
const response = {
|
|
350
|
+
id,
|
|
351
|
+
success: true,
|
|
352
|
+
data: this.getConnectorList()
|
|
353
|
+
};
|
|
354
|
+
ws.send(JSON.stringify(response) + "\n");
|
|
303
355
|
return;
|
|
304
356
|
}
|
|
357
|
+
const target = this.resolveConnector(message.connector);
|
|
358
|
+
if ("error" in target) {
|
|
359
|
+
ws.send(JSON.stringify({ id, success: false, error: target.error }) + "\n");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const routeId = randomBytes(12).toString("base64url");
|
|
305
363
|
const requestedTimeoutSec = Number(message.timeoutSec);
|
|
306
364
|
const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);
|
|
307
365
|
const timer = setTimeout(
|
|
308
366
|
() => {
|
|
309
|
-
this.pendingRequests.delete(
|
|
367
|
+
this.pendingRequests.delete(routeId);
|
|
310
368
|
if (ws.readyState === WebSocket.OPEN) {
|
|
311
369
|
ws.send(JSON.stringify({ id, success: false, error: "Request timeout" }) + "\n");
|
|
312
370
|
}
|
|
313
371
|
},
|
|
314
372
|
timeoutSec * 1e3 + RESPONSE_GRACE_MS
|
|
315
373
|
);
|
|
316
|
-
this.pendingRequests.set(
|
|
317
|
-
|
|
318
|
-
|
|
374
|
+
this.pendingRequests.set(routeId, {
|
|
375
|
+
agentRequestId: id,
|
|
376
|
+
agentWs: ws,
|
|
377
|
+
connectorWs: target.ws,
|
|
378
|
+
timer
|
|
379
|
+
});
|
|
380
|
+
target.ws.send(
|
|
381
|
+
JSON.stringify({
|
|
382
|
+
id: routeId,
|
|
383
|
+
action,
|
|
384
|
+
data,
|
|
385
|
+
browser: message.browser,
|
|
386
|
+
timeoutSec
|
|
387
|
+
}) + "\n"
|
|
319
388
|
);
|
|
320
389
|
}
|
|
321
|
-
handleResponse(ws, response
|
|
390
|
+
handleResponse(ws, response) {
|
|
322
391
|
const conn = this.connections.get(ws);
|
|
323
392
|
if (!conn) return;
|
|
324
|
-
if (conn.
|
|
393
|
+
if (conn.role !== "connector") return;
|
|
325
394
|
const pending = this.pendingRequests.get(response.id);
|
|
326
|
-
if (!pending) return;
|
|
395
|
+
if (!pending || pending.connectorWs !== ws) return;
|
|
327
396
|
this.pendingRequests.delete(response.id);
|
|
328
397
|
clearTimeout(pending.timer);
|
|
329
|
-
if (pending.
|
|
330
|
-
pending.
|
|
398
|
+
if (pending.agentWs.readyState === WebSocket.OPEN) {
|
|
399
|
+
pending.agentWs.send(
|
|
400
|
+
JSON.stringify({ ...response, id: pending.agentRequestId }) + "\n"
|
|
401
|
+
);
|
|
331
402
|
}
|
|
332
403
|
}
|
|
333
|
-
|
|
404
|
+
connectorInfo(ws, conn) {
|
|
405
|
+
const pendingRequests = Array.from(this.pendingRequests.values()).filter(
|
|
406
|
+
(pending) => pending.connectorWs === ws
|
|
407
|
+
).length;
|
|
408
|
+
return {
|
|
409
|
+
connectorId: conn.connector?.connectorId ?? conn.id,
|
|
410
|
+
name: conn.connector?.name ?? `legacy-${conn.id.slice(-8)}`,
|
|
411
|
+
...conn.connector?.version ? { version: conn.connector.version } : {},
|
|
412
|
+
connectedAt: new Date(conn.connectedAt).toISOString(),
|
|
413
|
+
lastHeartbeatAt: new Date(conn.lastHeartbeat).toISOString(),
|
|
414
|
+
pendingRequests,
|
|
415
|
+
...!conn.connector ? { legacy: true } : {}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
getConnectorConnections() {
|
|
419
|
+
const connectors = [];
|
|
334
420
|
for (const [ws, conn] of this.connections) {
|
|
335
|
-
if (
|
|
336
|
-
|
|
421
|
+
if (conn.role === "pending" && Date.now() - conn.connectedAt >= LEGACY_CONNECTOR_HELLO_GRACE_MS) {
|
|
422
|
+
conn.role = "connector";
|
|
337
423
|
}
|
|
424
|
+
if (conn.role !== "connector" || ws.readyState !== WebSocket.OPEN) continue;
|
|
425
|
+
connectors.push({ ws, conn, info: this.connectorInfo(ws, conn) });
|
|
338
426
|
}
|
|
339
|
-
return
|
|
427
|
+
return connectors.sort((a, b) => a.info.name.localeCompare(b.info.name));
|
|
428
|
+
}
|
|
429
|
+
getConnectorList() {
|
|
430
|
+
const connectors = this.getConnectorConnections().map(({ info }) => info);
|
|
431
|
+
return { count: connectors.length, connectors };
|
|
432
|
+
}
|
|
433
|
+
resolveConnector(selector) {
|
|
434
|
+
const connectors = this.getConnectorConnections();
|
|
435
|
+
if (connectors.length === 0) return { error: "No local connector available" };
|
|
436
|
+
if (!selector) {
|
|
437
|
+
if (connectors.length === 1) {
|
|
438
|
+
return { ws: connectors[0].ws, info: connectors[0].info };
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
error: `Multiple cloud connectors are connected (${connectors.length}). Run \`mearl connector_list\` and pass --connector <id|name>.`
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const selection = resolveCloudConnectorSelector(
|
|
445
|
+
connectors.map(({ info }) => info),
|
|
446
|
+
selector
|
|
447
|
+
);
|
|
448
|
+
if (selection.status === "matched") {
|
|
449
|
+
const matched = connectors.find(({ info }) => info === selection.connector);
|
|
450
|
+
return { ws: matched.ws, info: matched.info };
|
|
451
|
+
}
|
|
452
|
+
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
453
|
+
if (selection.status === "ambiguous") {
|
|
454
|
+
return {
|
|
455
|
+
error: `Cloud connector selector "${selector}" is ambiguous. Available: ${available}`
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
error: `No cloud connector matches "${selector}". Available: ${available}`
|
|
460
|
+
};
|
|
340
461
|
}
|
|
341
462
|
rejectPendingForWs(ws) {
|
|
342
463
|
this.pendingRequests.forEach((pending, id) => {
|
|
343
|
-
if (pending.ws
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
464
|
+
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
|
|
465
|
+
clearTimeout(pending.timer);
|
|
466
|
+
this.pendingRequests.delete(id);
|
|
467
|
+
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
|
|
468
|
+
pending.agentWs.send(
|
|
469
|
+
JSON.stringify({
|
|
470
|
+
id: pending.agentRequestId,
|
|
471
|
+
success: false,
|
|
472
|
+
error: "Connector connection closed"
|
|
473
|
+
}) + "\n"
|
|
474
|
+
);
|
|
349
475
|
}
|
|
350
476
|
});
|
|
351
477
|
}
|
|
@@ -401,10 +527,17 @@ var CloudServer = class {
|
|
|
401
527
|
writeConfigFile(info) {
|
|
402
528
|
try {
|
|
403
529
|
if (!existsSync(CONFIG_DIR)) {
|
|
404
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
530
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
405
531
|
}
|
|
406
532
|
const config = { server: info.localUrl, ...info };
|
|
407
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2),
|
|
533
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
534
|
+
encoding: "utf-8",
|
|
535
|
+
mode: 384
|
|
536
|
+
});
|
|
537
|
+
if (process.platform !== "win32") {
|
|
538
|
+
chmodSync(CONFIG_DIR, 448);
|
|
539
|
+
chmodSync(CONFIG_FILE, 384);
|
|
540
|
+
}
|
|
408
541
|
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);
|
|
409
542
|
} catch (error) {
|
|
410
543
|
console.error("[CloudServer] Failed to write config file:", error);
|
|
@@ -412,15 +545,13 @@ var CloudServer = class {
|
|
|
412
545
|
}
|
|
413
546
|
getStats() {
|
|
414
547
|
let agents = 0;
|
|
415
|
-
let connectors = 0;
|
|
416
548
|
for (const conn of this.connections.values()) {
|
|
417
|
-
if (conn.
|
|
418
|
-
else connectors++;
|
|
549
|
+
if (conn.role === "agent") agents++;
|
|
419
550
|
}
|
|
420
551
|
return {
|
|
421
552
|
connections: this.connections.size,
|
|
422
553
|
agents,
|
|
423
|
-
connectors,
|
|
554
|
+
connectors: this.getConnectorConnections().length,
|
|
424
555
|
pendingRequests: this.pendingRequests.size
|
|
425
556
|
};
|
|
426
557
|
}
|
|
@@ -489,7 +620,7 @@ async function runForeground(options) {
|
|
|
489
620
|
}
|
|
490
621
|
|
|
491
622
|
// src/cli.ts
|
|
492
|
-
var CLOUD_SERVER_VERSION = true ? "2.0
|
|
623
|
+
var CLOUD_SERVER_VERSION = true ? "2.2.0" : "unknown";
|
|
493
624
|
var argv = process.argv.slice(2);
|
|
494
625
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
495
626
|
console.log(CLOUD_SERVER_VERSION);
|
package/dist/index.d.ts
CHANGED
|
@@ -48,9 +48,14 @@ export declare class CloudServer {
|
|
|
48
48
|
stop(): Promise<void>;
|
|
49
49
|
private handleConnection;
|
|
50
50
|
private handleMessage;
|
|
51
|
+
private handleAgentHello;
|
|
52
|
+
private handleConnectorHello;
|
|
51
53
|
private handleRequest;
|
|
52
54
|
private handleResponse;
|
|
53
|
-
private
|
|
55
|
+
private connectorInfo;
|
|
56
|
+
private getConnectorConnections;
|
|
57
|
+
private getConnectorList;
|
|
58
|
+
private resolveConnector;
|
|
54
59
|
private rejectPendingForWs;
|
|
55
60
|
private startHeartbeatCheck;
|
|
56
61
|
private getPublicIP;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import WebSocket, { WebSocketServer } from "ws";
|
|
3
3
|
import { createServer as createHttpServer } from "node:http";
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { networkInterfaces } from "node:os";
|
|
6
7
|
|
|
7
8
|
// src/paths.ts
|
|
@@ -13,9 +14,12 @@ var LOG_FILE = join(CONFIG_DIR, "cloud-server.log");
|
|
|
13
14
|
|
|
14
15
|
// src/index.ts
|
|
15
16
|
import {
|
|
17
|
+
isAgentHelloMessage,
|
|
18
|
+
isConnectorHelloMessage,
|
|
16
19
|
isHeartbeatMessage,
|
|
17
20
|
isCloudMessage,
|
|
18
21
|
isCloudResponse,
|
|
22
|
+
resolveCloudConnectorSelector,
|
|
19
23
|
MAX_BUFFER_SIZE,
|
|
20
24
|
DEFAULT_HEARTBEAT_TIMEOUT,
|
|
21
25
|
DEFAULT_REQUEST_TIMEOUT
|
|
@@ -39,6 +43,7 @@ var DEFAULT_PATH = "/ws";
|
|
|
39
43
|
var DEFAULT_MAX_CONNECTIONS = 100;
|
|
40
44
|
var DEFAULT_MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
|
|
41
45
|
var RESPONSE_GRACE_MS = 5e3;
|
|
46
|
+
var LEGACY_CONNECTOR_HELLO_GRACE_MS = 1e3;
|
|
42
47
|
var CloudServer = class {
|
|
43
48
|
options;
|
|
44
49
|
connectorWss = null;
|
|
@@ -69,17 +74,12 @@ var CloudServer = class {
|
|
|
69
74
|
return this.options.token;
|
|
70
75
|
}
|
|
71
76
|
generateRandomToken() {
|
|
72
|
-
|
|
73
|
-
let token = "";
|
|
74
|
-
for (let i = 0; i < 32; i++) {
|
|
75
|
-
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
76
|
-
}
|
|
77
|
-
return token;
|
|
77
|
+
return randomBytes(24).toString("base64url");
|
|
78
78
|
}
|
|
79
79
|
async start() {
|
|
80
|
+
const token = this.getToken();
|
|
80
81
|
await this.createListener(this.options.agentPort, "127.0.0.1", true);
|
|
81
82
|
await this.createListener(this.options.port, "0.0.0.0", false);
|
|
82
|
-
const token = this.getToken();
|
|
83
83
|
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;
|
|
84
84
|
let publicWsUrl;
|
|
85
85
|
const sandboxUrl = await this.getSandboxPublicUrl(this.options.port);
|
|
@@ -98,6 +98,11 @@ var CloudServer = class {
|
|
|
98
98
|
const publicHost = process.env.CLOUD_SERVER_HOST || this.getPublicIP();
|
|
99
99
|
publicWsUrl = `ws://${publicHost}:${this.options.port}${this.options.path}?token=${token}`;
|
|
100
100
|
}
|
|
101
|
+
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
|
|
102
|
+
console.warn(
|
|
103
|
+
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
101
106
|
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
102
107
|
console.log(
|
|
103
108
|
`[CloudServer] Connector listener on 0.0.0.0:${this.options.port} (network); agent listener on 127.0.0.1:${this.options.agentPort} (local only)`
|
|
@@ -170,6 +175,9 @@ var CloudServer = class {
|
|
|
170
175
|
ws.close();
|
|
171
176
|
}
|
|
172
177
|
this.connections.clear();
|
|
178
|
+
for (const pending of this.pendingRequests.values()) {
|
|
179
|
+
clearTimeout(pending.timer);
|
|
180
|
+
}
|
|
173
181
|
this.pendingRequests.clear();
|
|
174
182
|
this.agentWss?.close();
|
|
175
183
|
this.connectorWss?.close();
|
|
@@ -210,7 +218,8 @@ var CloudServer = class {
|
|
|
210
218
|
ws,
|
|
211
219
|
id: connId,
|
|
212
220
|
lastHeartbeat: Date.now(),
|
|
213
|
-
|
|
221
|
+
connectedAt: Date.now(),
|
|
222
|
+
role: fromLocal ? "agent" : "pending",
|
|
214
223
|
fromLocal,
|
|
215
224
|
messageBuffer: ""
|
|
216
225
|
};
|
|
@@ -234,7 +243,7 @@ var CloudServer = class {
|
|
|
234
243
|
if (!line.trim()) continue;
|
|
235
244
|
try {
|
|
236
245
|
const message = JSON.parse(line);
|
|
237
|
-
this.handleMessage(ws, message
|
|
246
|
+
this.handleMessage(ws, message);
|
|
238
247
|
} catch (error) {
|
|
239
248
|
console.error(`[CloudServer] Failed to parse message from ${connId}:`, line, error);
|
|
240
249
|
}
|
|
@@ -249,7 +258,7 @@ var CloudServer = class {
|
|
|
249
258
|
console.error(`[CloudServer] Connection error for ${connId}:`, error.message);
|
|
250
259
|
});
|
|
251
260
|
}
|
|
252
|
-
handleMessage(ws, message
|
|
261
|
+
handleMessage(ws, message) {
|
|
253
262
|
const conn = this.connections.get(ws);
|
|
254
263
|
if (!conn) return;
|
|
255
264
|
if (isHeartbeatMessage(message)) {
|
|
@@ -263,20 +272,61 @@ var CloudServer = class {
|
|
|
263
272
|
}
|
|
264
273
|
return;
|
|
265
274
|
}
|
|
275
|
+
if (isAgentHelloMessage(message)) {
|
|
276
|
+
this.handleAgentHello(ws);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (isConnectorHelloMessage(message)) {
|
|
280
|
+
this.handleConnectorHello(ws, message);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
266
283
|
if (isCloudResponse(message)) {
|
|
267
|
-
this.handleResponse(ws, message
|
|
284
|
+
this.handleResponse(ws, message);
|
|
268
285
|
return;
|
|
269
286
|
}
|
|
270
287
|
if (isCloudMessage(message)) {
|
|
271
|
-
this.handleRequest(ws, message
|
|
288
|
+
this.handleRequest(ws, message);
|
|
272
289
|
return;
|
|
273
290
|
}
|
|
274
291
|
}
|
|
275
|
-
|
|
276
|
-
const { id, action, data } = message;
|
|
292
|
+
handleAgentHello(ws) {
|
|
277
293
|
const conn = this.connections.get(ws);
|
|
278
294
|
if (!conn) return;
|
|
279
295
|
if (!conn.fromLocal && !this.options.allowRemoteAgent) {
|
|
296
|
+
ws.close(1008, "Agent must be local");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
conn.role = "agent";
|
|
300
|
+
delete conn.connector;
|
|
301
|
+
}
|
|
302
|
+
handleConnectorHello(ws, message) {
|
|
303
|
+
const conn = this.connections.get(ws);
|
|
304
|
+
if (!conn || conn.fromLocal) {
|
|
305
|
+
ws.close(1008, "Connector must use the connector listener");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
for (const [otherWs, other] of this.connections) {
|
|
309
|
+
if (otherWs !== ws && other.role === "connector" && other.connector?.connectorId === message.connectorId) {
|
|
310
|
+
this.connections.delete(otherWs);
|
|
311
|
+
this.rejectPendingForWs(otherWs);
|
|
312
|
+
otherWs.close(1e3, "Connector replaced by a newer connection");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
conn.role = "connector";
|
|
316
|
+
conn.connector = {
|
|
317
|
+
connectorId: message.connectorId,
|
|
318
|
+
name: message.name,
|
|
319
|
+
...message.version ? { version: message.version } : {}
|
|
320
|
+
};
|
|
321
|
+
console.log(
|
|
322
|
+
`[CloudServer] Connector registered: ${message.name} (${message.connectorId}, v${message.version ?? "unknown"})`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
handleRequest(ws, message) {
|
|
326
|
+
const { id, action, data } = message;
|
|
327
|
+
const conn = this.connections.get(ws);
|
|
328
|
+
if (!conn) return;
|
|
329
|
+
if (conn.role !== "agent") {
|
|
280
330
|
console.warn(
|
|
281
331
|
`[CloudServer] Rejected agent request from non-local connection ${conn.id} (action: ${action})`
|
|
282
332
|
);
|
|
@@ -284,62 +334,138 @@ var CloudServer = class {
|
|
|
284
334
|
JSON.stringify({
|
|
285
335
|
id,
|
|
286
336
|
success: false,
|
|
287
|
-
error: "Agent requests are only accepted on the local interface (127.0.0.1). Run cloud-client on the same machine as cloud-server, or start the server with --allow-remote-agent to override."
|
|
337
|
+
error: this.options.allowRemoteAgent && !conn.fromLocal ? "Remote agents must identify themselves with an agent_hello message before issuing commands." : "Agent requests are only accepted on the local interface (127.0.0.1). Run cloud-client on the same machine as cloud-server, or start the server with --allow-remote-agent to override."
|
|
288
338
|
}) + "\n"
|
|
289
339
|
);
|
|
290
|
-
ws.close(1008, "Agent must be local");
|
|
291
340
|
return;
|
|
292
341
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
342
|
+
if (action === "connector_list") {
|
|
343
|
+
const response = {
|
|
344
|
+
id,
|
|
345
|
+
success: true,
|
|
346
|
+
data: this.getConnectorList()
|
|
347
|
+
};
|
|
348
|
+
ws.send(JSON.stringify(response) + "\n");
|
|
297
349
|
return;
|
|
298
350
|
}
|
|
351
|
+
const target = this.resolveConnector(message.connector);
|
|
352
|
+
if ("error" in target) {
|
|
353
|
+
ws.send(JSON.stringify({ id, success: false, error: target.error }) + "\n");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const routeId = randomBytes(12).toString("base64url");
|
|
299
357
|
const requestedTimeoutSec = Number(message.timeoutSec);
|
|
300
358
|
const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);
|
|
301
359
|
const timer = setTimeout(
|
|
302
360
|
() => {
|
|
303
|
-
this.pendingRequests.delete(
|
|
361
|
+
this.pendingRequests.delete(routeId);
|
|
304
362
|
if (ws.readyState === WebSocket.OPEN) {
|
|
305
363
|
ws.send(JSON.stringify({ id, success: false, error: "Request timeout" }) + "\n");
|
|
306
364
|
}
|
|
307
365
|
},
|
|
308
366
|
timeoutSec * 1e3 + RESPONSE_GRACE_MS
|
|
309
367
|
);
|
|
310
|
-
this.pendingRequests.set(
|
|
311
|
-
|
|
312
|
-
|
|
368
|
+
this.pendingRequests.set(routeId, {
|
|
369
|
+
agentRequestId: id,
|
|
370
|
+
agentWs: ws,
|
|
371
|
+
connectorWs: target.ws,
|
|
372
|
+
timer
|
|
373
|
+
});
|
|
374
|
+
target.ws.send(
|
|
375
|
+
JSON.stringify({
|
|
376
|
+
id: routeId,
|
|
377
|
+
action,
|
|
378
|
+
data,
|
|
379
|
+
browser: message.browser,
|
|
380
|
+
timeoutSec
|
|
381
|
+
}) + "\n"
|
|
313
382
|
);
|
|
314
383
|
}
|
|
315
|
-
handleResponse(ws, response
|
|
384
|
+
handleResponse(ws, response) {
|
|
316
385
|
const conn = this.connections.get(ws);
|
|
317
386
|
if (!conn) return;
|
|
318
|
-
if (conn.
|
|
387
|
+
if (conn.role !== "connector") return;
|
|
319
388
|
const pending = this.pendingRequests.get(response.id);
|
|
320
|
-
if (!pending) return;
|
|
389
|
+
if (!pending || pending.connectorWs !== ws) return;
|
|
321
390
|
this.pendingRequests.delete(response.id);
|
|
322
391
|
clearTimeout(pending.timer);
|
|
323
|
-
if (pending.
|
|
324
|
-
pending.
|
|
392
|
+
if (pending.agentWs.readyState === WebSocket.OPEN) {
|
|
393
|
+
pending.agentWs.send(
|
|
394
|
+
JSON.stringify({ ...response, id: pending.agentRequestId }) + "\n"
|
|
395
|
+
);
|
|
325
396
|
}
|
|
326
397
|
}
|
|
327
|
-
|
|
398
|
+
connectorInfo(ws, conn) {
|
|
399
|
+
const pendingRequests = Array.from(this.pendingRequests.values()).filter(
|
|
400
|
+
(pending) => pending.connectorWs === ws
|
|
401
|
+
).length;
|
|
402
|
+
return {
|
|
403
|
+
connectorId: conn.connector?.connectorId ?? conn.id,
|
|
404
|
+
name: conn.connector?.name ?? `legacy-${conn.id.slice(-8)}`,
|
|
405
|
+
...conn.connector?.version ? { version: conn.connector.version } : {},
|
|
406
|
+
connectedAt: new Date(conn.connectedAt).toISOString(),
|
|
407
|
+
lastHeartbeatAt: new Date(conn.lastHeartbeat).toISOString(),
|
|
408
|
+
pendingRequests,
|
|
409
|
+
...!conn.connector ? { legacy: true } : {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
getConnectorConnections() {
|
|
413
|
+
const connectors = [];
|
|
328
414
|
for (const [ws, conn] of this.connections) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
415
|
+
if (conn.role === "pending" && Date.now() - conn.connectedAt >= LEGACY_CONNECTOR_HELLO_GRACE_MS) {
|
|
416
|
+
conn.role = "connector";
|
|
331
417
|
}
|
|
418
|
+
if (conn.role !== "connector" || ws.readyState !== WebSocket.OPEN) continue;
|
|
419
|
+
connectors.push({ ws, conn, info: this.connectorInfo(ws, conn) });
|
|
332
420
|
}
|
|
333
|
-
return
|
|
421
|
+
return connectors.sort((a, b) => a.info.name.localeCompare(b.info.name));
|
|
422
|
+
}
|
|
423
|
+
getConnectorList() {
|
|
424
|
+
const connectors = this.getConnectorConnections().map(({ info }) => info);
|
|
425
|
+
return { count: connectors.length, connectors };
|
|
426
|
+
}
|
|
427
|
+
resolveConnector(selector) {
|
|
428
|
+
const connectors = this.getConnectorConnections();
|
|
429
|
+
if (connectors.length === 0) return { error: "No local connector available" };
|
|
430
|
+
if (!selector) {
|
|
431
|
+
if (connectors.length === 1) {
|
|
432
|
+
return { ws: connectors[0].ws, info: connectors[0].info };
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
error: `Multiple cloud connectors are connected (${connectors.length}). Run \`mearl connector_list\` and pass --connector <id|name>.`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const selection = resolveCloudConnectorSelector(
|
|
439
|
+
connectors.map(({ info }) => info),
|
|
440
|
+
selector
|
|
441
|
+
);
|
|
442
|
+
if (selection.status === "matched") {
|
|
443
|
+
const matched = connectors.find(({ info }) => info === selection.connector);
|
|
444
|
+
return { ws: matched.ws, info: matched.info };
|
|
445
|
+
}
|
|
446
|
+
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
447
|
+
if (selection.status === "ambiguous") {
|
|
448
|
+
return {
|
|
449
|
+
error: `Cloud connector selector "${selector}" is ambiguous. Available: ${available}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
error: `No cloud connector matches "${selector}". Available: ${available}`
|
|
454
|
+
};
|
|
334
455
|
}
|
|
335
456
|
rejectPendingForWs(ws) {
|
|
336
457
|
this.pendingRequests.forEach((pending, id) => {
|
|
337
|
-
if (pending.ws
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
458
|
+
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
|
|
459
|
+
clearTimeout(pending.timer);
|
|
460
|
+
this.pendingRequests.delete(id);
|
|
461
|
+
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
|
|
462
|
+
pending.agentWs.send(
|
|
463
|
+
JSON.stringify({
|
|
464
|
+
id: pending.agentRequestId,
|
|
465
|
+
success: false,
|
|
466
|
+
error: "Connector connection closed"
|
|
467
|
+
}) + "\n"
|
|
468
|
+
);
|
|
343
469
|
}
|
|
344
470
|
});
|
|
345
471
|
}
|
|
@@ -395,10 +521,17 @@ var CloudServer = class {
|
|
|
395
521
|
writeConfigFile(info) {
|
|
396
522
|
try {
|
|
397
523
|
if (!existsSync(CONFIG_DIR)) {
|
|
398
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
524
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
399
525
|
}
|
|
400
526
|
const config = { server: info.localUrl, ...info };
|
|
401
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2),
|
|
527
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
528
|
+
encoding: "utf-8",
|
|
529
|
+
mode: 384
|
|
530
|
+
});
|
|
531
|
+
if (process.platform !== "win32") {
|
|
532
|
+
chmodSync(CONFIG_DIR, 448);
|
|
533
|
+
chmodSync(CONFIG_FILE, 384);
|
|
534
|
+
}
|
|
402
535
|
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);
|
|
403
536
|
} catch (error) {
|
|
404
537
|
console.error("[CloudServer] Failed to write config file:", error);
|
|
@@ -406,15 +539,13 @@ var CloudServer = class {
|
|
|
406
539
|
}
|
|
407
540
|
getStats() {
|
|
408
541
|
let agents = 0;
|
|
409
|
-
let connectors = 0;
|
|
410
542
|
for (const conn of this.connections.values()) {
|
|
411
|
-
if (conn.
|
|
412
|
-
else connectors++;
|
|
543
|
+
if (conn.role === "agent") agents++;
|
|
413
544
|
}
|
|
414
545
|
return {
|
|
415
546
|
connections: this.connections.size,
|
|
416
547
|
agents,
|
|
417
|
-
connectors,
|
|
548
|
+
connectors: this.getConnectorConnections().length,
|
|
418
549
|
pendingRequests: this.pendingRequests.size
|
|
419
550
|
};
|
|
420
551
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/cloud-server",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Cloud WebSocket server for Mearl — bridges cloud agents to local connectors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"node-fetch": "^3.3.2",
|
|
31
31
|
"ws": "^8.18.0",
|
|
32
|
-
"@mearl/
|
|
33
|
-
"@mearl/
|
|
34
|
-
"@mearl/daemon-core": "2.0
|
|
32
|
+
"@mearl/cloud-types": "2.2.0",
|
|
33
|
+
"@mearl/client": "2.2.0",
|
|
34
|
+
"@mearl/daemon-core": "2.2.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^24.9.1",
|