@mearl/cloud-server 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +42 -24
- package/dist/index.js +41 -23
- package/package.json +4 -4
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
|
|
@@ -75,17 +76,12 @@ var CloudServer = class {
|
|
|
75
76
|
return this.options.token;
|
|
76
77
|
}
|
|
77
78
|
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;
|
|
79
|
+
return randomBytes(24).toString("base64url");
|
|
84
80
|
}
|
|
85
81
|
async start() {
|
|
82
|
+
const token = this.getToken();
|
|
86
83
|
await this.createListener(this.options.agentPort, "127.0.0.1", true);
|
|
87
84
|
await this.createListener(this.options.port, "0.0.0.0", false);
|
|
88
|
-
const token = this.getToken();
|
|
89
85
|
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;
|
|
90
86
|
let publicWsUrl;
|
|
91
87
|
const sandboxUrl = await this.getSandboxPublicUrl(this.options.port);
|
|
@@ -104,6 +100,11 @@ var CloudServer = class {
|
|
|
104
100
|
const publicHost = process.env.CLOUD_SERVER_HOST || this.getPublicIP();
|
|
105
101
|
publicWsUrl = `ws://${publicHost}:${this.options.port}${this.options.path}?token=${token}`;
|
|
106
102
|
}
|
|
103
|
+
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
|
|
104
|
+
console.warn(
|
|
105
|
+
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
107
108
|
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
108
109
|
console.log(
|
|
109
110
|
`[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 +177,9 @@ var CloudServer = class {
|
|
|
176
177
|
ws.close();
|
|
177
178
|
}
|
|
178
179
|
this.connections.clear();
|
|
180
|
+
for (const pending of this.pendingRequests.values()) {
|
|
181
|
+
clearTimeout(pending.timer);
|
|
182
|
+
}
|
|
179
183
|
this.pendingRequests.clear();
|
|
180
184
|
this.agentWss?.close();
|
|
181
185
|
this.connectorWss?.close();
|
|
@@ -216,7 +220,9 @@ var CloudServer = class {
|
|
|
216
220
|
ws,
|
|
217
221
|
id: connId,
|
|
218
222
|
lastHeartbeat: Date.now(),
|
|
219
|
-
|
|
223
|
+
// The loopback listener is agent-only. Marking it immediately prevents an
|
|
224
|
+
// idle local client from being mistaken for a connector during reconnects.
|
|
225
|
+
isAgent: fromLocal,
|
|
220
226
|
fromLocal,
|
|
221
227
|
messageBuffer: ""
|
|
222
228
|
};
|
|
@@ -302,6 +308,10 @@ var CloudServer = class {
|
|
|
302
308
|
ws.send(JSON.stringify({ id, success: false, error: "No local connector available" }) + "\n");
|
|
303
309
|
return;
|
|
304
310
|
}
|
|
311
|
+
if (this.pendingRequests.has(id)) {
|
|
312
|
+
ws.send(JSON.stringify({ id, success: false, error: "Duplicate request id" }) + "\n");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
305
315
|
const requestedTimeoutSec = Number(message.timeoutSec);
|
|
306
316
|
const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);
|
|
307
317
|
const timer = setTimeout(
|
|
@@ -313,7 +323,7 @@ var CloudServer = class {
|
|
|
313
323
|
},
|
|
314
324
|
timeoutSec * 1e3 + RESPONSE_GRACE_MS
|
|
315
325
|
);
|
|
316
|
-
this.pendingRequests.set(id, { ws, timer });
|
|
326
|
+
this.pendingRequests.set(id, { agentWs: ws, connectorWs: targetWs, timer });
|
|
317
327
|
targetWs.send(
|
|
318
328
|
JSON.stringify({ id, action, data, browser: message.browser, timeoutSec }) + "\n"
|
|
319
329
|
);
|
|
@@ -321,18 +331,18 @@ var CloudServer = class {
|
|
|
321
331
|
handleResponse(ws, response, _connId) {
|
|
322
332
|
const conn = this.connections.get(ws);
|
|
323
333
|
if (!conn) return;
|
|
324
|
-
if (conn.isAgent) return;
|
|
334
|
+
if (conn.fromLocal || conn.isAgent) return;
|
|
325
335
|
const pending = this.pendingRequests.get(response.id);
|
|
326
|
-
if (!pending) return;
|
|
336
|
+
if (!pending || pending.connectorWs !== ws) return;
|
|
327
337
|
this.pendingRequests.delete(response.id);
|
|
328
338
|
clearTimeout(pending.timer);
|
|
329
|
-
if (pending.
|
|
330
|
-
pending.
|
|
339
|
+
if (pending.agentWs.readyState === WebSocket.OPEN) {
|
|
340
|
+
pending.agentWs.send(JSON.stringify(response) + "\n");
|
|
331
341
|
}
|
|
332
342
|
}
|
|
333
343
|
findConnector() {
|
|
334
344
|
for (const [ws, conn] of this.connections) {
|
|
335
|
-
if (!conn.isAgent && ws.readyState === WebSocket.OPEN) {
|
|
345
|
+
if (!conn.fromLocal && !conn.isAgent && ws.readyState === WebSocket.OPEN) {
|
|
336
346
|
return ws;
|
|
337
347
|
}
|
|
338
348
|
}
|
|
@@ -340,12 +350,13 @@ var CloudServer = class {
|
|
|
340
350
|
}
|
|
341
351
|
rejectPendingForWs(ws) {
|
|
342
352
|
this.pendingRequests.forEach((pending, id) => {
|
|
343
|
-
if (pending.ws
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
353
|
+
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
|
|
354
|
+
clearTimeout(pending.timer);
|
|
355
|
+
this.pendingRequests.delete(id);
|
|
356
|
+
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
|
|
357
|
+
pending.agentWs.send(
|
|
358
|
+
JSON.stringify({ id, success: false, error: "Connector connection closed" }) + "\n"
|
|
359
|
+
);
|
|
349
360
|
}
|
|
350
361
|
});
|
|
351
362
|
}
|
|
@@ -401,10 +412,17 @@ var CloudServer = class {
|
|
|
401
412
|
writeConfigFile(info) {
|
|
402
413
|
try {
|
|
403
414
|
if (!existsSync(CONFIG_DIR)) {
|
|
404
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
415
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
405
416
|
}
|
|
406
417
|
const config = { server: info.localUrl, ...info };
|
|
407
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2),
|
|
418
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
419
|
+
encoding: "utf-8",
|
|
420
|
+
mode: 384
|
|
421
|
+
});
|
|
422
|
+
if (process.platform !== "win32") {
|
|
423
|
+
chmodSync(CONFIG_DIR, 448);
|
|
424
|
+
chmodSync(CONFIG_FILE, 384);
|
|
425
|
+
}
|
|
408
426
|
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);
|
|
409
427
|
} catch (error) {
|
|
410
428
|
console.error("[CloudServer] Failed to write config file:", error);
|
|
@@ -489,7 +507,7 @@ async function runForeground(options) {
|
|
|
489
507
|
}
|
|
490
508
|
|
|
491
509
|
// src/cli.ts
|
|
492
|
-
var CLOUD_SERVER_VERSION = true ? "2.0
|
|
510
|
+
var CLOUD_SERVER_VERSION = true ? "2.1.0" : "unknown";
|
|
493
511
|
var argv = process.argv.slice(2);
|
|
494
512
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
495
513
|
console.log(CLOUD_SERVER_VERSION);
|
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
|
|
@@ -69,17 +70,12 @@ var CloudServer = class {
|
|
|
69
70
|
return this.options.token;
|
|
70
71
|
}
|
|
71
72
|
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;
|
|
73
|
+
return randomBytes(24).toString("base64url");
|
|
78
74
|
}
|
|
79
75
|
async start() {
|
|
76
|
+
const token = this.getToken();
|
|
80
77
|
await this.createListener(this.options.agentPort, "127.0.0.1", true);
|
|
81
78
|
await this.createListener(this.options.port, "0.0.0.0", false);
|
|
82
|
-
const token = this.getToken();
|
|
83
79
|
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;
|
|
84
80
|
let publicWsUrl;
|
|
85
81
|
const sandboxUrl = await this.getSandboxPublicUrl(this.options.port);
|
|
@@ -98,6 +94,11 @@ var CloudServer = class {
|
|
|
98
94
|
const publicHost = process.env.CLOUD_SERVER_HOST || this.getPublicIP();
|
|
99
95
|
publicWsUrl = `ws://${publicHost}:${this.options.port}${this.options.path}?token=${token}`;
|
|
100
96
|
}
|
|
97
|
+
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
|
|
98
|
+
console.warn(
|
|
99
|
+
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
101
102
|
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
102
103
|
console.log(
|
|
103
104
|
`[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 +171,9 @@ var CloudServer = class {
|
|
|
170
171
|
ws.close();
|
|
171
172
|
}
|
|
172
173
|
this.connections.clear();
|
|
174
|
+
for (const pending of this.pendingRequests.values()) {
|
|
175
|
+
clearTimeout(pending.timer);
|
|
176
|
+
}
|
|
173
177
|
this.pendingRequests.clear();
|
|
174
178
|
this.agentWss?.close();
|
|
175
179
|
this.connectorWss?.close();
|
|
@@ -210,7 +214,9 @@ var CloudServer = class {
|
|
|
210
214
|
ws,
|
|
211
215
|
id: connId,
|
|
212
216
|
lastHeartbeat: Date.now(),
|
|
213
|
-
|
|
217
|
+
// The loopback listener is agent-only. Marking it immediately prevents an
|
|
218
|
+
// idle local client from being mistaken for a connector during reconnects.
|
|
219
|
+
isAgent: fromLocal,
|
|
214
220
|
fromLocal,
|
|
215
221
|
messageBuffer: ""
|
|
216
222
|
};
|
|
@@ -296,6 +302,10 @@ var CloudServer = class {
|
|
|
296
302
|
ws.send(JSON.stringify({ id, success: false, error: "No local connector available" }) + "\n");
|
|
297
303
|
return;
|
|
298
304
|
}
|
|
305
|
+
if (this.pendingRequests.has(id)) {
|
|
306
|
+
ws.send(JSON.stringify({ id, success: false, error: "Duplicate request id" }) + "\n");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
299
309
|
const requestedTimeoutSec = Number(message.timeoutSec);
|
|
300
310
|
const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);
|
|
301
311
|
const timer = setTimeout(
|
|
@@ -307,7 +317,7 @@ var CloudServer = class {
|
|
|
307
317
|
},
|
|
308
318
|
timeoutSec * 1e3 + RESPONSE_GRACE_MS
|
|
309
319
|
);
|
|
310
|
-
this.pendingRequests.set(id, { ws, timer });
|
|
320
|
+
this.pendingRequests.set(id, { agentWs: ws, connectorWs: targetWs, timer });
|
|
311
321
|
targetWs.send(
|
|
312
322
|
JSON.stringify({ id, action, data, browser: message.browser, timeoutSec }) + "\n"
|
|
313
323
|
);
|
|
@@ -315,18 +325,18 @@ var CloudServer = class {
|
|
|
315
325
|
handleResponse(ws, response, _connId) {
|
|
316
326
|
const conn = this.connections.get(ws);
|
|
317
327
|
if (!conn) return;
|
|
318
|
-
if (conn.isAgent) return;
|
|
328
|
+
if (conn.fromLocal || conn.isAgent) return;
|
|
319
329
|
const pending = this.pendingRequests.get(response.id);
|
|
320
|
-
if (!pending) return;
|
|
330
|
+
if (!pending || pending.connectorWs !== ws) return;
|
|
321
331
|
this.pendingRequests.delete(response.id);
|
|
322
332
|
clearTimeout(pending.timer);
|
|
323
|
-
if (pending.
|
|
324
|
-
pending.
|
|
333
|
+
if (pending.agentWs.readyState === WebSocket.OPEN) {
|
|
334
|
+
pending.agentWs.send(JSON.stringify(response) + "\n");
|
|
325
335
|
}
|
|
326
336
|
}
|
|
327
337
|
findConnector() {
|
|
328
338
|
for (const [ws, conn] of this.connections) {
|
|
329
|
-
if (!conn.isAgent && ws.readyState === WebSocket.OPEN) {
|
|
339
|
+
if (!conn.fromLocal && !conn.isAgent && ws.readyState === WebSocket.OPEN) {
|
|
330
340
|
return ws;
|
|
331
341
|
}
|
|
332
342
|
}
|
|
@@ -334,12 +344,13 @@ var CloudServer = class {
|
|
|
334
344
|
}
|
|
335
345
|
rejectPendingForWs(ws) {
|
|
336
346
|
this.pendingRequests.forEach((pending, id) => {
|
|
337
|
-
if (pending.ws
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
|
|
348
|
+
clearTimeout(pending.timer);
|
|
349
|
+
this.pendingRequests.delete(id);
|
|
350
|
+
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
|
|
351
|
+
pending.agentWs.send(
|
|
352
|
+
JSON.stringify({ id, success: false, error: "Connector connection closed" }) + "\n"
|
|
353
|
+
);
|
|
343
354
|
}
|
|
344
355
|
});
|
|
345
356
|
}
|
|
@@ -395,10 +406,17 @@ var CloudServer = class {
|
|
|
395
406
|
writeConfigFile(info) {
|
|
396
407
|
try {
|
|
397
408
|
if (!existsSync(CONFIG_DIR)) {
|
|
398
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
409
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
399
410
|
}
|
|
400
411
|
const config = { server: info.localUrl, ...info };
|
|
401
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2),
|
|
412
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
413
|
+
encoding: "utf-8",
|
|
414
|
+
mode: 384
|
|
415
|
+
});
|
|
416
|
+
if (process.platform !== "win32") {
|
|
417
|
+
chmodSync(CONFIG_DIR, 448);
|
|
418
|
+
chmodSync(CONFIG_FILE, 384);
|
|
419
|
+
}
|
|
402
420
|
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);
|
|
403
421
|
} catch (error) {
|
|
404
422
|
console.error("[CloudServer] Failed to write config file:", error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/cloud-server",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.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/client": "2.0
|
|
33
|
-
"@mearl/cloud-types": "2.0
|
|
34
|
-
"@mearl/daemon-core": "2.0
|
|
32
|
+
"@mearl/client": "2.1.0",
|
|
33
|
+
"@mearl/cloud-types": "2.1.0",
|
|
34
|
+
"@mearl/daemon-core": "2.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^24.9.1",
|