@mclawnet/agent 0.1.0 → 0.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/dist/index.js +148 -13
- package/dist/start.d.ts +28 -3
- package/dist/start.js +148 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/hub-connection.ts
|
|
2
|
+
import { hostname as osHostname } from "os";
|
|
2
3
|
import WebSocket from "ws";
|
|
3
4
|
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
4
5
|
var DEFAULT_RECONNECT_MS = 1e3;
|
|
@@ -9,22 +10,32 @@ var HubConnection = class {
|
|
|
9
10
|
reconnectTimer = null;
|
|
10
11
|
reconnectDelay;
|
|
11
12
|
destroyed = false;
|
|
13
|
+
authState = "pending";
|
|
14
|
+
proxySessions = /* @__PURE__ */ new Map();
|
|
12
15
|
hubUrl;
|
|
13
16
|
token;
|
|
17
|
+
hostname;
|
|
18
|
+
gatewayPort;
|
|
19
|
+
gatewayToken;
|
|
14
20
|
heartbeatInterval;
|
|
15
21
|
maxReconnectDelay;
|
|
22
|
+
/** Agent ID assigned by Hub after successful auth */
|
|
23
|
+
agentId = null;
|
|
16
24
|
onMessage;
|
|
17
|
-
|
|
25
|
+
onConnectCb;
|
|
18
26
|
onDisconnect;
|
|
19
27
|
onError;
|
|
20
28
|
constructor(opts) {
|
|
21
29
|
this.hubUrl = opts.hubUrl;
|
|
22
30
|
this.token = opts.token;
|
|
31
|
+
this.hostname = opts.hostname ?? osHostname();
|
|
32
|
+
this.gatewayPort = opts.gatewayPort ?? 18789;
|
|
33
|
+
this.gatewayToken = opts.gatewayToken ?? "";
|
|
23
34
|
this.heartbeatInterval = opts.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
24
35
|
this.reconnectDelay = opts.reconnectDelay ?? DEFAULT_RECONNECT_MS;
|
|
25
36
|
this.maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_MS;
|
|
26
37
|
this.onMessage = opts.onMessage;
|
|
27
|
-
this.
|
|
38
|
+
this.onConnectCb = opts.onConnect;
|
|
28
39
|
this.onDisconnect = opts.onDisconnect;
|
|
29
40
|
this.onError = opts.onError;
|
|
30
41
|
}
|
|
@@ -33,31 +44,53 @@ var HubConnection = class {
|
|
|
33
44
|
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
34
45
|
}
|
|
35
46
|
get isConnected() {
|
|
36
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
47
|
+
return this.ws?.readyState === WebSocket.OPEN && this.authState === "authenticated";
|
|
37
48
|
}
|
|
38
|
-
/** Open the connection to the hub */
|
|
49
|
+
/** Open the connection to the hub (no token in URL) */
|
|
39
50
|
connect() {
|
|
40
51
|
if (this.destroyed) return;
|
|
41
52
|
this.cleanup();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.ws = new WebSocket(url.toString());
|
|
53
|
+
this.authState = "pending";
|
|
54
|
+
this.ws = new WebSocket(this.hubUrl);
|
|
45
55
|
this.ws.on("open", () => {
|
|
46
56
|
this.reconnectDelay = DEFAULT_RECONNECT_MS;
|
|
47
|
-
this.startHeartbeat();
|
|
48
|
-
this.onConnect?.();
|
|
49
57
|
});
|
|
50
58
|
this.ws.on("message", (raw) => {
|
|
59
|
+
let data;
|
|
51
60
|
try {
|
|
52
|
-
|
|
53
|
-
this.onMessage?.(data);
|
|
61
|
+
data = JSON.parse(raw.toString());
|
|
54
62
|
} catch {
|
|
55
|
-
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (this.authState === "pending" && data.type === "auth_required") {
|
|
66
|
+
this.authState = "authenticating";
|
|
67
|
+
this.ws.send(JSON.stringify({
|
|
68
|
+
type: "auth",
|
|
69
|
+
token: this.token,
|
|
70
|
+
hostname: this.hostname
|
|
71
|
+
}));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (this.authState === "authenticating" && data.type === "registered") {
|
|
75
|
+
this.authState = "authenticated";
|
|
76
|
+
this.agentId = data.agentId ?? null;
|
|
77
|
+
this.startHeartbeat();
|
|
78
|
+
this.onConnectCb?.(this.agentId);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.authState === "authenticated") {
|
|
82
|
+
if (this.handleProxyMessage(data)) return;
|
|
83
|
+
this.onMessage?.(data);
|
|
56
84
|
}
|
|
57
85
|
});
|
|
58
86
|
this.ws.on("close", (code, reason) => {
|
|
59
87
|
this.stopHeartbeat();
|
|
88
|
+
this.authState = "pending";
|
|
60
89
|
this.onDisconnect?.(code, reason.toString());
|
|
90
|
+
if (code === 4002) {
|
|
91
|
+
console.error("[clawnet] Auth failed \u2014 not reconnecting. Check your token.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
61
94
|
this.scheduleReconnect();
|
|
62
95
|
});
|
|
63
96
|
this.ws.on("error", (err) => {
|
|
@@ -66,7 +99,7 @@ var HubConnection = class {
|
|
|
66
99
|
}
|
|
67
100
|
/** Send a JSON message to the hub */
|
|
68
101
|
send(data) {
|
|
69
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
102
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.authState !== "authenticated") return false;
|
|
70
103
|
this.ws.send(JSON.stringify(data));
|
|
71
104
|
return true;
|
|
72
105
|
}
|
|
@@ -96,6 +129,7 @@ var HubConnection = class {
|
|
|
96
129
|
}
|
|
97
130
|
cleanup() {
|
|
98
131
|
this.stopHeartbeat();
|
|
132
|
+
this.closeAllProxySessions();
|
|
99
133
|
if (this.reconnectTimer) {
|
|
100
134
|
clearTimeout(this.reconnectTimer);
|
|
101
135
|
this.reconnectTimer = null;
|
|
@@ -108,6 +142,96 @@ var HubConnection = class {
|
|
|
108
142
|
this.ws = null;
|
|
109
143
|
}
|
|
110
144
|
}
|
|
145
|
+
// ── Proxy session management ──────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Handle proxy protocol messages from the Hub.
|
|
148
|
+
* Returns true if the message was handled.
|
|
149
|
+
*/
|
|
150
|
+
handleProxyMessage(msg) {
|
|
151
|
+
if (msg.type === "proxy.open" && msg.sessionId) {
|
|
152
|
+
this.openLocalGateway(msg.sessionId, msg.gatewayPort ?? this.gatewayPort);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (msg.type === "proxy.data" && msg.sessionId) {
|
|
156
|
+
const session = this.proxySessions.get(msg.sessionId);
|
|
157
|
+
if (session && session.localWs.readyState === WebSocket.OPEN && msg.data) {
|
|
158
|
+
const patched = this.patchBrowserConnect(msg.data, session);
|
|
159
|
+
session.localWs.send(patched);
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (msg.type === "proxy.close" && msg.sessionId) {
|
|
164
|
+
this.closeProxySession(msg.sessionId);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
openLocalGateway(sessionId, port) {
|
|
170
|
+
const localWs = new WebSocket(`ws://127.0.0.1:${port}/`, {
|
|
171
|
+
headers: { Origin: `http://127.0.0.1:${port}` }
|
|
172
|
+
});
|
|
173
|
+
const session = {
|
|
174
|
+
sessionId,
|
|
175
|
+
localWs,
|
|
176
|
+
gatewayNonce: null
|
|
177
|
+
};
|
|
178
|
+
this.proxySessions.set(sessionId, session);
|
|
179
|
+
localWs.on("open", () => {
|
|
180
|
+
this.send({ type: "proxy.opened", sessionId });
|
|
181
|
+
});
|
|
182
|
+
localWs.on("message", (raw) => {
|
|
183
|
+
const text = raw.toString();
|
|
184
|
+
try {
|
|
185
|
+
const frame = JSON.parse(text);
|
|
186
|
+
if (frame.type === "evt" && frame.event === "connect.challenge" && frame.payload?.nonce) {
|
|
187
|
+
session.gatewayNonce = frame.payload.nonce;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
this.send({ type: "proxy.data", sessionId, data: text });
|
|
192
|
+
});
|
|
193
|
+
localWs.on("close", () => {
|
|
194
|
+
this.proxySessions.delete(sessionId);
|
|
195
|
+
this.send({ type: "proxy.close", sessionId });
|
|
196
|
+
});
|
|
197
|
+
localWs.on("error", () => {
|
|
198
|
+
this.proxySessions.delete(sessionId);
|
|
199
|
+
this.send({ type: "proxy.close", sessionId });
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Intercept browser's "connect" request and patch it for the local gateway:
|
|
204
|
+
* - Inject gateway auth token
|
|
205
|
+
* - Remove `device` object (browser's device identity/signature doesn't apply
|
|
206
|
+
* through the proxy; the gateway will use token-only auth)
|
|
207
|
+
*/
|
|
208
|
+
patchBrowserConnect(raw, session) {
|
|
209
|
+
if (!this.gatewayToken) return raw;
|
|
210
|
+
try {
|
|
211
|
+
const frame = JSON.parse(raw);
|
|
212
|
+
if (frame.type === "req" && frame.method === "connect" && frame.params) {
|
|
213
|
+
if (!frame.params.auth) frame.params.auth = {};
|
|
214
|
+
frame.params.auth.token = this.gatewayToken;
|
|
215
|
+
delete frame.params.device;
|
|
216
|
+
return JSON.stringify(frame);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
return raw;
|
|
221
|
+
}
|
|
222
|
+
closeProxySession(sessionId) {
|
|
223
|
+
const session = this.proxySessions.get(sessionId);
|
|
224
|
+
if (!session) return;
|
|
225
|
+
this.proxySessions.delete(sessionId);
|
|
226
|
+
if (session.localWs.readyState === WebSocket.OPEN || session.localWs.readyState === WebSocket.CONNECTING) {
|
|
227
|
+
session.localWs.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
closeAllProxySessions() {
|
|
231
|
+
for (const [sessionId] of this.proxySessions) {
|
|
232
|
+
this.closeProxySession(sessionId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
111
235
|
};
|
|
112
236
|
|
|
113
237
|
// src/config.ts
|
|
@@ -272,6 +396,15 @@ function ensureGatewayToken() {
|
|
|
272
396
|
config.gateway.mode = "local";
|
|
273
397
|
dirty = true;
|
|
274
398
|
}
|
|
399
|
+
if (!config.gateway.controlUi) config.gateway.controlUi = {};
|
|
400
|
+
if (!config.gateway.controlUi.allowedOrigins) {
|
|
401
|
+
config.gateway.controlUi.allowedOrigins = ["*"];
|
|
402
|
+
dirty = true;
|
|
403
|
+
}
|
|
404
|
+
if (config.gateway.controlUi.allowInsecureAuth === void 0) {
|
|
405
|
+
config.gateway.controlUi.allowInsecureAuth = true;
|
|
406
|
+
dirty = true;
|
|
407
|
+
}
|
|
275
408
|
if (dirty) {
|
|
276
409
|
writeFileSync2(file, JSON.stringify(config, null, 2) + "\n");
|
|
277
410
|
}
|
|
@@ -548,6 +681,8 @@ async function startAgent(opts) {
|
|
|
548
681
|
const hubConnection = new HubConnection({
|
|
549
682
|
hubUrl: config.hubUrl,
|
|
550
683
|
token: config.token,
|
|
684
|
+
gatewayPort: config.port,
|
|
685
|
+
gatewayToken,
|
|
551
686
|
onConnect: () => {
|
|
552
687
|
console.log("[clawnet] Connected to Hub");
|
|
553
688
|
},
|
package/dist/start.d.ts
CHANGED
|
@@ -3,11 +3,14 @@ import { ChildProcess } from 'node:child_process';
|
|
|
3
3
|
interface HubConnectionOptions {
|
|
4
4
|
hubUrl: string;
|
|
5
5
|
token: string;
|
|
6
|
+
hostname?: string;
|
|
7
|
+
gatewayPort?: number;
|
|
8
|
+
gatewayToken?: string;
|
|
6
9
|
heartbeatInterval?: number;
|
|
7
10
|
reconnectDelay?: number;
|
|
8
11
|
maxReconnectDelay?: number;
|
|
9
12
|
onMessage?: (data: unknown) => void;
|
|
10
|
-
onConnect?: () => void;
|
|
13
|
+
onConnect?: (agentId: string) => void;
|
|
11
14
|
onDisconnect?: (code: number, reason: string) => void;
|
|
12
15
|
onError?: (err: Error) => void;
|
|
13
16
|
}
|
|
@@ -17,19 +20,26 @@ declare class HubConnection {
|
|
|
17
20
|
private reconnectTimer;
|
|
18
21
|
private reconnectDelay;
|
|
19
22
|
private destroyed;
|
|
23
|
+
private authState;
|
|
24
|
+
private proxySessions;
|
|
20
25
|
readonly hubUrl: string;
|
|
21
26
|
readonly token: string;
|
|
27
|
+
readonly hostname: string;
|
|
28
|
+
readonly gatewayPort: number;
|
|
29
|
+
readonly gatewayToken: string;
|
|
22
30
|
readonly heartbeatInterval: number;
|
|
23
31
|
readonly maxReconnectDelay: number;
|
|
32
|
+
/** Agent ID assigned by Hub after successful auth */
|
|
33
|
+
agentId: string | null;
|
|
24
34
|
private onMessage?;
|
|
25
|
-
private
|
|
35
|
+
private onConnectCb?;
|
|
26
36
|
private onDisconnect?;
|
|
27
37
|
private onError?;
|
|
28
38
|
constructor(opts: HubConnectionOptions);
|
|
29
39
|
/** Current WebSocket readyState (or CLOSED if no socket) */
|
|
30
40
|
get readyState(): number;
|
|
31
41
|
get isConnected(): boolean;
|
|
32
|
-
/** Open the connection to the hub */
|
|
42
|
+
/** Open the connection to the hub (no token in URL) */
|
|
33
43
|
connect(): void;
|
|
34
44
|
/** Send a JSON message to the hub */
|
|
35
45
|
send(data: unknown): boolean;
|
|
@@ -39,6 +49,21 @@ declare class HubConnection {
|
|
|
39
49
|
private stopHeartbeat;
|
|
40
50
|
private scheduleReconnect;
|
|
41
51
|
private cleanup;
|
|
52
|
+
/**
|
|
53
|
+
* Handle proxy protocol messages from the Hub.
|
|
54
|
+
* Returns true if the message was handled.
|
|
55
|
+
*/
|
|
56
|
+
private handleProxyMessage;
|
|
57
|
+
private openLocalGateway;
|
|
58
|
+
/**
|
|
59
|
+
* Intercept browser's "connect" request and patch it for the local gateway:
|
|
60
|
+
* - Inject gateway auth token
|
|
61
|
+
* - Remove `device` object (browser's device identity/signature doesn't apply
|
|
62
|
+
* through the proxy; the gateway will use token-only auth)
|
|
63
|
+
*/
|
|
64
|
+
private patchBrowserConnect;
|
|
65
|
+
private closeProxySession;
|
|
66
|
+
private closeAllProxySessions;
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
interface AgentConfig {
|
package/dist/start.js
CHANGED
|
@@ -148,6 +148,15 @@ function ensureGatewayToken() {
|
|
|
148
148
|
config.gateway.mode = "local";
|
|
149
149
|
dirty = true;
|
|
150
150
|
}
|
|
151
|
+
if (!config.gateway.controlUi) config.gateway.controlUi = {};
|
|
152
|
+
if (!config.gateway.controlUi.allowedOrigins) {
|
|
153
|
+
config.gateway.controlUi.allowedOrigins = ["*"];
|
|
154
|
+
dirty = true;
|
|
155
|
+
}
|
|
156
|
+
if (config.gateway.controlUi.allowInsecureAuth === void 0) {
|
|
157
|
+
config.gateway.controlUi.allowInsecureAuth = true;
|
|
158
|
+
dirty = true;
|
|
159
|
+
}
|
|
151
160
|
if (dirty) {
|
|
152
161
|
writeFileSync2(file, JSON.stringify(config, null, 2) + "\n");
|
|
153
162
|
}
|
|
@@ -382,6 +391,7 @@ function promptChoice(max) {
|
|
|
382
391
|
}
|
|
383
392
|
|
|
384
393
|
// src/hub-connection.ts
|
|
394
|
+
import { hostname as osHostname } from "os";
|
|
385
395
|
import WebSocket from "ws";
|
|
386
396
|
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
387
397
|
var DEFAULT_RECONNECT_MS = 1e3;
|
|
@@ -392,22 +402,32 @@ var HubConnection = class {
|
|
|
392
402
|
reconnectTimer = null;
|
|
393
403
|
reconnectDelay;
|
|
394
404
|
destroyed = false;
|
|
405
|
+
authState = "pending";
|
|
406
|
+
proxySessions = /* @__PURE__ */ new Map();
|
|
395
407
|
hubUrl;
|
|
396
408
|
token;
|
|
409
|
+
hostname;
|
|
410
|
+
gatewayPort;
|
|
411
|
+
gatewayToken;
|
|
397
412
|
heartbeatInterval;
|
|
398
413
|
maxReconnectDelay;
|
|
414
|
+
/** Agent ID assigned by Hub after successful auth */
|
|
415
|
+
agentId = null;
|
|
399
416
|
onMessage;
|
|
400
|
-
|
|
417
|
+
onConnectCb;
|
|
401
418
|
onDisconnect;
|
|
402
419
|
onError;
|
|
403
420
|
constructor(opts) {
|
|
404
421
|
this.hubUrl = opts.hubUrl;
|
|
405
422
|
this.token = opts.token;
|
|
423
|
+
this.hostname = opts.hostname ?? osHostname();
|
|
424
|
+
this.gatewayPort = opts.gatewayPort ?? 18789;
|
|
425
|
+
this.gatewayToken = opts.gatewayToken ?? "";
|
|
406
426
|
this.heartbeatInterval = opts.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
407
427
|
this.reconnectDelay = opts.reconnectDelay ?? DEFAULT_RECONNECT_MS;
|
|
408
428
|
this.maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_MS;
|
|
409
429
|
this.onMessage = opts.onMessage;
|
|
410
|
-
this.
|
|
430
|
+
this.onConnectCb = opts.onConnect;
|
|
411
431
|
this.onDisconnect = opts.onDisconnect;
|
|
412
432
|
this.onError = opts.onError;
|
|
413
433
|
}
|
|
@@ -416,31 +436,53 @@ var HubConnection = class {
|
|
|
416
436
|
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
417
437
|
}
|
|
418
438
|
get isConnected() {
|
|
419
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
439
|
+
return this.ws?.readyState === WebSocket.OPEN && this.authState === "authenticated";
|
|
420
440
|
}
|
|
421
|
-
/** Open the connection to the hub */
|
|
441
|
+
/** Open the connection to the hub (no token in URL) */
|
|
422
442
|
connect() {
|
|
423
443
|
if (this.destroyed) return;
|
|
424
444
|
this.cleanup();
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.ws = new WebSocket(url.toString());
|
|
445
|
+
this.authState = "pending";
|
|
446
|
+
this.ws = new WebSocket(this.hubUrl);
|
|
428
447
|
this.ws.on("open", () => {
|
|
429
448
|
this.reconnectDelay = DEFAULT_RECONNECT_MS;
|
|
430
|
-
this.startHeartbeat();
|
|
431
|
-
this.onConnect?.();
|
|
432
449
|
});
|
|
433
450
|
this.ws.on("message", (raw) => {
|
|
451
|
+
let data;
|
|
434
452
|
try {
|
|
435
|
-
|
|
436
|
-
this.onMessage?.(data);
|
|
453
|
+
data = JSON.parse(raw.toString());
|
|
437
454
|
} catch {
|
|
438
|
-
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (this.authState === "pending" && data.type === "auth_required") {
|
|
458
|
+
this.authState = "authenticating";
|
|
459
|
+
this.ws.send(JSON.stringify({
|
|
460
|
+
type: "auth",
|
|
461
|
+
token: this.token,
|
|
462
|
+
hostname: this.hostname
|
|
463
|
+
}));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (this.authState === "authenticating" && data.type === "registered") {
|
|
467
|
+
this.authState = "authenticated";
|
|
468
|
+
this.agentId = data.agentId ?? null;
|
|
469
|
+
this.startHeartbeat();
|
|
470
|
+
this.onConnectCb?.(this.agentId);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (this.authState === "authenticated") {
|
|
474
|
+
if (this.handleProxyMessage(data)) return;
|
|
475
|
+
this.onMessage?.(data);
|
|
439
476
|
}
|
|
440
477
|
});
|
|
441
478
|
this.ws.on("close", (code, reason) => {
|
|
442
479
|
this.stopHeartbeat();
|
|
480
|
+
this.authState = "pending";
|
|
443
481
|
this.onDisconnect?.(code, reason.toString());
|
|
482
|
+
if (code === 4002) {
|
|
483
|
+
console.error("[clawnet] Auth failed \u2014 not reconnecting. Check your token.");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
444
486
|
this.scheduleReconnect();
|
|
445
487
|
});
|
|
446
488
|
this.ws.on("error", (err) => {
|
|
@@ -449,7 +491,7 @@ var HubConnection = class {
|
|
|
449
491
|
}
|
|
450
492
|
/** Send a JSON message to the hub */
|
|
451
493
|
send(data) {
|
|
452
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
494
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.authState !== "authenticated") return false;
|
|
453
495
|
this.ws.send(JSON.stringify(data));
|
|
454
496
|
return true;
|
|
455
497
|
}
|
|
@@ -479,6 +521,7 @@ var HubConnection = class {
|
|
|
479
521
|
}
|
|
480
522
|
cleanup() {
|
|
481
523
|
this.stopHeartbeat();
|
|
524
|
+
this.closeAllProxySessions();
|
|
482
525
|
if (this.reconnectTimer) {
|
|
483
526
|
clearTimeout(this.reconnectTimer);
|
|
484
527
|
this.reconnectTimer = null;
|
|
@@ -491,6 +534,96 @@ var HubConnection = class {
|
|
|
491
534
|
this.ws = null;
|
|
492
535
|
}
|
|
493
536
|
}
|
|
537
|
+
// ── Proxy session management ──────────────────────────────────────
|
|
538
|
+
/**
|
|
539
|
+
* Handle proxy protocol messages from the Hub.
|
|
540
|
+
* Returns true if the message was handled.
|
|
541
|
+
*/
|
|
542
|
+
handleProxyMessage(msg) {
|
|
543
|
+
if (msg.type === "proxy.open" && msg.sessionId) {
|
|
544
|
+
this.openLocalGateway(msg.sessionId, msg.gatewayPort ?? this.gatewayPort);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
if (msg.type === "proxy.data" && msg.sessionId) {
|
|
548
|
+
const session = this.proxySessions.get(msg.sessionId);
|
|
549
|
+
if (session && session.localWs.readyState === WebSocket.OPEN && msg.data) {
|
|
550
|
+
const patched = this.patchBrowserConnect(msg.data, session);
|
|
551
|
+
session.localWs.send(patched);
|
|
552
|
+
}
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
if (msg.type === "proxy.close" && msg.sessionId) {
|
|
556
|
+
this.closeProxySession(msg.sessionId);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
openLocalGateway(sessionId, port) {
|
|
562
|
+
const localWs = new WebSocket(`ws://127.0.0.1:${port}/`, {
|
|
563
|
+
headers: { Origin: `http://127.0.0.1:${port}` }
|
|
564
|
+
});
|
|
565
|
+
const session = {
|
|
566
|
+
sessionId,
|
|
567
|
+
localWs,
|
|
568
|
+
gatewayNonce: null
|
|
569
|
+
};
|
|
570
|
+
this.proxySessions.set(sessionId, session);
|
|
571
|
+
localWs.on("open", () => {
|
|
572
|
+
this.send({ type: "proxy.opened", sessionId });
|
|
573
|
+
});
|
|
574
|
+
localWs.on("message", (raw) => {
|
|
575
|
+
const text = raw.toString();
|
|
576
|
+
try {
|
|
577
|
+
const frame = JSON.parse(text);
|
|
578
|
+
if (frame.type === "evt" && frame.event === "connect.challenge" && frame.payload?.nonce) {
|
|
579
|
+
session.gatewayNonce = frame.payload.nonce;
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
this.send({ type: "proxy.data", sessionId, data: text });
|
|
584
|
+
});
|
|
585
|
+
localWs.on("close", () => {
|
|
586
|
+
this.proxySessions.delete(sessionId);
|
|
587
|
+
this.send({ type: "proxy.close", sessionId });
|
|
588
|
+
});
|
|
589
|
+
localWs.on("error", () => {
|
|
590
|
+
this.proxySessions.delete(sessionId);
|
|
591
|
+
this.send({ type: "proxy.close", sessionId });
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Intercept browser's "connect" request and patch it for the local gateway:
|
|
596
|
+
* - Inject gateway auth token
|
|
597
|
+
* - Remove `device` object (browser's device identity/signature doesn't apply
|
|
598
|
+
* through the proxy; the gateway will use token-only auth)
|
|
599
|
+
*/
|
|
600
|
+
patchBrowserConnect(raw, session) {
|
|
601
|
+
if (!this.gatewayToken) return raw;
|
|
602
|
+
try {
|
|
603
|
+
const frame = JSON.parse(raw);
|
|
604
|
+
if (frame.type === "req" && frame.method === "connect" && frame.params) {
|
|
605
|
+
if (!frame.params.auth) frame.params.auth = {};
|
|
606
|
+
frame.params.auth.token = this.gatewayToken;
|
|
607
|
+
delete frame.params.device;
|
|
608
|
+
return JSON.stringify(frame);
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
return raw;
|
|
613
|
+
}
|
|
614
|
+
closeProxySession(sessionId) {
|
|
615
|
+
const session = this.proxySessions.get(sessionId);
|
|
616
|
+
if (!session) return;
|
|
617
|
+
this.proxySessions.delete(sessionId);
|
|
618
|
+
if (session.localWs.readyState === WebSocket.OPEN || session.localWs.readyState === WebSocket.CONNECTING) {
|
|
619
|
+
session.localWs.close();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
closeAllProxySessions() {
|
|
623
|
+
for (const [sessionId] of this.proxySessions) {
|
|
624
|
+
this.closeProxySession(sessionId);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
494
627
|
};
|
|
495
628
|
|
|
496
629
|
// src/start.ts
|
|
@@ -536,6 +669,8 @@ async function startAgent(opts) {
|
|
|
536
669
|
const hubConnection = new HubConnection({
|
|
537
670
|
hubUrl: config.hubUrl,
|
|
538
671
|
token: config.token,
|
|
672
|
+
gatewayPort: config.port,
|
|
673
|
+
gatewayToken,
|
|
539
674
|
onConnect: () => {
|
|
540
675
|
console.log("[clawnet] Connected to Hub");
|
|
541
676
|
},
|