svamp-cli 0.1.85 → 0.1.87
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/package.json +1 -1
- package/dist/agentCommands-BrImgZsc.mjs +0 -263
- package/dist/api-BRbsyqJ4.mjs +0 -147
- package/dist/cli.mjs +0 -1535
- package/dist/commands-2Hgmysua.mjs +0 -593
- package/dist/commands-BYbuedOK.mjs +0 -469
- package/dist/commands-DJo7kCIf.mjs +0 -1899
- package/dist/commands-DJoYOM_1.mjs +0 -531
- package/dist/index.mjs +0 -20
- package/dist/package-BXisDMWj.mjs +0 -62
- package/dist/run-D0PoM5_v.mjs +0 -8260
- package/dist/run-Ds7dVmnU.mjs +0 -1103
- package/dist/staticServer-CWcmMF5V.mjs +0 -477
- package/dist/tunnel-BDKdemh0.mjs +0 -383
package/dist/tunnel-BDKdemh0.mjs
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import * as os from 'os';
|
|
2
|
-
import { requireSandboxApiEnv } from './api-BRbsyqJ4.mjs';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
|
|
5
|
-
const PING_INTERVAL_MS = 3e4;
|
|
6
|
-
const PONG_TIMEOUT_MS = 1e4;
|
|
7
|
-
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 20;
|
|
8
|
-
class TunnelClient {
|
|
9
|
-
ws = null;
|
|
10
|
-
options;
|
|
11
|
-
env;
|
|
12
|
-
sandboxId;
|
|
13
|
-
reconnectAttempts = 0;
|
|
14
|
-
maxReconnects;
|
|
15
|
-
destroyed = false;
|
|
16
|
-
pingTimer = null;
|
|
17
|
-
pongTimeoutTimer = null;
|
|
18
|
-
lastPongAt = 0;
|
|
19
|
-
requestCount = 0;
|
|
20
|
-
localWebSockets = /* @__PURE__ */ new Map();
|
|
21
|
-
// request_id → local WS connection
|
|
22
|
-
pendingBinaryBody = null;
|
|
23
|
-
// awaiting binary body frame
|
|
24
|
-
constructor(options) {
|
|
25
|
-
this.options = {
|
|
26
|
-
localHost: "localhost",
|
|
27
|
-
requestTimeout: 12e4,
|
|
28
|
-
...options
|
|
29
|
-
};
|
|
30
|
-
this.maxReconnects = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
31
|
-
this.env = options.env || requireSandboxApiEnv();
|
|
32
|
-
this.sandboxId = options.sandboxId || this.env.sandboxId || `local-${os.hostname()}-${process.pid}`;
|
|
33
|
-
}
|
|
34
|
-
/** Build the WebSocket URL for the tunnel endpoint. */
|
|
35
|
-
buildWsUrl() {
|
|
36
|
-
const baseUrl = this.env.apiUrl.replace(/\/+$/, "");
|
|
37
|
-
const wsBase = baseUrl.replace(/^http/, "ws");
|
|
38
|
-
const ns = this.env.namespace || "_tunnel";
|
|
39
|
-
const params = new URLSearchParams({
|
|
40
|
-
token: this.env.apiKey,
|
|
41
|
-
sandbox_id: this.sandboxId
|
|
42
|
-
});
|
|
43
|
-
return `${wsBase}/services/${ns}/${this.options.name}/tunnel?${params}`;
|
|
44
|
-
}
|
|
45
|
-
/** Connect to the tunnel endpoint. */
|
|
46
|
-
async connect() {
|
|
47
|
-
if (this.destroyed) return;
|
|
48
|
-
const url = this.buildWsUrl();
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
try {
|
|
51
|
-
this.ws = new WebSocket(url, {
|
|
52
|
-
maxPayload: 100 * 1024 * 1024
|
|
53
|
-
// 100MB for large binary bodies
|
|
54
|
-
});
|
|
55
|
-
} catch (err) {
|
|
56
|
-
reject(new Error(`Failed to create WebSocket: ${err.message}`));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
this.ws.on("open", () => {
|
|
60
|
-
this.reconnectAttempts = 0;
|
|
61
|
-
this.lastPongAt = Date.now();
|
|
62
|
-
this.startPingInterval();
|
|
63
|
-
this.options.onConnect?.();
|
|
64
|
-
resolve();
|
|
65
|
-
});
|
|
66
|
-
this.ws.on("message", (data, isBinary) => {
|
|
67
|
-
if (isBinary) {
|
|
68
|
-
this.handleBinaryBody(data);
|
|
69
|
-
} else {
|
|
70
|
-
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
71
|
-
this.handleMessage(raw);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
this.ws.on("close", () => {
|
|
75
|
-
this.stopPingInterval();
|
|
76
|
-
this.cleanupLocalWebSockets();
|
|
77
|
-
this.pendingBinaryBody = null;
|
|
78
|
-
this.options.onDisconnect?.();
|
|
79
|
-
if (!this.destroyed) {
|
|
80
|
-
this.scheduleReconnect();
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
this.ws.on("error", (err) => {
|
|
84
|
-
this.options.onError?.(err);
|
|
85
|
-
if (this.reconnectAttempts === 0) {
|
|
86
|
-
reject(err);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/** Disconnect and stop reconnecting. */
|
|
92
|
-
destroy() {
|
|
93
|
-
this.destroyed = true;
|
|
94
|
-
this.stopPingInterval();
|
|
95
|
-
this.cleanupLocalWebSockets();
|
|
96
|
-
if (this.ws) {
|
|
97
|
-
this.ws.close();
|
|
98
|
-
this.ws = null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/** Number of HTTP requests proxied. */
|
|
102
|
-
get totalRequests() {
|
|
103
|
-
return this.requestCount;
|
|
104
|
-
}
|
|
105
|
-
/** Number of active WebSocket connections being proxied. */
|
|
106
|
-
get activeWebSockets() {
|
|
107
|
-
return this.localWebSockets.size;
|
|
108
|
-
}
|
|
109
|
-
/** Whether the tunnel WebSocket is currently open. */
|
|
110
|
-
get isConnected() {
|
|
111
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
112
|
-
}
|
|
113
|
-
/** Snapshot of tunnel health for external monitoring. */
|
|
114
|
-
get status() {
|
|
115
|
-
return {
|
|
116
|
-
connected: this.ws?.readyState === WebSocket.OPEN,
|
|
117
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
118
|
-
lastPongAt: this.lastPongAt,
|
|
119
|
-
totalRequests: this.requestCount,
|
|
120
|
-
activeWebSockets: this.localWebSockets.size
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
// ── Message handling ────────────────────────────────────────────────
|
|
124
|
-
handleMessage(raw) {
|
|
125
|
-
let msg;
|
|
126
|
-
try {
|
|
127
|
-
msg = JSON.parse(raw);
|
|
128
|
-
} catch {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (!msg || typeof msg !== "object") return;
|
|
132
|
-
switch (msg.type) {
|
|
133
|
-
case "ping":
|
|
134
|
-
this.send({ type: "pong" });
|
|
135
|
-
break;
|
|
136
|
-
case "pong":
|
|
137
|
-
this.lastPongAt = Date.now();
|
|
138
|
-
this.clearPongTimeout();
|
|
139
|
-
break;
|
|
140
|
-
case "request":
|
|
141
|
-
this.options.onRequest?.(msg);
|
|
142
|
-
if (msg.has_body) {
|
|
143
|
-
this.pendingBinaryBody = msg;
|
|
144
|
-
} else {
|
|
145
|
-
this.proxyRequest(msg, Buffer.alloc(0)).catch((err) => {
|
|
146
|
-
this.options.onError?.(err);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
case "ws_open":
|
|
151
|
-
this.handleWsOpen(msg).catch((err) => {
|
|
152
|
-
this.options.onError?.(err);
|
|
153
|
-
});
|
|
154
|
-
break;
|
|
155
|
-
case "ws_data":
|
|
156
|
-
this.handleWsData(msg);
|
|
157
|
-
break;
|
|
158
|
-
case "ws_close":
|
|
159
|
-
this.handleWsClose(msg);
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
handleBinaryBody(data) {
|
|
164
|
-
const req = this.pendingBinaryBody;
|
|
165
|
-
this.pendingBinaryBody = null;
|
|
166
|
-
if (!req) return;
|
|
167
|
-
this.proxyRequest(req, data).catch((err) => {
|
|
168
|
-
this.options.onError?.(err);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
async proxyRequest(req, bodyBuffer) {
|
|
172
|
-
this.requestCount++;
|
|
173
|
-
const port = req.port || this.options.ports[0];
|
|
174
|
-
const url = `http://${this.options.localHost}:${port}${req.path}`;
|
|
175
|
-
const controller = new AbortController();
|
|
176
|
-
const timeout = setTimeout(() => controller.abort(), this.options.requestTimeout);
|
|
177
|
-
const forwardHeaders = { ...req.headers };
|
|
178
|
-
delete forwardHeaders["accept-encoding"];
|
|
179
|
-
delete forwardHeaders["Accept-Encoding"];
|
|
180
|
-
const init = {
|
|
181
|
-
method: req.method,
|
|
182
|
-
headers: forwardHeaders,
|
|
183
|
-
signal: controller.signal
|
|
184
|
-
};
|
|
185
|
-
if (bodyBuffer.byteLength > 0 && !["GET", "HEAD"].includes(req.method.toUpperCase())) {
|
|
186
|
-
init.body = bodyBuffer;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
const res = await fetch(url, init);
|
|
190
|
-
clearTimeout(timeout);
|
|
191
|
-
const headers = {};
|
|
192
|
-
res.headers.forEach((value, key) => {
|
|
193
|
-
headers[key] = value;
|
|
194
|
-
});
|
|
195
|
-
this.send({
|
|
196
|
-
type: "response",
|
|
197
|
-
id: req.id,
|
|
198
|
-
status: res.status,
|
|
199
|
-
headers,
|
|
200
|
-
streaming: true
|
|
201
|
-
});
|
|
202
|
-
if (res.body) {
|
|
203
|
-
const reader = res.body.getReader();
|
|
204
|
-
try {
|
|
205
|
-
while (true) {
|
|
206
|
-
const { done, value } = await reader.read();
|
|
207
|
-
if (done) break;
|
|
208
|
-
this.send({ type: "response_chunk", id: req.id });
|
|
209
|
-
this.sendBinary(Buffer.from(value));
|
|
210
|
-
}
|
|
211
|
-
} finally {
|
|
212
|
-
reader.releaseLock();
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
this.send({ type: "response_end", id: req.id });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
clearTimeout(timeout);
|
|
218
|
-
const status = err.name === "AbortError" ? 504 : 502;
|
|
219
|
-
const message = err.name === "AbortError" ? "Tunnel: request timeout" : `Tunnel: local service error: ${err.message}`;
|
|
220
|
-
this.send({
|
|
221
|
-
type: "response",
|
|
222
|
-
id: req.id,
|
|
223
|
-
status,
|
|
224
|
-
headers: { "content-type": "text/plain" },
|
|
225
|
-
streaming: true
|
|
226
|
-
});
|
|
227
|
-
this.send({ type: "response_chunk", id: req.id });
|
|
228
|
-
this.sendBinary(Buffer.from(message));
|
|
229
|
-
this.send({ type: "response_end", id: req.id });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// ── WebSocket proxying ───────────────────────────────────────────────
|
|
233
|
-
async handleWsOpen(msg) {
|
|
234
|
-
const port = msg.port || this.options.ports[0];
|
|
235
|
-
const localUrl = `ws://${this.options.localHost}:${port}${msg.path}`;
|
|
236
|
-
try {
|
|
237
|
-
const localWs = new WebSocket(localUrl, {
|
|
238
|
-
headers: msg.headers
|
|
239
|
-
});
|
|
240
|
-
this.localWebSockets.set(msg.id, localWs);
|
|
241
|
-
localWs.on("message", (data, isBinary) => {
|
|
242
|
-
const encoded = typeof data === "string" ? Buffer.from(data).toString("base64") : (data instanceof Buffer ? data : Buffer.from(data)).toString("base64");
|
|
243
|
-
this.send({
|
|
244
|
-
type: "ws_data",
|
|
245
|
-
id: msg.id,
|
|
246
|
-
data: encoded,
|
|
247
|
-
is_text: !isBinary
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
localWs.on("close", (code) => {
|
|
251
|
-
this.localWebSockets.delete(msg.id);
|
|
252
|
-
this.send({ type: "ws_close", id: msg.id, code: code || 1e3 });
|
|
253
|
-
});
|
|
254
|
-
localWs.on("error", () => {
|
|
255
|
-
this.localWebSockets.delete(msg.id);
|
|
256
|
-
this.send({ type: "ws_close", id: msg.id, code: 1011 });
|
|
257
|
-
});
|
|
258
|
-
} catch {
|
|
259
|
-
this.send({ type: "ws_close", id: msg.id, code: 1011 });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
handleWsData(msg) {
|
|
263
|
-
const localWs = this.localWebSockets.get(msg.id);
|
|
264
|
-
if (!localWs || localWs.readyState !== WebSocket.OPEN) return;
|
|
265
|
-
const buf = Buffer.from(msg.data, "base64");
|
|
266
|
-
try {
|
|
267
|
-
if (msg.is_text) {
|
|
268
|
-
localWs.send(buf.toString("utf8"));
|
|
269
|
-
} else {
|
|
270
|
-
localWs.send(buf);
|
|
271
|
-
}
|
|
272
|
-
} catch (err) {
|
|
273
|
-
this.options.onError?.(err);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
handleWsClose(msg) {
|
|
277
|
-
const localWs = this.localWebSockets.get(msg.id);
|
|
278
|
-
if (localWs) {
|
|
279
|
-
this.localWebSockets.delete(msg.id);
|
|
280
|
-
localWs.close(msg.code || 1e3);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
cleanupLocalWebSockets() {
|
|
284
|
-
for (const [id, ws] of this.localWebSockets) {
|
|
285
|
-
ws.close(1001);
|
|
286
|
-
}
|
|
287
|
-
this.localWebSockets.clear();
|
|
288
|
-
}
|
|
289
|
-
// ── WebSocket helpers ───────────────────────────────────────────────
|
|
290
|
-
send(msg) {
|
|
291
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
292
|
-
this.ws.send(JSON.stringify(msg));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
sendBinary(data) {
|
|
296
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
297
|
-
this.ws.send(data);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
startPingInterval() {
|
|
301
|
-
this.stopPingInterval();
|
|
302
|
-
this.pingTimer = setInterval(() => {
|
|
303
|
-
this.send({ type: "ping" });
|
|
304
|
-
this.schedulePongTimeout();
|
|
305
|
-
}, PING_INTERVAL_MS);
|
|
306
|
-
}
|
|
307
|
-
stopPingInterval() {
|
|
308
|
-
if (this.pingTimer) {
|
|
309
|
-
clearInterval(this.pingTimer);
|
|
310
|
-
this.pingTimer = null;
|
|
311
|
-
}
|
|
312
|
-
this.clearPongTimeout();
|
|
313
|
-
}
|
|
314
|
-
schedulePongTimeout() {
|
|
315
|
-
this.clearPongTimeout();
|
|
316
|
-
this.pongTimeoutTimer = setTimeout(() => {
|
|
317
|
-
this.options.onError?.(new Error("Tunnel: pong timeout \u2014 connection stale"));
|
|
318
|
-
if (this.ws) {
|
|
319
|
-
this.ws.terminate();
|
|
320
|
-
}
|
|
321
|
-
}, PONG_TIMEOUT_MS);
|
|
322
|
-
}
|
|
323
|
-
clearPongTimeout() {
|
|
324
|
-
if (this.pongTimeoutTimer) {
|
|
325
|
-
clearTimeout(this.pongTimeoutTimer);
|
|
326
|
-
this.pongTimeoutTimer = null;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
scheduleReconnect() {
|
|
330
|
-
if (this.maxReconnects > 0 && this.reconnectAttempts >= this.maxReconnects) {
|
|
331
|
-
this.options.onError?.(new Error(
|
|
332
|
-
`Tunnel disconnected: max reconnect attempts (${this.maxReconnects}) reached`
|
|
333
|
-
));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
this.reconnectAttempts++;
|
|
337
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
338
|
-
setTimeout(() => {
|
|
339
|
-
if (!this.destroyed) {
|
|
340
|
-
this.connect().catch((err) => {
|
|
341
|
-
this.options.onError?.(err);
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}, delay);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
async function runTunnel(name, ports) {
|
|
348
|
-
const portList = ports.join(", ");
|
|
349
|
-
const client = new TunnelClient({
|
|
350
|
-
name,
|
|
351
|
-
ports,
|
|
352
|
-
onConnect: () => {
|
|
353
|
-
console.log(`Tunnel connected: ${name} \u2192 localhost:[${portList}]`);
|
|
354
|
-
},
|
|
355
|
-
onDisconnect: () => {
|
|
356
|
-
console.log("Tunnel disconnected, reconnecting...");
|
|
357
|
-
},
|
|
358
|
-
onRequest: (req) => {
|
|
359
|
-
console.log(` ${req.method} :${req.port || ports[0]}${req.path}`);
|
|
360
|
-
},
|
|
361
|
-
onError: (err) => {
|
|
362
|
-
console.error(`Tunnel error: ${err.message}`);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
const cleanup = () => {
|
|
366
|
-
console.log("\nTunnel shutting down...");
|
|
367
|
-
client.destroy();
|
|
368
|
-
process.exit(0);
|
|
369
|
-
};
|
|
370
|
-
process.on("SIGINT", cleanup);
|
|
371
|
-
process.on("SIGTERM", cleanup);
|
|
372
|
-
try {
|
|
373
|
-
await client.connect();
|
|
374
|
-
console.log(`Tunnel is active. Press Ctrl+C to stop.`);
|
|
375
|
-
await new Promise(() => {
|
|
376
|
-
});
|
|
377
|
-
} catch (err) {
|
|
378
|
-
console.error(`Failed to establish tunnel: ${err.message}`);
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export { TunnelClient, runTunnel };
|