sessioncast-cli 2.2.2 → 2.3.1
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/agent/api-client.js +17 -3
- package/dist/agent/browser-handler.d.ts +100 -0
- package/dist/agent/browser-handler.js +485 -0
- package/dist/agent/cdp-client.d.ts +28 -0
- package/dist/agent/cdp-client.js +115 -0
- package/dist/agent/chrome-finder.d.ts +5 -0
- package/dist/agent/chrome-finder.js +100 -0
- package/dist/agent/crypto.d.ts +2 -0
- package/dist/agent/crypto.js +24 -0
- package/dist/agent/llm-service.d.ts +13 -0
- package/dist/agent/llm-service.js +588 -0
- package/dist/agent/runner.d.ts +7 -1
- package/dist/agent/runner.js +60 -2
- package/dist/agent/session-handler.js +2 -1
- package/dist/agent/tunnel-handler.d.ts +12 -0
- package/dist/agent/tunnel-handler.js +180 -0
- package/dist/agent/types.d.ts +22 -1
- package/dist/agent/websocket.d.ts +18 -0
- package/dist/agent/websocket.js +113 -15
- package/dist/commands/agent.js +1 -1
- package/dist/commands/tunnel.d.ts +10 -0
- package/dist/commands/tunnel.js +201 -0
- package/dist/index.js +23 -0
- package/package.json +3 -2
package/dist/agent/runner.js
CHANGED
|
@@ -32,12 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.AgentRunner = void 0;
|
|
37
40
|
const fs = __importStar(require("fs"));
|
|
38
41
|
const path = __importStar(require("path"));
|
|
39
42
|
const os = __importStar(require("os"));
|
|
40
43
|
const yaml = __importStar(require("js-yaml"));
|
|
44
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
41
45
|
const session_handler_1 = require("./session-handler");
|
|
42
46
|
const api_client_1 = require("./api-client");
|
|
43
47
|
const tmux = __importStar(require("./tmux"));
|
|
@@ -52,7 +56,29 @@ class AgentRunner {
|
|
|
52
56
|
this.running = false;
|
|
53
57
|
this.config = config;
|
|
54
58
|
}
|
|
55
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Fetch optimal relay URL from Platform API using agent token.
|
|
61
|
+
* Returns null on failure (caller should fall back to default).
|
|
62
|
+
*/
|
|
63
|
+
static async fetchRelayUrlForAgent(token) {
|
|
64
|
+
try {
|
|
65
|
+
const apiUrl = (0, config_1.getApiUrl)();
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
68
|
+
const res = await (0, node_fetch_1.default)(`${apiUrl}/public/agent-tokens/${token}/relay`, {
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
return null;
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
return data.relayUrl || null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
static async loadConfig(configPath) {
|
|
56
82
|
// Check if agent token is available (for relay connection)
|
|
57
83
|
const agentToken = (0, config_1.getAgentToken)();
|
|
58
84
|
// Check environment variable
|
|
@@ -75,9 +101,18 @@ class AgentRunner {
|
|
|
75
101
|
if ((!finalPath || !fs.existsSync(finalPath)) && agentToken) {
|
|
76
102
|
console.log('Using OAuth authentication');
|
|
77
103
|
const machineId = os.hostname();
|
|
104
|
+
// Try to fetch optimal relay URL from Platform API
|
|
105
|
+
let relayUrl = await AgentRunner.fetchRelayUrlForAgent(agentToken);
|
|
106
|
+
if (relayUrl) {
|
|
107
|
+
console.log(`Fetched relay URL from Platform API: ${relayUrl}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
relayUrl = (0, config_1.getRelayUrl)();
|
|
111
|
+
console.log(`Using default relay URL: ${relayUrl}`);
|
|
112
|
+
}
|
|
78
113
|
return {
|
|
79
114
|
machineId,
|
|
80
|
-
relay:
|
|
115
|
+
relay: relayUrl,
|
|
81
116
|
token: agentToken,
|
|
82
117
|
api: {
|
|
83
118
|
enabled: false
|
|
@@ -101,10 +136,23 @@ class AgentRunner {
|
|
|
101
136
|
if (this.running)
|
|
102
137
|
return;
|
|
103
138
|
this.running = true;
|
|
139
|
+
// Fetch encryption key from Platform API if not already set
|
|
140
|
+
if (!this.config.encKey && this.config.token) {
|
|
141
|
+
try {
|
|
142
|
+
const encKey = await this.fetchEncKey(this.config.token);
|
|
143
|
+
if (encKey) {
|
|
144
|
+
this.config.encKey = encKey;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
console.warn('Failed to fetch encryption key (E2E disabled):', e.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
104
151
|
console.log('Starting SessionCast Agent...');
|
|
105
152
|
console.log(`Machine ID: ${this.config.machineId}`);
|
|
106
153
|
console.log(`Relay: ${this.config.relay}`);
|
|
107
154
|
console.log(`Token: ${this.config.token ? 'present' : 'none'}`);
|
|
155
|
+
console.log(`E2E Encryption: ${this.config.encKey ? 'enabled' : 'disabled'}`);
|
|
108
156
|
// Check tmux/itmux availability before starting
|
|
109
157
|
if (!tmux.isAvailable()) {
|
|
110
158
|
const platform = os.platform();
|
|
@@ -220,6 +268,16 @@ class AgentRunner {
|
|
|
220
268
|
console.error(`Failed to create tmux session: ${sanitized}`);
|
|
221
269
|
}
|
|
222
270
|
}
|
|
271
|
+
async fetchEncKey(agentToken) {
|
|
272
|
+
const fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
|
|
273
|
+
const apiUrl = (0, config_1.getApiUrl)();
|
|
274
|
+
const url = `${apiUrl}/internal/agents/enc-key?token=${encodeURIComponent(agentToken)}`;
|
|
275
|
+
const res = await fetch(url, { timeout: 5000 });
|
|
276
|
+
if (!res.ok)
|
|
277
|
+
return null;
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
return data.encKey || null;
|
|
280
|
+
}
|
|
223
281
|
stop() {
|
|
224
282
|
console.log('Shutting down Agent...');
|
|
225
283
|
this.running = false;
|
|
@@ -89,7 +89,8 @@ class TmuxSessionHandler {
|
|
|
89
89
|
machineId: this.config.machineId,
|
|
90
90
|
token: this.config.token,
|
|
91
91
|
label: this.tmuxSession,
|
|
92
|
-
autoReconnect: true
|
|
92
|
+
autoReconnect: true,
|
|
93
|
+
encKey: this.config.encKey
|
|
93
94
|
});
|
|
94
95
|
this.wsClient.on('connected', () => {
|
|
95
96
|
console.log(`[${this.tmuxSession}] Connected to relay`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class TunnelHandler {
|
|
2
|
+
private port;
|
|
3
|
+
private host;
|
|
4
|
+
private activeRequests;
|
|
5
|
+
private readonly MAX_CONCURRENT;
|
|
6
|
+
private readonly MAX_RESPONSE_SIZE;
|
|
7
|
+
private readonly CHUNK_SIZE;
|
|
8
|
+
constructor(port: number, host?: string);
|
|
9
|
+
handleRequest(requestId: string, method: string, path: string, queryString: string, headers: Record<string, string>, body: string | undefined, sendResponse: (msg: any) => void): Promise<void>;
|
|
10
|
+
private getBase64ChunkSize;
|
|
11
|
+
private makeHttpRequest;
|
|
12
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TunnelHandler = void 0;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const debug_1 = require("./debug");
|
|
39
|
+
const ALLOWED_HOSTS = ['localhost', '127.0.0.1', '::1'];
|
|
40
|
+
class TunnelHandler {
|
|
41
|
+
constructor(port, host = 'localhost') {
|
|
42
|
+
this.activeRequests = 0;
|
|
43
|
+
this.MAX_CONCURRENT = 10;
|
|
44
|
+
this.MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
45
|
+
this.CHUNK_SIZE = 200 * 1024; // 200KB before base64
|
|
46
|
+
this.port = port;
|
|
47
|
+
this.host = host;
|
|
48
|
+
}
|
|
49
|
+
async handleRequest(requestId, method, path, queryString, headers, body, sendResponse) {
|
|
50
|
+
// Check concurrent limit
|
|
51
|
+
if (this.activeRequests >= this.MAX_CONCURRENT) {
|
|
52
|
+
sendResponse({
|
|
53
|
+
requestId,
|
|
54
|
+
statusCode: 503,
|
|
55
|
+
headers: { 'content-type': 'text/plain' },
|
|
56
|
+
body: Buffer.from('Too many concurrent requests').toString('base64'),
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Validate host (SSRF prevention)
|
|
61
|
+
if (!ALLOWED_HOSTS.includes(this.host)) {
|
|
62
|
+
sendResponse({
|
|
63
|
+
requestId,
|
|
64
|
+
statusCode: 403,
|
|
65
|
+
headers: { 'content-type': 'text/plain' },
|
|
66
|
+
body: Buffer.from('Forbidden: only localhost connections allowed').toString('base64'),
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.activeRequests++;
|
|
71
|
+
try {
|
|
72
|
+
const fullPath = queryString ? `${path}?${queryString}` : path;
|
|
73
|
+
const requestBody = body ? Buffer.from(body, 'base64') : undefined;
|
|
74
|
+
(0, debug_1.debugLog)('TUNNEL', `${method} ${fullPath} -> ${this.host}:${this.port}`);
|
|
75
|
+
const { statusCode, responseHeaders, responseBody } = await this.makeHttpRequest(method, fullPath, headers, requestBody);
|
|
76
|
+
const bodyBase64 = responseBody.toString('base64');
|
|
77
|
+
if (responseBody.length > this.CHUNK_SIZE) {
|
|
78
|
+
// Split into chunks
|
|
79
|
+
const totalChunks = Math.ceil(bodyBase64.length / this.getBase64ChunkSize());
|
|
80
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
81
|
+
const start = i * this.getBase64ChunkSize();
|
|
82
|
+
const end = Math.min(start + this.getBase64ChunkSize(), bodyBase64.length);
|
|
83
|
+
const chunk = bodyBase64.slice(start, end);
|
|
84
|
+
const isFinal = i === totalChunks - 1;
|
|
85
|
+
sendResponse({
|
|
86
|
+
requestId,
|
|
87
|
+
statusCode,
|
|
88
|
+
headers: responseHeaders,
|
|
89
|
+
body: chunk,
|
|
90
|
+
chunkIndex: i,
|
|
91
|
+
isFinal,
|
|
92
|
+
chunked: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
sendResponse({
|
|
98
|
+
requestId,
|
|
99
|
+
statusCode,
|
|
100
|
+
headers: responseHeaders,
|
|
101
|
+
body: bodyBase64,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
(0, debug_1.debugLog)('TUNNEL', `Error: ${error.message}`);
|
|
107
|
+
sendResponse({
|
|
108
|
+
requestId,
|
|
109
|
+
statusCode: 502,
|
|
110
|
+
headers: { 'content-type': 'text/plain' },
|
|
111
|
+
body: Buffer.from(`Bad Gateway: ${error.message}`).toString('base64'),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.activeRequests--;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
getBase64ChunkSize() {
|
|
119
|
+
// base64 encoding expands data by ~4/3, so chunk size in base64 chars
|
|
120
|
+
// for 200KB raw data is approximately 200*1024*4/3 ≈ 273KB base64
|
|
121
|
+
return Math.ceil(this.CHUNK_SIZE * 4 / 3);
|
|
122
|
+
}
|
|
123
|
+
makeHttpRequest(method, path, headers, body) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
// Build headers, removing hop-by-hop headers that shouldn't be forwarded
|
|
126
|
+
const forwardHeaders = {
|
|
127
|
+
...headers,
|
|
128
|
+
host: `${this.host}:${this.port}`,
|
|
129
|
+
};
|
|
130
|
+
delete forwardHeaders['connection'];
|
|
131
|
+
delete forwardHeaders['keep-alive'];
|
|
132
|
+
delete forwardHeaders['transfer-encoding'];
|
|
133
|
+
const options = {
|
|
134
|
+
hostname: this.host,
|
|
135
|
+
port: this.port,
|
|
136
|
+
path,
|
|
137
|
+
method,
|
|
138
|
+
headers: forwardHeaders,
|
|
139
|
+
timeout: 30000,
|
|
140
|
+
};
|
|
141
|
+
const req = http.request(options, (res) => {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let totalSize = 0;
|
|
144
|
+
res.on('data', (chunk) => {
|
|
145
|
+
totalSize += chunk.length;
|
|
146
|
+
if (totalSize > this.MAX_RESPONSE_SIZE) {
|
|
147
|
+
req.destroy();
|
|
148
|
+
reject(new Error(`Response exceeds maximum size of ${this.MAX_RESPONSE_SIZE} bytes`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
chunks.push(chunk);
|
|
152
|
+
});
|
|
153
|
+
res.on('end', () => {
|
|
154
|
+
const responseHeaders = {};
|
|
155
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
156
|
+
if (value) {
|
|
157
|
+
responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
resolve({
|
|
161
|
+
statusCode: res.statusCode || 502,
|
|
162
|
+
responseHeaders,
|
|
163
|
+
responseBody: Buffer.concat(chunks),
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
res.on('error', reject);
|
|
167
|
+
});
|
|
168
|
+
req.on('error', reject);
|
|
169
|
+
req.on('timeout', () => {
|
|
170
|
+
req.destroy();
|
|
171
|
+
reject(new Error('Request timed out'));
|
|
172
|
+
});
|
|
173
|
+
if (body) {
|
|
174
|
+
req.write(body);
|
|
175
|
+
}
|
|
176
|
+
req.end();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.TunnelHandler = TunnelHandler;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface AgentConfig {
|
|
|
2
2
|
machineId: string;
|
|
3
3
|
relay: string;
|
|
4
4
|
token: string;
|
|
5
|
+
encKey?: string;
|
|
5
6
|
api?: ApiConfig;
|
|
6
7
|
}
|
|
7
8
|
export interface ApiConfig {
|
|
@@ -34,7 +35,7 @@ export interface ExecConfig {
|
|
|
34
35
|
}
|
|
35
36
|
export interface LlmConfig {
|
|
36
37
|
enabled: boolean;
|
|
37
|
-
provider: 'ollama' | 'openai' | 'anthropic' | 'claude-code';
|
|
38
|
+
provider: 'ollama' | 'openai' | 'anthropic' | 'claude-code' | 'codex' | 'gemini' | 'cursor';
|
|
38
39
|
baseUrl: string;
|
|
39
40
|
model: string;
|
|
40
41
|
apiKey?: string;
|
|
@@ -64,6 +65,26 @@ export interface ExecResult {
|
|
|
64
65
|
stderr: string;
|
|
65
66
|
duration: number;
|
|
66
67
|
}
|
|
68
|
+
export interface TunnelConfig {
|
|
69
|
+
chromePath?: string;
|
|
70
|
+
width?: number;
|
|
71
|
+
height?: number;
|
|
72
|
+
cdpPort?: number;
|
|
73
|
+
}
|
|
74
|
+
export interface WebInputEvent {
|
|
75
|
+
inputType?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
x?: number;
|
|
78
|
+
y?: number;
|
|
79
|
+
button?: string;
|
|
80
|
+
clickCount?: number;
|
|
81
|
+
key?: string;
|
|
82
|
+
code?: string;
|
|
83
|
+
text?: string;
|
|
84
|
+
modifiers?: number;
|
|
85
|
+
deltaX?: number;
|
|
86
|
+
deltaY?: number;
|
|
87
|
+
}
|
|
67
88
|
export interface LlmMessage {
|
|
68
89
|
role: 'system' | 'user' | 'assistant';
|
|
69
90
|
content: string;
|
|
@@ -7,6 +7,8 @@ interface WebSocketClientOptions {
|
|
|
7
7
|
token: string;
|
|
8
8
|
label?: string;
|
|
9
9
|
autoReconnect?: boolean;
|
|
10
|
+
encKey?: string;
|
|
11
|
+
skipAutoRegister?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export declare class RelayWebSocketClient extends EventEmitter {
|
|
12
14
|
private ws;
|
|
@@ -16,6 +18,8 @@ export declare class RelayWebSocketClient extends EventEmitter {
|
|
|
16
18
|
private token;
|
|
17
19
|
private label;
|
|
18
20
|
private autoReconnect;
|
|
21
|
+
private encKeyBuf;
|
|
22
|
+
private skipAutoRegister;
|
|
19
23
|
private isConnected;
|
|
20
24
|
private reconnectAttempts;
|
|
21
25
|
private circuitBreakerOpen;
|
|
@@ -25,12 +29,14 @@ export declare class RelayWebSocketClient extends EventEmitter {
|
|
|
25
29
|
constructor(options: WebSocketClientOptions);
|
|
26
30
|
connect(): void;
|
|
27
31
|
private registerAsHost;
|
|
32
|
+
private decryptPayload;
|
|
28
33
|
private handleMessage;
|
|
29
34
|
private handleError;
|
|
30
35
|
private scheduleReconnect;
|
|
31
36
|
send(message: Message): boolean;
|
|
32
37
|
sendScreen(data: Buffer): boolean;
|
|
33
38
|
sendScreenWithMeta(data: Buffer, meta: Record<string, string>): boolean;
|
|
39
|
+
private compressAndEncrypt;
|
|
34
40
|
sendScreenCompressed(data: Buffer): boolean;
|
|
35
41
|
sendScreenCompressedWithMeta(data: Buffer, meta: Record<string, string>): boolean;
|
|
36
42
|
sendSessionMeta(meta: Record<string, string>): boolean;
|
|
@@ -60,6 +66,18 @@ export declare class RelayWebSocketClient extends EventEmitter {
|
|
|
60
66
|
* Send upload error notification to web viewer
|
|
61
67
|
*/
|
|
62
68
|
sendUploadError(filename: string, error: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Send a single rrweb DOM event to the relay.
|
|
71
|
+
*/
|
|
72
|
+
sendWebDom(event: any): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Send a batch of rrweb DOM events.
|
|
75
|
+
*/
|
|
76
|
+
sendWebDomBatch(events: any[]): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Send web page metadata (title, URL, viewport).
|
|
79
|
+
*/
|
|
80
|
+
sendWebMeta(meta: Record<string, string>): boolean;
|
|
63
81
|
getConnected(): boolean;
|
|
64
82
|
destroy(): void;
|
|
65
83
|
}
|
package/dist/agent/websocket.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.RelayWebSocketClient = void 0;
|
|
|
40
40
|
const ws_1 = __importDefault(require("ws"));
|
|
41
41
|
const events_1 = require("events");
|
|
42
42
|
const zlib = __importStar(require("zlib"));
|
|
43
|
+
const zstd = __importStar(require("zstd-napi"));
|
|
44
|
+
const crypto_1 = require("./crypto");
|
|
43
45
|
const debug_1 = require("./debug");
|
|
44
46
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
45
47
|
const BASE_RECONNECT_DELAY_MS = 2000;
|
|
@@ -49,6 +51,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
49
51
|
constructor(options) {
|
|
50
52
|
super();
|
|
51
53
|
this.ws = null;
|
|
54
|
+
this.encKeyBuf = null;
|
|
52
55
|
this.isConnected = false;
|
|
53
56
|
this.reconnectAttempts = 0;
|
|
54
57
|
this.circuitBreakerOpen = false;
|
|
@@ -61,6 +64,10 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
61
64
|
this.token = options.token;
|
|
62
65
|
this.label = options.label || options.sessionId;
|
|
63
66
|
this.autoReconnect = options.autoReconnect ?? true;
|
|
67
|
+
if (options.encKey) {
|
|
68
|
+
this.encKeyBuf = Buffer.from(options.encKey, 'base64url');
|
|
69
|
+
}
|
|
70
|
+
this.skipAutoRegister = options.skipAutoRegister ?? false;
|
|
64
71
|
}
|
|
65
72
|
connect() {
|
|
66
73
|
if (this.destroyed)
|
|
@@ -72,7 +79,9 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
72
79
|
this.reconnectAttempts = 0;
|
|
73
80
|
this.circuitBreakerOpen = false;
|
|
74
81
|
this.emit('connected');
|
|
75
|
-
this.
|
|
82
|
+
if (!this.skipAutoRegister) {
|
|
83
|
+
this.registerAsHost();
|
|
84
|
+
}
|
|
76
85
|
});
|
|
77
86
|
this.ws.on('message', (data) => {
|
|
78
87
|
try {
|
|
@@ -116,9 +125,28 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
116
125
|
meta
|
|
117
126
|
});
|
|
118
127
|
}
|
|
128
|
+
decryptPayload(payload) {
|
|
129
|
+
if (!this.encKeyBuf)
|
|
130
|
+
return payload;
|
|
131
|
+
try {
|
|
132
|
+
const buf = Buffer.from(payload, 'base64');
|
|
133
|
+
const decrypted = (0, crypto_1.decrypt)(buf, this.encKeyBuf);
|
|
134
|
+
return decrypted.toString('utf-8');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// If decryption fails, return as-is (unencrypted message)
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
handleMessage(message) {
|
|
120
142
|
(0, debug_1.debugLog)('WS:IN', message.type, message.session);
|
|
121
143
|
switch (message.type) {
|
|
144
|
+
case 'keysEnc':
|
|
145
|
+
if (message.session === this.sessionId && message.payload) {
|
|
146
|
+
const keys = this.decryptPayload(message.payload);
|
|
147
|
+
this.emit('keys', keys, message.meta?.pane);
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
122
150
|
case 'keys':
|
|
123
151
|
if (message.session === this.sessionId && message.payload) {
|
|
124
152
|
this.emit('keys', message.payload, message.meta?.pane);
|
|
@@ -148,6 +176,25 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
148
176
|
this.emit('createWorktree', message.meta.branch);
|
|
149
177
|
}
|
|
150
178
|
break;
|
|
179
|
+
case 'webInput':
|
|
180
|
+
if (message.session === this.sessionId && message.payload) {
|
|
181
|
+
try {
|
|
182
|
+
const inputEvent = JSON.parse(message.payload);
|
|
183
|
+
this.emit('webInput', inputEvent);
|
|
184
|
+
}
|
|
185
|
+
catch { /* ignore parse errors */ }
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'webNavigate':
|
|
189
|
+
if (message.session === this.sessionId && message.meta?.url) {
|
|
190
|
+
this.emit('webNavigate', message.meta.url);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'requestSnapshot':
|
|
194
|
+
if (message.session === this.sessionId) {
|
|
195
|
+
this.emit('requestSnapshot');
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
151
198
|
case 'requestFileView':
|
|
152
199
|
if (message.session === this.sessionId && message.meta?.filePath) {
|
|
153
200
|
this.emit('requestFileView', message.meta.filePath);
|
|
@@ -268,17 +315,37 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
268
315
|
meta
|
|
269
316
|
});
|
|
270
317
|
}
|
|
318
|
+
compressAndEncrypt(data) {
|
|
319
|
+
// Step 1: Compress with zstd (fallback to gzip)
|
|
320
|
+
let compressed;
|
|
321
|
+
let compressType = 'zstd';
|
|
322
|
+
try {
|
|
323
|
+
compressed = zstd.compress(data, { compressionLevel: 3 });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
compressed = zlib.gzipSync(data);
|
|
327
|
+
compressType = 'gz';
|
|
328
|
+
}
|
|
329
|
+
// Step 2: Encrypt if encKey is available
|
|
330
|
+
if (this.encKeyBuf) {
|
|
331
|
+
const encrypted = (0, crypto_1.encrypt)(compressed, this.encKeyBuf);
|
|
332
|
+
return {
|
|
333
|
+
payload: encrypted.toString('base64'),
|
|
334
|
+
type: 'screenEnc'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// No encryption — send compressed only
|
|
338
|
+
return {
|
|
339
|
+
payload: compressed.toString('base64'),
|
|
340
|
+
type: compressType === 'zstd' ? 'screenZstd' : 'screenGz'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
271
343
|
sendScreenCompressed(data) {
|
|
272
344
|
if (!this.isConnected)
|
|
273
345
|
return false;
|
|
274
346
|
try {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
return this.send({
|
|
278
|
-
type: 'screenGz',
|
|
279
|
-
session: this.sessionId,
|
|
280
|
-
payload: base64Data
|
|
281
|
-
});
|
|
347
|
+
const { payload, type } = this.compressAndEncrypt(data);
|
|
348
|
+
return this.send({ type, session: this.sessionId, payload });
|
|
282
349
|
}
|
|
283
350
|
catch {
|
|
284
351
|
return this.sendScreen(data);
|
|
@@ -288,13 +355,8 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
288
355
|
if (!this.isConnected)
|
|
289
356
|
return false;
|
|
290
357
|
try {
|
|
291
|
-
const
|
|
292
|
-
return this.send({
|
|
293
|
-
type: 'screenGz',
|
|
294
|
-
session: this.sessionId,
|
|
295
|
-
payload: compressed.toString('base64'),
|
|
296
|
-
meta
|
|
297
|
-
});
|
|
358
|
+
const { payload, type } = this.compressAndEncrypt(data);
|
|
359
|
+
return this.send({ type, session: this.sessionId, payload, meta });
|
|
298
360
|
}
|
|
299
361
|
catch {
|
|
300
362
|
return this.sendScreenWithMeta(data, meta);
|
|
@@ -370,6 +432,42 @@ class RelayWebSocketClient extends events_1.EventEmitter {
|
|
|
370
432
|
}
|
|
371
433
|
});
|
|
372
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Send a single rrweb DOM event to the relay.
|
|
437
|
+
*/
|
|
438
|
+
sendWebDom(event) {
|
|
439
|
+
if (!this.isConnected)
|
|
440
|
+
return false;
|
|
441
|
+
return this.send({
|
|
442
|
+
type: 'webDom',
|
|
443
|
+
session: this.sessionId,
|
|
444
|
+
payload: JSON.stringify(event),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Send a batch of rrweb DOM events.
|
|
449
|
+
*/
|
|
450
|
+
sendWebDomBatch(events) {
|
|
451
|
+
if (!this.isConnected)
|
|
452
|
+
return false;
|
|
453
|
+
return this.send({
|
|
454
|
+
type: 'webDomBatch',
|
|
455
|
+
session: this.sessionId,
|
|
456
|
+
payload: JSON.stringify(events),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Send web page metadata (title, URL, viewport).
|
|
461
|
+
*/
|
|
462
|
+
sendWebMeta(meta) {
|
|
463
|
+
if (!this.isConnected)
|
|
464
|
+
return false;
|
|
465
|
+
return this.send({
|
|
466
|
+
type: 'webMeta',
|
|
467
|
+
session: this.sessionId,
|
|
468
|
+
meta,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
373
471
|
getConnected() {
|
|
374
472
|
return this.isConnected;
|
|
375
473
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -14,7 +14,7 @@ async function startAgent(options) {
|
|
|
14
14
|
(0, debug_1.setDebug)(true);
|
|
15
15
|
console.log(chalk_1.default.yellow('[DEBUG] Debug mode enabled'));
|
|
16
16
|
}
|
|
17
|
-
const config = runner_1.AgentRunner.loadConfig(options.config);
|
|
17
|
+
const config = await runner_1.AgentRunner.loadConfig(options.config);
|
|
18
18
|
const runner = new runner_1.AgentRunner(config);
|
|
19
19
|
await runner.start();
|
|
20
20
|
}
|