skydive-cli 0.5.0-beta.9 → 0.5.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/CHANGELOG.md +13 -0
- package/README.md +61 -13
- package/dist/js/api-BFQ4PQDA.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +674 -307
- package/dist/js/{boot-Q-Kh3nn5.mjs → boot-DD4T-61U.mjs} +4558 -930
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
- package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-Ct0-JZSS.mjs +5 -0
- package/dist/js/daemon-CCgNLD0H.mjs +7 -0
- package/dist/js/{daemon-D21wQ7DI.mjs → daemon-Do1jU2UF.mjs} +123 -43
- package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
- package/dist/js/{daemon-client-DPUNjhBB.mjs → daemon-client-Dvad009G.mjs} +1 -1
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-C-f04uyE.mjs +208 -0
- package/dist/js/{profiler-BkCV__ao.mjs → install-CtAVvERm.mjs} +545 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
- package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
- package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
- package/dist/js/rest-Dc0EEok3.mjs +6 -0
- package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
- package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
- package/package.json +12 -4
- package/dist/js/api-DG5W6iwx.mjs +0 -131
- package/dist/js/client-BuU34IVE.mjs +0 -5
- package/dist/js/daemon-LSDSvMaC.mjs +0 -6
- package/dist/js/daemon-client-CUSq-Wuh.mjs +0 -7
- package/dist/js/forward-18QoL5dO.mjs +0 -68
- package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
- package/dist/js/rest-D29qNkto.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { r as warnOnRebindProtection } from "./tls-cert-Rua2oV7n.mjs";
|
|
3
|
+
import { l as startTlsForward, t as fetchForwardTarget } from "./api-BFQ4PQDA.mjs";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import { WebSocket, createWebSocketStream } from "ws";
|
|
6
|
+
|
|
7
|
+
//#region src/chat/portal/tunnel-pool.ts
|
|
8
|
+
const MAX_AGE_MS = 4e4;
|
|
9
|
+
const SWEEP_INTERVAL_MS = 1e4;
|
|
10
|
+
const REFILL_BACKOFF_MS = 15e3;
|
|
11
|
+
/**
|
|
12
|
+
* Pre-warmed tunnel pool for the forward portal's accept path.
|
|
13
|
+
*
|
|
14
|
+
* Dialing a tunnel costs a full edge round-trip (~300ms measured). The pool
|
|
15
|
+
* pays that in the background so accept can adopt a warm tunnel and move the
|
|
16
|
+
* first byte immediately. Target starts at `initialSize` and grows by one on
|
|
17
|
+
* each miss, never above `maxSize` — a miss still cold-dials (today's path);
|
|
18
|
+
* growth only helps the next wave. No decay: a runaway client can hold
|
|
19
|
+
* `maxSize` idle edge sockets until `forward` exits.
|
|
20
|
+
*
|
|
21
|
+
* `dial` must resolve with a stream that is already paused, so any bytes a
|
|
22
|
+
* server-first protocol sends before adoption buffer instead of dropping.
|
|
23
|
+
*/
|
|
24
|
+
function createTunnelPool({ initialSize, maxSize, dial, log }) {
|
|
25
|
+
let entries = [];
|
|
26
|
+
let dialsInFlight = 0;
|
|
27
|
+
let backoffUntil = 0;
|
|
28
|
+
let closed = false;
|
|
29
|
+
let targetSize = initialSize;
|
|
30
|
+
function refill() {
|
|
31
|
+
if (closed) return;
|
|
32
|
+
if (Date.now() < backoffUntil) return;
|
|
33
|
+
while (entries.length + dialsInFlight < targetSize) {
|
|
34
|
+
dialsInFlight += 1;
|
|
35
|
+
dial().then((tunnel) => {
|
|
36
|
+
dialsInFlight -= 1;
|
|
37
|
+
if (closed) {
|
|
38
|
+
tunnel.close();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const entry = {
|
|
42
|
+
tunnel,
|
|
43
|
+
dialedAt: Date.now(),
|
|
44
|
+
dead: false
|
|
45
|
+
};
|
|
46
|
+
tunnel.stream.once("close", () => {
|
|
47
|
+
entry.dead = true;
|
|
48
|
+
});
|
|
49
|
+
tunnel.stream.once("error", () => {
|
|
50
|
+
entry.dead = true;
|
|
51
|
+
});
|
|
52
|
+
entries.push(entry);
|
|
53
|
+
}).catch((err) => {
|
|
54
|
+
dialsInFlight -= 1;
|
|
55
|
+
backoffUntil = Date.now() + REFILL_BACKOFF_MS;
|
|
56
|
+
log(`forward: pool dial failed (backing off): ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function sweep() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const keep = [];
|
|
63
|
+
for (const entry of entries) if (entry.dead || now - entry.dialedAt > MAX_AGE_MS) entry.tunnel.close();
|
|
64
|
+
else keep.push(entry);
|
|
65
|
+
entries = keep;
|
|
66
|
+
refill();
|
|
67
|
+
}
|
|
68
|
+
const sweeper = setInterval(sweep, SWEEP_INTERVAL_MS);
|
|
69
|
+
sweeper.unref();
|
|
70
|
+
refill();
|
|
71
|
+
return {
|
|
72
|
+
take: () => {
|
|
73
|
+
while (entries.length > 0) {
|
|
74
|
+
const entry = entries.pop();
|
|
75
|
+
if (!entry) break;
|
|
76
|
+
if (entry.dead || entry.tunnel.stream.destroyed) continue;
|
|
77
|
+
refill();
|
|
78
|
+
return entry.tunnel;
|
|
79
|
+
}
|
|
80
|
+
if (targetSize < maxSize) targetSize += 1;
|
|
81
|
+
refill();
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
close: () => {
|
|
85
|
+
closed = true;
|
|
86
|
+
clearInterval(sweeper);
|
|
87
|
+
for (const entry of entries) entry.tunnel.close();
|
|
88
|
+
entries = [];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/chat/portal/forward.ts
|
|
95
|
+
const TARGET_REFRESH_SAFETY_MS = 12e4;
|
|
96
|
+
const INITIAL_POOL_SIZE = 8;
|
|
97
|
+
const MAX_POOL_SIZE = 16;
|
|
98
|
+
/**
|
|
99
|
+
* The reverse portal's client half: listen on the local machine's loopback and
|
|
100
|
+
* pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
|
|
101
|
+
* which pipes to the sandbox's own loopback. The daemon is reached through the
|
|
102
|
+
* agent-webserver edge Worker (sandboxes have public ingress disabled; the
|
|
103
|
+
* Worker owns boot-resolution and injects the sandbox edge-auth token), so a
|
|
104
|
+
* sandbox recycle just costs the next connection a cold-start wait rather
|
|
105
|
+
* than invalidating the forward.
|
|
106
|
+
*/
|
|
107
|
+
async function startForward({ auth, agentId, localPort, targetPort, tlsCertSource, log }) {
|
|
108
|
+
let target = await fetchForwardTarget(auth, agentId);
|
|
109
|
+
let mintedAt = Date.now();
|
|
110
|
+
async function freshTarget() {
|
|
111
|
+
const ttlMs = target.expiresInSeconds * 1e3;
|
|
112
|
+
if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
|
|
113
|
+
target = await fetchForwardTarget(auth, agentId);
|
|
114
|
+
mintedAt = Date.now();
|
|
115
|
+
}
|
|
116
|
+
return target;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Dial one tunnel through the edge to the daemon's /portal/tcp. Resolves
|
|
120
|
+
* once the upgrade completes, with the duplex PAUSED — the pool holds it
|
|
121
|
+
* warm without dropping any early bytes, and adoption resumes it by piping.
|
|
122
|
+
*/
|
|
123
|
+
async function dialTunnel() {
|
|
124
|
+
const resolved = await freshTarget();
|
|
125
|
+
const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
ws.once("error", reject);
|
|
128
|
+
ws.once("open", () => {
|
|
129
|
+
ws.removeListener("error", reject);
|
|
130
|
+
const stream = createWebSocketStream(ws);
|
|
131
|
+
stream.pause();
|
|
132
|
+
resolve({
|
|
133
|
+
stream,
|
|
134
|
+
close: () => stream.destroy()
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const pool = createTunnelPool({
|
|
140
|
+
initialSize: INITIAL_POOL_SIZE,
|
|
141
|
+
maxSize: MAX_POOL_SIZE,
|
|
142
|
+
dial: dialTunnel,
|
|
143
|
+
log
|
|
144
|
+
});
|
|
145
|
+
const server = net.createServer((sock) => {
|
|
146
|
+
sock.pause();
|
|
147
|
+
const warm = pool.take();
|
|
148
|
+
if (warm) {
|
|
149
|
+
warm.stream.on("error", () => sock.destroy());
|
|
150
|
+
sock.on("error", () => warm.stream.destroy());
|
|
151
|
+
sock.pipe(warm.stream).pipe(sock);
|
|
152
|
+
sock.resume();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
(async () => {
|
|
156
|
+
let tunnel;
|
|
157
|
+
try {
|
|
158
|
+
tunnel = await dialTunnel();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
log(`forward: tunnel connect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
161
|
+
sock.destroy();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
tunnel.stream.on("error", () => sock.destroy());
|
|
165
|
+
sock.on("error", () => tunnel.stream.destroy());
|
|
166
|
+
sock.pipe(tunnel.stream).pipe(sock);
|
|
167
|
+
sock.resume();
|
|
168
|
+
})();
|
|
169
|
+
});
|
|
170
|
+
await new Promise((resolve, reject) => {
|
|
171
|
+
server.once("error", reject);
|
|
172
|
+
server.listen(localPort, "127.0.0.1", () => {
|
|
173
|
+
server.removeListener("error", reject);
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
const addr = server.address();
|
|
178
|
+
const boundPort = addr && typeof addr === "object" ? addr.port : localPort;
|
|
179
|
+
let tls = null;
|
|
180
|
+
if (tlsCertSource) try {
|
|
181
|
+
const cert = await tlsCertSource();
|
|
182
|
+
if (cert) {
|
|
183
|
+
if (cert.hostname !== "localhost") await warnOnRebindProtection(cert.hostname, log);
|
|
184
|
+
tls = await startTlsForward({
|
|
185
|
+
cert,
|
|
186
|
+
localPort: boundPort,
|
|
187
|
+
targetPort,
|
|
188
|
+
acquireTunnel: async () => pool.take() ?? dialTunnel(),
|
|
189
|
+
log
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log(`forward: TLS origin unavailable (${err instanceof Error ? err.message : String(err)}); continuing with http only`);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
port: boundPort,
|
|
197
|
+
tls,
|
|
198
|
+
close: () => new Promise((resolve) => {
|
|
199
|
+
pool.close();
|
|
200
|
+
(tls ? tls.close() : Promise.resolve()).then(() => {
|
|
201
|
+
server.close(() => resolve());
|
|
202
|
+
});
|
|
203
|
+
})
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
export { startForward };
|