Sign In

@mearl/cloud-server

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mearl/cloud-server - npm Package Compare versions

Comparing version
2.0.2
to
2.1.0
+42
-24
dist/cli.js

@@ -10,3 +10,4 @@ #!/usr/bin/env node

import { createServer as createHttpServer } from "node:http";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { networkInterfaces } from "node:os";

@@ -79,13 +80,8 @@

generateRandomToken() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let token = "";
for (let i = 0; i < 32; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
return randomBytes(24).toString("base64url");
}
async start() {
const token = this.getToken();
await this.createListener(this.options.agentPort, "127.0.0.1", true);
await this.createListener(this.options.port, "0.0.0.0", false);
const token = this.getToken();
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;

@@ -109,2 +105,7 @@ let publicWsUrl;

}
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
console.warn(
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
);
}
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;

@@ -182,2 +183,5 @@ console.log(

this.connections.clear();
for (const pending of this.pendingRequests.values()) {
clearTimeout(pending.timer);
}
this.pendingRequests.clear();

@@ -223,3 +227,5 @@ this.agentWss?.close();

lastHeartbeat: Date.now(),
isAgent: false,
// The loopback listener is agent-only. Marking it immediately prevents an
// idle local client from being mistaken for a connector during reconnects.
isAgent: fromLocal,
fromLocal,

@@ -310,2 +316,6 @@ messageBuffer: ""

}
if (this.pendingRequests.has(id)) {
ws.send(JSON.stringify({ id, success: false, error: "Duplicate request id" }) + "\n");
return;
}
const requestedTimeoutSec = Number(message.timeoutSec);

@@ -322,3 +332,3 @@ const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);

);
this.pendingRequests.set(id, { ws, timer });
this.pendingRequests.set(id, { agentWs: ws, connectorWs: targetWs, timer });
targetWs.send(

@@ -331,9 +341,9 @@ JSON.stringify({ id, action, data, browser: message.browser, timeoutSec }) + "\n"

if (!conn) return;
if (conn.isAgent) return;
if (conn.fromLocal || conn.isAgent) return;
const pending = this.pendingRequests.get(response.id);
if (!pending) return;
if (!pending || pending.connectorWs !== ws) return;
this.pendingRequests.delete(response.id);
clearTimeout(pending.timer);
if (pending.ws.readyState === WebSocket.OPEN) {
pending.ws.send(JSON.stringify(response) + "\n");
if (pending.agentWs.readyState === WebSocket.OPEN) {
pending.agentWs.send(JSON.stringify(response) + "\n");
}

@@ -343,3 +353,3 @@ }

for (const [ws, conn] of this.connections) {
if (!conn.isAgent && ws.readyState === WebSocket.OPEN) {
if (!conn.fromLocal && !conn.isAgent && ws.readyState === WebSocket.OPEN) {
return ws;

@@ -352,8 +362,9 @@ }

this.pendingRequests.forEach((pending, id) => {
if (pending.ws === ws) {
clearTimeout(pending.timer);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ id, success: false, error: "Connection closed" }) + "\n");
}
this.pendingRequests.delete(id);
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
clearTimeout(pending.timer);
this.pendingRequests.delete(id);
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
pending.agentWs.send(
JSON.stringify({ id, success: false, error: "Connector connection closed" }) + "\n"
);
}

@@ -414,6 +425,13 @@ });

if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
}
const config = { server: info.localUrl, ...info };
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
encoding: "utf-8",
mode: 384
});
if (process.platform !== "win32") {
chmodSync(CONFIG_DIR, 448);
chmodSync(CONFIG_FILE, 384);
}
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);

@@ -503,3 +521,3 @@ } catch (error) {

// src/cli.ts
var CLOUD_SERVER_VERSION = true ? "2.0.2" : "unknown";
var CLOUD_SERVER_VERSION = true ? "2.1.0" : "unknown";
var argv = process.argv.slice(2);

@@ -506,0 +524,0 @@ if (argv.includes("--version") || argv.includes("-v")) {

// src/index.ts
import WebSocket, { WebSocketServer } from "ws";
import { createServer as createHttpServer } from "node:http";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { networkInterfaces } from "node:os";

@@ -72,13 +73,8 @@

generateRandomToken() {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let token = "";
for (let i = 0; i < 32; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
return randomBytes(24).toString("base64url");
}
async start() {
const token = this.getToken();
await this.createListener(this.options.agentPort, "127.0.0.1", true);
await this.createListener(this.options.port, "0.0.0.0", false);
const token = this.getToken();
const localWsUrl = `ws://localhost:${this.options.agentPort}${this.options.path}?token=${token}`;

@@ -102,2 +98,7 @@ let publicWsUrl;

}
if (publicWsUrl.startsWith("ws://") && !publicWsUrl.startsWith("ws://localhost")) {
console.warn(
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
);
}
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;

@@ -175,2 +176,5 @@ console.log(

this.connections.clear();
for (const pending of this.pendingRequests.values()) {
clearTimeout(pending.timer);
}
this.pendingRequests.clear();

@@ -216,3 +220,5 @@ this.agentWss?.close();

lastHeartbeat: Date.now(),
isAgent: false,
// The loopback listener is agent-only. Marking it immediately prevents an
// idle local client from being mistaken for a connector during reconnects.
isAgent: fromLocal,
fromLocal,

@@ -303,2 +309,6 @@ messageBuffer: ""

}
if (this.pendingRequests.has(id)) {
ws.send(JSON.stringify({ id, success: false, error: "Duplicate request id" }) + "\n");
return;
}
const requestedTimeoutSec = Number(message.timeoutSec);

@@ -315,3 +325,3 @@ const timeoutSec = Number.isFinite(requestedTimeoutSec) && requestedTimeoutSec > 0 ? Math.min(requestedTimeoutSec, 3600) : resolveActionTimeoutSec(action, data, this.options.requestTimeout);

);
this.pendingRequests.set(id, { ws, timer });
this.pendingRequests.set(id, { agentWs: ws, connectorWs: targetWs, timer });
targetWs.send(

@@ -324,9 +334,9 @@ JSON.stringify({ id, action, data, browser: message.browser, timeoutSec }) + "\n"

if (!conn) return;
if (conn.isAgent) return;
if (conn.fromLocal || conn.isAgent) return;
const pending = this.pendingRequests.get(response.id);
if (!pending) return;
if (!pending || pending.connectorWs !== ws) return;
this.pendingRequests.delete(response.id);
clearTimeout(pending.timer);
if (pending.ws.readyState === WebSocket.OPEN) {
pending.ws.send(JSON.stringify(response) + "\n");
if (pending.agentWs.readyState === WebSocket.OPEN) {
pending.agentWs.send(JSON.stringify(response) + "\n");
}

@@ -336,3 +346,3 @@ }

for (const [ws, conn] of this.connections) {
if (!conn.isAgent && ws.readyState === WebSocket.OPEN) {
if (!conn.fromLocal && !conn.isAgent && ws.readyState === WebSocket.OPEN) {
return ws;

@@ -345,8 +355,9 @@ }

this.pendingRequests.forEach((pending, id) => {
if (pending.ws === ws) {
clearTimeout(pending.timer);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ id, success: false, error: "Connection closed" }) + "\n");
}
this.pendingRequests.delete(id);
if (pending.agentWs !== ws && pending.connectorWs !== ws) return;
clearTimeout(pending.timer);
this.pendingRequests.delete(id);
if (pending.connectorWs === ws && pending.agentWs !== ws && pending.agentWs.readyState === WebSocket.OPEN) {
pending.agentWs.send(
JSON.stringify({ id, success: false, error: "Connector connection closed" }) + "\n"
);
}

@@ -407,6 +418,13 @@ });

if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
}
const config = { server: info.localUrl, ...info };
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
encoding: "utf-8",
mode: 384
});
if (process.platform !== "win32") {
chmodSync(CONFIG_DIR, 448);
chmodSync(CONFIG_FILE, 384);
}
console.log(`[CloudServer] Config written to: ${CONFIG_FILE}`);

@@ -413,0 +431,0 @@ } catch (error) {

{
"name": "@mearl/cloud-server",
"version": "2.0.2",
"version": "2.1.0",
"description": "Cloud WebSocket server for Mearl — bridges cloud agents to local connectors",

@@ -32,5 +32,5 @@ "type": "module",

"ws": "^8.18.0",
"@mearl/client": "2.0.2",
"@mearl/cloud-types": "2.0.2",
"@mearl/daemon-core": "2.0.2"
"@mearl/client": "2.1.0",
"@mearl/cloud-types": "2.1.0",
"@mearl/daemon-core": "2.1.0"
},

@@ -37,0 +37,0 @@ "devDependencies": {