@agentproto/acp 0.1.0-alpha.2 → 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/client/index.d.ts +35 -1
- package/dist/client/index.mjs +45 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/tunnel/index.d.ts +383 -6
- package/dist/tunnel/index.mjs +740 -23
- package/dist/tunnel/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/tunnel/index.mjs
CHANGED
|
@@ -18,6 +18,12 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
|
18
18
|
"resize",
|
|
19
19
|
"http_request",
|
|
20
20
|
"http_response",
|
|
21
|
+
"http_response_head",
|
|
22
|
+
"http_response_chunk",
|
|
23
|
+
"ws_open",
|
|
24
|
+
"ws_open_ack",
|
|
25
|
+
"ws_message",
|
|
26
|
+
"ws_close",
|
|
21
27
|
"ping",
|
|
22
28
|
"pong",
|
|
23
29
|
"error",
|
|
@@ -25,7 +31,8 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
|
25
31
|
"spawned",
|
|
26
32
|
"stdout",
|
|
27
33
|
"stderr",
|
|
28
|
-
"exit"
|
|
34
|
+
"exit",
|
|
35
|
+
"reconnect_soon"
|
|
29
36
|
]);
|
|
30
37
|
function parseFrame(raw) {
|
|
31
38
|
let parsed;
|
|
@@ -51,13 +58,21 @@ function encodeData(bytes) {
|
|
|
51
58
|
function decodeData(data) {
|
|
52
59
|
return Buffer.from(data, "base64");
|
|
53
60
|
}
|
|
61
|
+
var DEFAULT_WS_DIAL_TIMEOUT_MS = 1e4;
|
|
62
|
+
var DEFAULT_HTTP_FORWARD_TIMEOUT_MS = 3e4;
|
|
54
63
|
function createTunnelServer(opts) {
|
|
55
64
|
const children = /* @__PURE__ */ new Map();
|
|
65
|
+
const ptyProcs = /* @__PURE__ */ new Map();
|
|
66
|
+
const upstreamWs = /* @__PURE__ */ new Map();
|
|
56
67
|
let closed = false;
|
|
57
68
|
opts.sink.send({
|
|
58
69
|
t: "hello",
|
|
59
70
|
version: TUNNEL_VERSION,
|
|
60
|
-
capabilities: {
|
|
71
|
+
capabilities: {
|
|
72
|
+
pty: opts.pty === true,
|
|
73
|
+
wsForward: opts.dialUpstreamWs !== void 0 && !!opts.httpUpstream,
|
|
74
|
+
...opts.tools && opts.tools.length ? { tools: opts.tools } : {}
|
|
75
|
+
},
|
|
61
76
|
label: opts.label,
|
|
62
77
|
daemon: {
|
|
63
78
|
name: "agentproto",
|
|
@@ -76,6 +91,15 @@ function createTunnelServer(opts) {
|
|
|
76
91
|
closed = true;
|
|
77
92
|
for (const [, child] of children) child.kill("SIGTERM");
|
|
78
93
|
children.clear();
|
|
94
|
+
for (const [, pty] of ptyProcs) pty.kill();
|
|
95
|
+
ptyProcs.clear();
|
|
96
|
+
for (const [, ws] of upstreamWs) {
|
|
97
|
+
try {
|
|
98
|
+
ws.close(1001, "tunnel_closed");
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
upstreamWs.clear();
|
|
79
103
|
offFrame();
|
|
80
104
|
offClose();
|
|
81
105
|
});
|
|
@@ -85,6 +109,11 @@ function createTunnelServer(opts) {
|
|
|
85
109
|
await handleSpawn(frame);
|
|
86
110
|
return;
|
|
87
111
|
case "stdin": {
|
|
112
|
+
const pty = ptyProcs.get(frame.execId);
|
|
113
|
+
if (pty) {
|
|
114
|
+
pty.write(Buffer.from(frame.data, "base64").toString("utf8"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
88
117
|
const child = children.get(frame.execId);
|
|
89
118
|
if (!child || !child.stdin) {
|
|
90
119
|
opts.sink.send({
|
|
@@ -99,6 +128,11 @@ function createTunnelServer(opts) {
|
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
101
130
|
case "kill": {
|
|
131
|
+
const pty = ptyProcs.get(frame.execId);
|
|
132
|
+
if (pty) {
|
|
133
|
+
pty.kill(frame.signal ?? "SIGTERM");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
102
136
|
const child = children.get(frame.execId);
|
|
103
137
|
if (!child) {
|
|
104
138
|
opts.sink.send({
|
|
@@ -112,14 +146,60 @@ function createTunnelServer(opts) {
|
|
|
112
146
|
child.kill(frame.signal ?? "SIGTERM");
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
|
-
case "resize":
|
|
149
|
+
case "resize": {
|
|
150
|
+
ptyProcs.get(frame.execId)?.resize(frame.cols, frame.rows);
|
|
116
151
|
return;
|
|
152
|
+
}
|
|
117
153
|
case "http_request":
|
|
118
154
|
await handleHttpRequest(frame);
|
|
119
155
|
return;
|
|
156
|
+
case "ws_open":
|
|
157
|
+
await handleWsOpen(frame);
|
|
158
|
+
return;
|
|
159
|
+
case "ws_message": {
|
|
160
|
+
const ws = upstreamWs.get(frame.reqId);
|
|
161
|
+
if (!ws) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
ws.send(decodeData(frame.data), { binary: frame.binary === true });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
+
opts.sink.send({
|
|
169
|
+
t: "ws_close",
|
|
170
|
+
reqId: frame.reqId,
|
|
171
|
+
code: 1011,
|
|
172
|
+
reason: `send_failed: ${message}`
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
ws.close(1011, "send_failed");
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
upstreamWs.delete(frame.reqId);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
case "ws_close": {
|
|
183
|
+
const ws = upstreamWs.get(frame.reqId);
|
|
184
|
+
if (!ws) return;
|
|
185
|
+
upstreamWs.delete(frame.reqId);
|
|
186
|
+
try {
|
|
187
|
+
ws.close(frame.code, frame.reason);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
120
192
|
case "ping":
|
|
121
193
|
opts.sink.send({ t: "pong", nonce: frame.nonce });
|
|
122
194
|
return;
|
|
195
|
+
case "reconnect_soon":
|
|
196
|
+
try {
|
|
197
|
+
opts.onReconnectSoon?.({
|
|
198
|
+
...frame.reasonMs !== void 0 ? { reasonMs: frame.reasonMs } : {}
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
123
203
|
case "pong":
|
|
124
204
|
case "error":
|
|
125
205
|
return;
|
|
@@ -155,6 +235,48 @@ function createTunnelServer(opts) {
|
|
|
155
235
|
});
|
|
156
236
|
return;
|
|
157
237
|
}
|
|
238
|
+
if (approved.pty && opts.spawnPty) {
|
|
239
|
+
let pty;
|
|
240
|
+
try {
|
|
241
|
+
pty = opts.spawnPty({
|
|
242
|
+
command: approved.command,
|
|
243
|
+
args: [...approved.args],
|
|
244
|
+
cwd: approved.cwd,
|
|
245
|
+
env: approved.env ? { ...approved.env } : void 0,
|
|
246
|
+
cols: approved.cols ?? 80,
|
|
247
|
+
rows: approved.rows ?? 24
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
251
|
+
opts.sink.send({ t: "error", execId: req.execId, code: "spawn_failed", message });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
ptyProcs.set(req.execId, pty);
|
|
255
|
+
opts.sink.send({ t: "spawned", execId: req.execId, pid: pty.pid });
|
|
256
|
+
pty.onData((data) => {
|
|
257
|
+
opts.sink.send({ t: "stdout", execId: req.execId, data: encodeData(data) });
|
|
258
|
+
});
|
|
259
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
260
|
+
opts.sink.send({
|
|
261
|
+
t: "exit",
|
|
262
|
+
execId: req.execId,
|
|
263
|
+
code: exitCode,
|
|
264
|
+
signal: signal != null ? String(signal) : null
|
|
265
|
+
});
|
|
266
|
+
ptyProcs.delete(req.execId);
|
|
267
|
+
});
|
|
268
|
+
if (opts.onChildSpawned) {
|
|
269
|
+
try {
|
|
270
|
+
opts.onChildSpawned({
|
|
271
|
+
execId: req.execId,
|
|
272
|
+
child: { pid: pty.pid },
|
|
273
|
+
request: approved
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
158
280
|
let child;
|
|
159
281
|
try {
|
|
160
282
|
child = spawn(approved.command, [...approved.args], {
|
|
@@ -223,6 +345,162 @@ function createTunnelServer(opts) {
|
|
|
223
345
|
children.delete(req.execId);
|
|
224
346
|
});
|
|
225
347
|
}
|
|
348
|
+
async function handleWsOpen(req) {
|
|
349
|
+
if (closed) return;
|
|
350
|
+
const reject = (status, code, message) => {
|
|
351
|
+
opts.sink.send({
|
|
352
|
+
t: "ws_open_ack",
|
|
353
|
+
reqId: req.reqId,
|
|
354
|
+
status,
|
|
355
|
+
error: { code, message }
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
if (!opts.dialUpstreamWs || !opts.httpUpstream) {
|
|
359
|
+
reject(
|
|
360
|
+
502,
|
|
361
|
+
"ws_upstream_not_configured",
|
|
362
|
+
"Daemon does not support WS forwarding \u2014 start with `--upstream ws://\u2026` (or upgrade)."
|
|
363
|
+
);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!req.path.startsWith("/") || req.path.startsWith("//") || req.path.includes("..")) {
|
|
367
|
+
reject(
|
|
368
|
+
400,
|
|
369
|
+
"invalid_path",
|
|
370
|
+
`Daemon WS forward requires a safe relative path; got '${req.path}'.`
|
|
371
|
+
);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
375
|
+
"connection",
|
|
376
|
+
"upgrade",
|
|
377
|
+
"sec-websocket-version",
|
|
378
|
+
"sec-websocket-key",
|
|
379
|
+
"sec-websocket-extensions",
|
|
380
|
+
"sec-websocket-protocol",
|
|
381
|
+
"host",
|
|
382
|
+
"content-length"
|
|
383
|
+
]);
|
|
384
|
+
const headers = {};
|
|
385
|
+
for (const [k, v] of Object.entries(req.headers ?? {})) {
|
|
386
|
+
if (HOP_BY_HOP.has(k.toLowerCase())) continue;
|
|
387
|
+
headers[k] = v;
|
|
388
|
+
}
|
|
389
|
+
let httpBase = opts.httpUpstream;
|
|
390
|
+
if (req.upstream) {
|
|
391
|
+
const resolved = await opts.resolveWsUpstream?.(req.upstream);
|
|
392
|
+
if (!resolved) {
|
|
393
|
+
reject(
|
|
394
|
+
502,
|
|
395
|
+
"unknown_ws_upstream",
|
|
396
|
+
`No WS upstream registered for '${req.upstream}'.`
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
httpBase = resolved;
|
|
401
|
+
}
|
|
402
|
+
const base = httpBase.replace(/^http(s?):\/\//, "ws$1://");
|
|
403
|
+
const url = `${base.replace(/\/$/, "")}${req.path}`;
|
|
404
|
+
const dialTimeoutMs = opts.wsDialTimeoutMs ?? DEFAULT_WS_DIAL_TIMEOUT_MS;
|
|
405
|
+
const dialAbort = new AbortController();
|
|
406
|
+
let dialTimedOut = false;
|
|
407
|
+
let dialTimer = null;
|
|
408
|
+
const dialPromise = opts.dialUpstreamWs({
|
|
409
|
+
url,
|
|
410
|
+
...req.protocols && req.protocols.length > 0 ? { protocols: req.protocols } : {},
|
|
411
|
+
headers,
|
|
412
|
+
signal: dialAbort.signal
|
|
413
|
+
});
|
|
414
|
+
const timeoutPromise = new Promise((_resolve, rej) => {
|
|
415
|
+
dialTimer = setTimeout(() => {
|
|
416
|
+
dialTimedOut = true;
|
|
417
|
+
dialAbort.abort(new Error("ws_dial_timeout"));
|
|
418
|
+
rej(new Error(`upstream WS dial timed out after ${dialTimeoutMs}ms`));
|
|
419
|
+
}, dialTimeoutMs);
|
|
420
|
+
if (typeof dialTimer !== "number") dialTimer.unref();
|
|
421
|
+
});
|
|
422
|
+
let upstream;
|
|
423
|
+
try {
|
|
424
|
+
upstream = await Promise.race([dialPromise, timeoutPromise]);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (dialTimer) clearTimeout(dialTimer);
|
|
427
|
+
if (dialTimedOut) {
|
|
428
|
+
dialPromise.then(
|
|
429
|
+
(sock) => {
|
|
430
|
+
try {
|
|
431
|
+
sock.close(1001, "dial_timeout");
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
() => {
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
reject(
|
|
439
|
+
504,
|
|
440
|
+
"upstream_ws_timeout",
|
|
441
|
+
`Upstream did not complete the WS upgrade within ${dialTimeoutMs}ms.`
|
|
442
|
+
);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
446
|
+
const m = message.match(/Unexpected server response: (\d+)/);
|
|
447
|
+
const status = m ? Number.parseInt(m[1], 10) : 502;
|
|
448
|
+
reject(status, "upstream_ws_failed", message);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (dialTimer) clearTimeout(dialTimer);
|
|
452
|
+
if (closed) {
|
|
453
|
+
try {
|
|
454
|
+
upstream.close(1001, "tunnel_closed");
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
upstreamWs.set(req.reqId, upstream);
|
|
460
|
+
opts.sink.send({
|
|
461
|
+
t: "ws_open_ack",
|
|
462
|
+
reqId: req.reqId,
|
|
463
|
+
status: 101,
|
|
464
|
+
...upstream.protocol ? { protocol: upstream.protocol } : {}
|
|
465
|
+
});
|
|
466
|
+
upstream.onMessage((data, isBinary) => {
|
|
467
|
+
if (!opts.sink.isOpen) return;
|
|
468
|
+
opts.sink.send({
|
|
469
|
+
t: "ws_message",
|
|
470
|
+
reqId: req.reqId,
|
|
471
|
+
data: encodeData(data),
|
|
472
|
+
binary: isBinary
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
upstream.onClose((code, reason) => {
|
|
476
|
+
if (!upstreamWs.has(req.reqId)) return;
|
|
477
|
+
upstreamWs.delete(req.reqId);
|
|
478
|
+
if (opts.sink.isOpen) {
|
|
479
|
+
opts.sink.send({
|
|
480
|
+
t: "ws_close",
|
|
481
|
+
reqId: req.reqId,
|
|
482
|
+
code,
|
|
483
|
+
...reason ? { reason } : {}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
upstream.onError((err) => {
|
|
488
|
+
if (!upstreamWs.has(req.reqId)) return;
|
|
489
|
+
upstreamWs.delete(req.reqId);
|
|
490
|
+
if (opts.sink.isOpen) {
|
|
491
|
+
opts.sink.send({
|
|
492
|
+
t: "ws_close",
|
|
493
|
+
reqId: req.reqId,
|
|
494
|
+
code: 1011,
|
|
495
|
+
reason: `upstream_error: ${err.message}`
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
upstream.close(1011, "upstream_error");
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
226
504
|
async function handleHttpRequest(req) {
|
|
227
505
|
if (closed) return;
|
|
228
506
|
const respond = (partial) => {
|
|
@@ -273,8 +551,17 @@ function createTunnelServer(opts) {
|
|
|
273
551
|
}
|
|
274
552
|
const url = `${opts.httpUpstream.replace(/\/$/, "")}${req.path}`;
|
|
275
553
|
const controller = new AbortController();
|
|
276
|
-
const timeoutMs = req.timeoutMs ??
|
|
277
|
-
|
|
554
|
+
const timeoutMs = req.timeoutMs ?? opts.httpForwardTimeoutMs ?? DEFAULT_HTTP_FORWARD_TIMEOUT_MS;
|
|
555
|
+
let timer = setTimeout(
|
|
556
|
+
() => controller.abort(),
|
|
557
|
+
timeoutMs
|
|
558
|
+
);
|
|
559
|
+
const clearTimer = () => {
|
|
560
|
+
if (timer) {
|
|
561
|
+
clearTimeout(timer);
|
|
562
|
+
timer = null;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
278
565
|
try {
|
|
279
566
|
const upstreamRes = await fetch(url, {
|
|
280
567
|
method: req.method,
|
|
@@ -284,12 +571,92 @@ function createTunnelServer(opts) {
|
|
|
284
571
|
// node fetch follows redirects by default — fine for MCP, the
|
|
285
572
|
// upstream gateway doesn't redirect anyway.
|
|
286
573
|
});
|
|
287
|
-
const buf = Buffer.from(await upstreamRes.arrayBuffer());
|
|
288
574
|
const outHeaders = {};
|
|
289
575
|
upstreamRes.headers.forEach((value, key) => {
|
|
290
576
|
if (HOP_BY_HOP.has(key.toLowerCase())) return;
|
|
291
577
|
outHeaders[key] = value;
|
|
292
578
|
});
|
|
579
|
+
const contentType = (outHeaders["content-type"] ?? "").toLowerCase();
|
|
580
|
+
const isStream = contentType.startsWith("text/event-stream") || contentType.includes("application/x-ndjson");
|
|
581
|
+
if (isStream && upstreamRes.body) {
|
|
582
|
+
clearTimer();
|
|
583
|
+
opts.sink.send({
|
|
584
|
+
t: "http_response_head",
|
|
585
|
+
reqId: req.reqId,
|
|
586
|
+
status: upstreamRes.status,
|
|
587
|
+
headers: outHeaders
|
|
588
|
+
});
|
|
589
|
+
const reader = upstreamRes.body.getReader();
|
|
590
|
+
const idleMs = opts.httpStreamIdleTimeoutMs;
|
|
591
|
+
const IDLE = /* @__PURE__ */ Symbol("idle");
|
|
592
|
+
const readChunk = async () => {
|
|
593
|
+
if (!idleMs) return reader.read();
|
|
594
|
+
let idleTimer = null;
|
|
595
|
+
const idle = new Promise((resolve) => {
|
|
596
|
+
idleTimer = setTimeout(() => resolve(IDLE), idleMs);
|
|
597
|
+
if (typeof idleTimer !== "number") idleTimer.unref();
|
|
598
|
+
});
|
|
599
|
+
try {
|
|
600
|
+
return await Promise.race([reader.read(), idle]);
|
|
601
|
+
} finally {
|
|
602
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
try {
|
|
606
|
+
while (true) {
|
|
607
|
+
const chunk = await readChunk();
|
|
608
|
+
if (chunk === IDLE) {
|
|
609
|
+
try {
|
|
610
|
+
await reader.cancel();
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
opts.sink.send({
|
|
614
|
+
t: "http_response_chunk",
|
|
615
|
+
reqId: req.reqId,
|
|
616
|
+
end: true,
|
|
617
|
+
error: {
|
|
618
|
+
code: "upstream_stream_idle_timeout",
|
|
619
|
+
message: `No data from upstream for ${idleMs}ms.`
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const { value, done } = chunk;
|
|
625
|
+
if (done) {
|
|
626
|
+
opts.sink.send({
|
|
627
|
+
t: "http_response_chunk",
|
|
628
|
+
reqId: req.reqId,
|
|
629
|
+
end: true
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (value && value.byteLength > 0) {
|
|
634
|
+
opts.sink.send({
|
|
635
|
+
t: "http_response_chunk",
|
|
636
|
+
reqId: req.reqId,
|
|
637
|
+
data: encodeData(Buffer.from(value))
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (!opts.sink.isOpen) {
|
|
641
|
+
try {
|
|
642
|
+
await reader.cancel();
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch (err) {
|
|
649
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
650
|
+
opts.sink.send({
|
|
651
|
+
t: "http_response_chunk",
|
|
652
|
+
reqId: req.reqId,
|
|
653
|
+
end: true,
|
|
654
|
+
error: { code: "upstream_stream_failed", message }
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const buf = Buffer.from(await upstreamRes.arrayBuffer());
|
|
293
660
|
respond({
|
|
294
661
|
status: upstreamRes.status,
|
|
295
662
|
headers: outHeaders,
|
|
@@ -306,7 +673,7 @@ function createTunnelServer(opts) {
|
|
|
306
673
|
}
|
|
307
674
|
});
|
|
308
675
|
} finally {
|
|
309
|
-
|
|
676
|
+
clearTimer();
|
|
310
677
|
}
|
|
311
678
|
}
|
|
312
679
|
return {
|
|
@@ -327,6 +694,8 @@ function createTunnelClient(opts) {
|
|
|
327
694
|
const childByExec = /* @__PURE__ */ new Map();
|
|
328
695
|
const spawnPending = /* @__PURE__ */ new Map();
|
|
329
696
|
const httpPending = /* @__PURE__ */ new Map();
|
|
697
|
+
const wsOpenPending = /* @__PURE__ */ new Map();
|
|
698
|
+
const wsByReq = /* @__PURE__ */ new Map();
|
|
330
699
|
const offFrame = opts.sink.onFrame((frame) => routeIncoming(frame));
|
|
331
700
|
const offClose = opts.sink.onClose(() => {
|
|
332
701
|
for (const [, duck] of childByExec) duck.__handleSinkClosed();
|
|
@@ -337,9 +706,29 @@ function createTunnelClient(opts) {
|
|
|
337
706
|
spawnPending.clear();
|
|
338
707
|
for (const [, p] of httpPending) {
|
|
339
708
|
clearTimeout(p.timer);
|
|
340
|
-
p.
|
|
709
|
+
if (p.mode === "buffered") {
|
|
710
|
+
p.reject(new Error("Tunnel closed before HTTP response arrived."));
|
|
711
|
+
} else {
|
|
712
|
+
if (!p.headEmitted) {
|
|
713
|
+
p.reject(new Error("Tunnel closed before HTTP response head arrived."));
|
|
714
|
+
} else if (p.controller && !p.ended) {
|
|
715
|
+
try {
|
|
716
|
+
p.controller.error(new Error("Tunnel closed mid-stream."));
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
341
721
|
}
|
|
342
722
|
httpPending.clear();
|
|
723
|
+
for (const [, p] of wsOpenPending) {
|
|
724
|
+
clearTimeout(p.timer);
|
|
725
|
+
p.reject(new Error("Tunnel closed before WS upgrade completed."));
|
|
726
|
+
}
|
|
727
|
+
wsOpenPending.clear();
|
|
728
|
+
for (const [, duck] of wsByReq) {
|
|
729
|
+
duck.__handleSinkClosed();
|
|
730
|
+
}
|
|
731
|
+
wsByReq.clear();
|
|
343
732
|
offFrame();
|
|
344
733
|
offClose();
|
|
345
734
|
});
|
|
@@ -396,12 +785,171 @@ function createTunnelClient(opts) {
|
|
|
396
785
|
if (!pending) return;
|
|
397
786
|
httpPending.delete(frame.reqId);
|
|
398
787
|
clearTimeout(pending.timer);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
788
|
+
const body = frame.body ? decodeData(frame.body) : Buffer.alloc(0);
|
|
789
|
+
if (pending.mode === "buffered") {
|
|
790
|
+
pending.resolve({
|
|
791
|
+
status: frame.status,
|
|
792
|
+
headers: frame.headers ?? {},
|
|
793
|
+
body,
|
|
794
|
+
...frame.error ? { error: frame.error } : {}
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
const stream = new ReadableStream({
|
|
798
|
+
start(controller) {
|
|
799
|
+
if (body.length > 0) controller.enqueue(new Uint8Array(body));
|
|
800
|
+
controller.close();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
pending.resolveHead({
|
|
804
|
+
status: frame.status,
|
|
805
|
+
headers: frame.headers ?? {},
|
|
806
|
+
body: stream
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
case "http_response_head": {
|
|
812
|
+
const pending = httpPending.get(frame.reqId);
|
|
813
|
+
if (!pending) return;
|
|
814
|
+
const headers = frame.headers ?? {};
|
|
815
|
+
if (pending.mode === "stream") {
|
|
816
|
+
const stream = new ReadableStream({
|
|
817
|
+
start(controller) {
|
|
818
|
+
pending.controller = controller;
|
|
819
|
+
for (const chunk of pending.pendingChunks) {
|
|
820
|
+
if (chunk.length > 0) controller.enqueue(new Uint8Array(chunk));
|
|
821
|
+
}
|
|
822
|
+
pending.pendingChunks = [];
|
|
823
|
+
},
|
|
824
|
+
cancel: () => {
|
|
825
|
+
if (httpPending.get(frame.reqId) === pending) {
|
|
826
|
+
clearTimeout(pending.timer);
|
|
827
|
+
httpPending.delete(frame.reqId);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
pending.headEmitted = true;
|
|
832
|
+
clearTimeout(pending.timer);
|
|
833
|
+
pending.resolveHead({
|
|
834
|
+
status: frame.status,
|
|
835
|
+
headers,
|
|
836
|
+
body: stream
|
|
837
|
+
});
|
|
838
|
+
} else {
|
|
839
|
+
pending.streamHead = { status: frame.status, headers };
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
case "http_response_chunk": {
|
|
844
|
+
const pending = httpPending.get(frame.reqId);
|
|
845
|
+
if (!pending) return;
|
|
846
|
+
const data = frame.data ? decodeData(frame.data) : Buffer.alloc(0);
|
|
847
|
+
if (pending.mode === "stream") {
|
|
848
|
+
if (frame.error) {
|
|
849
|
+
try {
|
|
850
|
+
pending.controller?.error(
|
|
851
|
+
new Error(`${frame.error.code}: ${frame.error.message}`)
|
|
852
|
+
);
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
pending.ended = true;
|
|
856
|
+
clearTimeout(pending.timer);
|
|
857
|
+
httpPending.delete(frame.reqId);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (pending.controller) {
|
|
861
|
+
if (data.length > 0) {
|
|
862
|
+
try {
|
|
863
|
+
pending.controller.enqueue(new Uint8Array(data));
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (frame.end) {
|
|
868
|
+
try {
|
|
869
|
+
pending.controller.close();
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
pending.ended = true;
|
|
873
|
+
clearTimeout(pending.timer);
|
|
874
|
+
httpPending.delete(frame.reqId);
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
if (data.length > 0) pending.pendingChunks.push(data);
|
|
878
|
+
if (frame.end) {
|
|
879
|
+
pending.ended = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
if (data.length > 0) pending.streamChunks.push(data);
|
|
884
|
+
if (frame.error) {
|
|
885
|
+
clearTimeout(pending.timer);
|
|
886
|
+
httpPending.delete(frame.reqId);
|
|
887
|
+
const concatenated = Buffer.concat(pending.streamChunks);
|
|
888
|
+
pending.resolve({
|
|
889
|
+
status: pending.streamHead?.status ?? 502,
|
|
890
|
+
headers: pending.streamHead?.headers ?? {},
|
|
891
|
+
body: concatenated,
|
|
892
|
+
error: frame.error
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (frame.end) {
|
|
897
|
+
clearTimeout(pending.timer);
|
|
898
|
+
httpPending.delete(frame.reqId);
|
|
899
|
+
const concatenated = Buffer.concat(pending.streamChunks);
|
|
900
|
+
pending.resolve({
|
|
901
|
+
status: pending.streamHead?.status ?? 200,
|
|
902
|
+
headers: pending.streamHead?.headers ?? {},
|
|
903
|
+
body: concatenated
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
case "ws_open_ack": {
|
|
910
|
+
const pending = wsOpenPending.get(frame.reqId);
|
|
911
|
+
if (!pending) return;
|
|
912
|
+
wsOpenPending.delete(frame.reqId);
|
|
913
|
+
clearTimeout(pending.timer);
|
|
914
|
+
if (frame.error || frame.status !== 101) {
|
|
915
|
+
const code = frame.error?.code ?? `http_${frame.status}`;
|
|
916
|
+
const msg = frame.error?.message ?? `Upstream WS upgrade returned ${frame.status}`;
|
|
917
|
+
pending.reject(new Error(`${code}: ${msg}`));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const duck = new TunnelWebSocketDuck(
|
|
921
|
+
frame.reqId,
|
|
922
|
+
frame.protocol ?? null,
|
|
923
|
+
opts.sink,
|
|
924
|
+
() => wsByReq.delete(frame.reqId)
|
|
925
|
+
);
|
|
926
|
+
wsByReq.set(frame.reqId, duck);
|
|
927
|
+
pending.resolve(duck);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
case "ws_message": {
|
|
931
|
+
const duck = wsByReq.get(frame.reqId);
|
|
932
|
+
if (!duck) return;
|
|
933
|
+
duck.__handleMessage(decodeData(frame.data), frame.binary === true);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
case "ws_close": {
|
|
937
|
+
const pending = wsOpenPending.get(frame.reqId);
|
|
938
|
+
if (pending) {
|
|
939
|
+
wsOpenPending.delete(frame.reqId);
|
|
940
|
+
clearTimeout(pending.timer);
|
|
941
|
+
pending.reject(
|
|
942
|
+
new Error(
|
|
943
|
+
`WS closed before upgrade (code ${frame.code}${frame.reason ? `: ${frame.reason}` : ""})`
|
|
944
|
+
)
|
|
945
|
+
);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const duck = wsByReq.get(frame.reqId);
|
|
949
|
+
if (duck) {
|
|
950
|
+
wsByReq.delete(frame.reqId);
|
|
951
|
+
duck.__handleClose(frame.code, frame.reason ?? "");
|
|
952
|
+
}
|
|
405
953
|
return;
|
|
406
954
|
}
|
|
407
955
|
case "ping":
|
|
@@ -413,6 +961,7 @@ function createTunnelClient(opts) {
|
|
|
413
961
|
case "kill":
|
|
414
962
|
case "resize":
|
|
415
963
|
case "http_request":
|
|
964
|
+
case "ws_open":
|
|
416
965
|
return;
|
|
417
966
|
}
|
|
418
967
|
}
|
|
@@ -461,7 +1010,8 @@ function createTunnelClient(opts) {
|
|
|
461
1010
|
args,
|
|
462
1011
|
cwd: spawnOpts?.cwd,
|
|
463
1012
|
env: spawnOpts?.env,
|
|
464
|
-
pty: spawnOpts?.pty
|
|
1013
|
+
pty: spawnOpts?.pty,
|
|
1014
|
+
...spawnOpts?.pty ? { cols: spawnOpts.cols ?? 80, rows: spawnOpts.rows ?? 24 } : {}
|
|
465
1015
|
};
|
|
466
1016
|
const spawnedFrame = await new Promise(
|
|
467
1017
|
(resolve, reject) => {
|
|
@@ -498,7 +1048,86 @@ function createTunnelClient(opts) {
|
|
|
498
1048
|
)
|
|
499
1049
|
);
|
|
500
1050
|
}, timeoutMs + 5e3);
|
|
501
|
-
httpPending.set(reqId, {
|
|
1051
|
+
httpPending.set(reqId, {
|
|
1052
|
+
mode: "buffered",
|
|
1053
|
+
resolve,
|
|
1054
|
+
reject,
|
|
1055
|
+
timer,
|
|
1056
|
+
streamHead: null,
|
|
1057
|
+
streamChunks: []
|
|
1058
|
+
});
|
|
1059
|
+
opts.sink.send(frame);
|
|
1060
|
+
});
|
|
1061
|
+
},
|
|
1062
|
+
async forwardHttpStream(req) {
|
|
1063
|
+
if (!hello) await this.ready();
|
|
1064
|
+
const reqId = randomUUID();
|
|
1065
|
+
const headTimeoutMs = req.timeoutMs ?? 3e4;
|
|
1066
|
+
const body = req.body === void 0 ? void 0 : encodeData(
|
|
1067
|
+
typeof req.body === "string" ? req.body : Buffer.from(req.body)
|
|
1068
|
+
);
|
|
1069
|
+
const frame = {
|
|
1070
|
+
t: "http_request",
|
|
1071
|
+
reqId,
|
|
1072
|
+
method: req.method,
|
|
1073
|
+
path: req.path,
|
|
1074
|
+
...req.headers ? { headers: req.headers } : {},
|
|
1075
|
+
...body !== void 0 ? { body } : {},
|
|
1076
|
+
timeoutMs: 24 * 60 * 60 * 1e3
|
|
1077
|
+
// 24h — effectively "forever"
|
|
1078
|
+
};
|
|
1079
|
+
return new Promise((resolveHead, reject) => {
|
|
1080
|
+
const timer = setTimeout(() => {
|
|
1081
|
+
if (httpPending.has(reqId)) {
|
|
1082
|
+
httpPending.delete(reqId);
|
|
1083
|
+
reject(
|
|
1084
|
+
new Error(
|
|
1085
|
+
`Tunnel forwardHttpStream(${req.method} ${req.path}) head did not arrive within ${headTimeoutMs}ms.`
|
|
1086
|
+
)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}, headTimeoutMs);
|
|
1090
|
+
httpPending.set(reqId, {
|
|
1091
|
+
mode: "stream",
|
|
1092
|
+
resolveHead,
|
|
1093
|
+
reject,
|
|
1094
|
+
timer,
|
|
1095
|
+
controller: null,
|
|
1096
|
+
pendingChunks: [],
|
|
1097
|
+
headEmitted: false,
|
|
1098
|
+
ended: false
|
|
1099
|
+
});
|
|
1100
|
+
opts.sink.send(frame);
|
|
1101
|
+
});
|
|
1102
|
+
},
|
|
1103
|
+
async forwardWebSocket(req) {
|
|
1104
|
+
const h = await this.ready();
|
|
1105
|
+
if (h.capabilities.wsForward !== true) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Tunnel daemon '${h.label ?? "(unnamed)"}' does not advertise wsForward capability. Update the daemon to forward browser WS upgrades.`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const reqId = randomUUID();
|
|
1111
|
+
const timeoutMs = req.openTimeoutMs ?? 3e4;
|
|
1112
|
+
const frame = {
|
|
1113
|
+
t: "ws_open",
|
|
1114
|
+
reqId,
|
|
1115
|
+
path: req.path,
|
|
1116
|
+
...req.upstream ? { upstream: req.upstream } : {},
|
|
1117
|
+
...req.headers ? { headers: req.headers } : {},
|
|
1118
|
+
...req.protocols && req.protocols.length > 0 ? { protocols: req.protocols } : {}
|
|
1119
|
+
};
|
|
1120
|
+
return new Promise((resolve, reject) => {
|
|
1121
|
+
const timer = setTimeout(() => {
|
|
1122
|
+
if (!wsOpenPending.has(reqId)) return;
|
|
1123
|
+
wsOpenPending.delete(reqId);
|
|
1124
|
+
reject(
|
|
1125
|
+
new Error(
|
|
1126
|
+
`Tunnel forwardWebSocket(${req.path}) did not receive ws_open_ack within ${timeoutMs}ms.`
|
|
1127
|
+
)
|
|
1128
|
+
);
|
|
1129
|
+
}, timeoutMs);
|
|
1130
|
+
wsOpenPending.set(reqId, { resolve, reject, timer });
|
|
502
1131
|
opts.sink.send(frame);
|
|
503
1132
|
});
|
|
504
1133
|
},
|
|
@@ -513,11 +1142,13 @@ var TunnelChildDuck = class extends EventEmitter {
|
|
|
513
1142
|
stdin;
|
|
514
1143
|
stdout;
|
|
515
1144
|
stderr;
|
|
1145
|
+
sink;
|
|
516
1146
|
exited = false;
|
|
517
1147
|
constructor(execId, pid, sink) {
|
|
518
1148
|
super();
|
|
519
1149
|
this.execId = execId;
|
|
520
1150
|
this.pid = pid > 0 ? pid : null;
|
|
1151
|
+
this.sink = sink;
|
|
521
1152
|
this.stdout = new Readable({ read() {
|
|
522
1153
|
} });
|
|
523
1154
|
this.stderr = new Readable({ read() {
|
|
@@ -540,10 +1171,6 @@ var TunnelChildDuck = class extends EventEmitter {
|
|
|
540
1171
|
cb();
|
|
541
1172
|
}
|
|
542
1173
|
});
|
|
543
|
-
Object.defineProperty(this, "__sink", {
|
|
544
|
-
value: sink,
|
|
545
|
-
enumerable: false
|
|
546
|
-
});
|
|
547
1174
|
}
|
|
548
1175
|
__pushStdout(buf) {
|
|
549
1176
|
this.stdout.push(buf);
|
|
@@ -575,14 +1202,104 @@ var TunnelChildDuck = class extends EventEmitter {
|
|
|
575
1202
|
}
|
|
576
1203
|
kill(signal = "SIGTERM") {
|
|
577
1204
|
if (this.exited) return false;
|
|
578
|
-
|
|
579
|
-
sink.send({
|
|
1205
|
+
this.sink.send({
|
|
580
1206
|
t: "kill",
|
|
581
1207
|
execId: this.execId,
|
|
582
1208
|
signal: typeof signal === "string" ? signal : `SIG${signal}`
|
|
583
1209
|
});
|
|
584
1210
|
return true;
|
|
585
1211
|
}
|
|
1212
|
+
resize(cols, rows) {
|
|
1213
|
+
if (this.exited) return;
|
|
1214
|
+
this.sink.send({ t: "resize", execId: this.execId, cols, rows });
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
var TunnelWebSocketDuck = class {
|
|
1218
|
+
protocol;
|
|
1219
|
+
_readyState = "open";
|
|
1220
|
+
reqId;
|
|
1221
|
+
sink;
|
|
1222
|
+
onCleanup;
|
|
1223
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
1224
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
1225
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
1226
|
+
constructor(reqId, protocol, sink, onCleanup) {
|
|
1227
|
+
this.reqId = reqId;
|
|
1228
|
+
this.protocol = protocol;
|
|
1229
|
+
this.sink = sink;
|
|
1230
|
+
this.onCleanup = onCleanup;
|
|
1231
|
+
}
|
|
1232
|
+
get readyState() {
|
|
1233
|
+
return this._readyState;
|
|
1234
|
+
}
|
|
1235
|
+
send(data) {
|
|
1236
|
+
if (this._readyState !== "open") return;
|
|
1237
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
1238
|
+
this.sink.send({
|
|
1239
|
+
t: "ws_message",
|
|
1240
|
+
reqId: this.reqId,
|
|
1241
|
+
data: encodeData(bytes),
|
|
1242
|
+
binary: typeof data !== "string"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
close(code = 1e3, reason) {
|
|
1246
|
+
if (this._readyState === "closed") return;
|
|
1247
|
+
if (this._readyState === "open") {
|
|
1248
|
+
this._readyState = "closing";
|
|
1249
|
+
this.sink.send({
|
|
1250
|
+
t: "ws_close",
|
|
1251
|
+
reqId: this.reqId,
|
|
1252
|
+
code,
|
|
1253
|
+
...reason ? { reason } : {}
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
this.__handleClose(code, reason ?? "");
|
|
1257
|
+
}
|
|
1258
|
+
onMessage(listener) {
|
|
1259
|
+
this.messageListeners.add(listener);
|
|
1260
|
+
return () => this.messageListeners.delete(listener);
|
|
1261
|
+
}
|
|
1262
|
+
onClose(listener) {
|
|
1263
|
+
this.closeListeners.add(listener);
|
|
1264
|
+
return () => this.closeListeners.delete(listener);
|
|
1265
|
+
}
|
|
1266
|
+
onError(listener) {
|
|
1267
|
+
this.errorListeners.add(listener);
|
|
1268
|
+
return () => this.errorListeners.delete(listener);
|
|
1269
|
+
}
|
|
1270
|
+
__handleMessage(data, isBinary) {
|
|
1271
|
+
if (this._readyState !== "open") return;
|
|
1272
|
+
for (const l of this.messageListeners) {
|
|
1273
|
+
try {
|
|
1274
|
+
l(data, isBinary);
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
__handleClose(code, reason) {
|
|
1280
|
+
if (this._readyState === "closed") return;
|
|
1281
|
+
this._readyState = "closed";
|
|
1282
|
+
this.onCleanup();
|
|
1283
|
+
for (const l of this.closeListeners) {
|
|
1284
|
+
try {
|
|
1285
|
+
l(code, reason);
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
this.messageListeners.clear();
|
|
1290
|
+
this.closeListeners.clear();
|
|
1291
|
+
this.errorListeners.clear();
|
|
1292
|
+
}
|
|
1293
|
+
__handleSinkClosed() {
|
|
1294
|
+
if (this._readyState === "closed") return;
|
|
1295
|
+
for (const l of this.errorListeners) {
|
|
1296
|
+
try {
|
|
1297
|
+
l(new Error("Tunnel closed mid-WS."));
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
this.__handleClose(1006, "tunnel_closed");
|
|
1302
|
+
}
|
|
586
1303
|
};
|
|
587
1304
|
|
|
588
1305
|
// src/tunnel/ws-adapter.ts
|
|
@@ -646,6 +1363,6 @@ function wrapWebSocket(ws) {
|
|
|
646
1363
|
};
|
|
647
1364
|
}
|
|
648
1365
|
|
|
649
|
-
export { TUNNEL_VERSION, createTunnelClient, createTunnelServer, decodeData, encodeData, encodeFrame, parseFrame, wrapWebSocket };
|
|
1366
|
+
export { DEFAULT_HTTP_FORWARD_TIMEOUT_MS, DEFAULT_WS_DIAL_TIMEOUT_MS, TUNNEL_VERSION, createTunnelClient, createTunnelServer, decodeData, encodeData, encodeFrame, parseFrame, wrapWebSocket };
|
|
650
1367
|
//# sourceMappingURL=index.mjs.map
|
|
651
1368
|
//# sourceMappingURL=index.mjs.map
|