skydive-cli 0.5.0-beta.27 → 0.5.0-beta.28
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/js/bin.mjs +8 -8
- package/dist/js/{boot-DDmudCUI.mjs → boot-DPB2qX9x.mjs} +3 -3
- package/dist/js/{daemon-DGsTcIVx.mjs → daemon-B5SNuIhD.mjs} +1 -1
- package/dist/js/{daemon-DnADJ1gG.mjs → daemon-C5e4hKwL.mjs} +1 -1
- package/dist/js/daemon-client-C6d14JU1.mjs +7 -0
- package/dist/js/{daemon-client-CwkdmYOc.mjs → daemon-client-CneyOup7.mjs} +1 -1
- package/dist/js/forward-DL6DYqyc.mjs +187 -0
- package/dist/js/{profiler-DLIjDaz_.mjs → profiler-rC7AZSxL.mjs} +1 -1
- package/package.json +1 -1
- package/dist/js/daemon-client-JFzZrNRz.mjs +0 -7
- package/dist/js/forward-18QoL5dO.mjs +0 -68
package/dist/js/bin.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-
|
|
2
|
+
import { A as takenAliasNames, B as name, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, I as getSessionIdentity, L as listWorkspaces, M as defaultInstallEnv, N as detectShell, O as installAgentAlias, P as ensureActiveOrganization, R as resolveWorkspaceId, S as themes, T as machineOsFromPlatform, V as version, a as installCrashHandler, j as SUPPORTED_SHELLS, k as slugifyAliasName, t as maybeStartProfiling, u as brandHelpArt, w as buildImportSeedPrompt, z as setActiveWorkspace } from "./profiler-rC7AZSxL.mjs";
|
|
3
3
|
import { A as getShareMachineDefault, C as getConfigPath, D as getPromptHistoryPath, E as getPreference, F as resolveAppUrl, H as saveSession, I as resolveChatAuth, L as resolveConfig, M as getStoredApiKeyWorkspaceName, N as getUpdateCheckDisabled, R as resolveManagementAuth, S as deleteConfig, T as getLastSeenVersion, V as saveConfig, W as setLastSeenVersion, _ as API_KEY_FAMILY_PREFIX, g as API_KEYS_URL, i as resolveAgent$1, j as getStoredApiKeyId, v as API_KEY_PREFIX, w as getDefaultAgent, x as PREFERENCES, y as DEFAULT_API_URL, z as resolveSession } from "./print-DYiQC5IW.mjs";
|
|
4
4
|
import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
|
|
5
5
|
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
6
6
|
import { t as createRestClient } from "./rest-xXfqT4SX.mjs";
|
|
7
7
|
import "./billing-blocked-2wju4gC_.mjs";
|
|
8
8
|
import { r as resolveMachineIdentity } from "./client-DbqRBquD.mjs";
|
|
9
|
-
import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-
|
|
9
|
+
import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-B5SNuIhD.mjs";
|
|
10
10
|
import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
|
|
11
11
|
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
12
12
|
import { hideBin } from "yargs/helpers";
|
|
@@ -1584,7 +1584,7 @@ const importCommand = {
|
|
|
1584
1584
|
process.exit(1);
|
|
1585
1585
|
}
|
|
1586
1586
|
}
|
|
1587
|
-
const { runChat } = await import("./boot-
|
|
1587
|
+
const { runChat } = await import("./boot-DPB2qX9x.mjs");
|
|
1588
1588
|
await runChat({
|
|
1589
1589
|
appUrl,
|
|
1590
1590
|
sessionToken: session.value.sessionToken,
|
|
@@ -2029,7 +2029,7 @@ const chatCommand = {
|
|
|
2029
2029
|
sessionToken: auth.value.token,
|
|
2030
2030
|
agentSelector: argv.agent ?? null
|
|
2031
2031
|
});
|
|
2032
|
-
const { runChat } = await import("./boot-
|
|
2032
|
+
const { runChat } = await import("./boot-DPB2qX9x.mjs");
|
|
2033
2033
|
await runChat({
|
|
2034
2034
|
appUrl: auth.value.appUrl,
|
|
2035
2035
|
sessionToken: auth.value.token,
|
|
@@ -2411,7 +2411,7 @@ const switchCommand = {
|
|
|
2411
2411
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
2412
2412
|
process.exit(1);
|
|
2413
2413
|
}
|
|
2414
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
2414
|
+
const { runWorkspacePicker } = await import("./boot-DPB2qX9x.mjs");
|
|
2415
2415
|
await runWorkspacePicker(session);
|
|
2416
2416
|
return;
|
|
2417
2417
|
}
|
|
@@ -2490,7 +2490,7 @@ const openCommand = {
|
|
|
2490
2490
|
const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
2491
2491
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
2492
2492
|
const { machineName } = await resolveMachineIdentity(null);
|
|
2493
|
-
const { PortalDaemonClient } = await import("./daemon-client-
|
|
2493
|
+
const { PortalDaemonClient } = await import("./daemon-client-C6d14JU1.mjs");
|
|
2494
2494
|
let lastLine = "";
|
|
2495
2495
|
let signalConnected;
|
|
2496
2496
|
const connected = new Promise((resolve) => {
|
|
@@ -2736,7 +2736,7 @@ const forwardCommand = {
|
|
|
2736
2736
|
const agent = resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent ?? null);
|
|
2737
2737
|
const targetPort = argv.port;
|
|
2738
2738
|
const localPort = argv["local-port"] ?? targetPort;
|
|
2739
|
-
const { startForward } = await import("./forward-
|
|
2739
|
+
const { startForward } = await import("./forward-DL6DYqyc.mjs");
|
|
2740
2740
|
const listener = await startForward({
|
|
2741
2741
|
auth: session,
|
|
2742
2742
|
agentId: agent.id,
|
|
@@ -4336,7 +4336,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
|
4336
4336
|
process.exit(0);
|
|
4337
4337
|
}
|
|
4338
4338
|
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
4339
|
-
const { runPortalDaemon } = await import("./daemon-
|
|
4339
|
+
const { runPortalDaemon } = await import("./daemon-C5e4hKwL.mjs");
|
|
4340
4340
|
runPortalDaemon(process.argv);
|
|
4341
4341
|
} else runCli();
|
|
4342
4342
|
function runCli() {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-
|
|
2
|
+
import { A as takenAliasNames, C as themesForMode, D as dedupeAliasName, E as aliasActivationHint, F as getActiveWorkspaceId, L as listWorkspaces, N as detectShell, O as installAgentAlias, T as machineOsFromPlatform, _ as theme, a as installCrashHandler, b as themeModeFromColorFgBg, c as MARK_CELLS, d as splashFitsWidth, f as DEFAULT_THEME_ID, g as noColorRequested, h as monoTheme, i as writeArtifact, k as slugifyAliasName, l as WORDMARK, m as findTheme, n as profilingEnabled, o as buildCrashReport, p as applyTheme, r as record, s as writeCrashReport, v as themeForMode, x as themeVersion, y as themeMode, z as setActiveWorkspace } from "./profiler-rC7AZSxL.mjs";
|
|
3
3
|
import { B as resolveWebUrl, C as getConfigPath, O as getReviewStateDir, P as recordDefaultAgent, U as saveTheme, b as DEFAULT_APP_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, h as specKeyFor, i as resolveAgent, k as getSavedTheme, l as parseExternalOauthConnectParams, m as parseConnectCard, p as computeSettledLabel, s as MASK_CHAR, u as parseOauthConnectParams, y as DEFAULT_API_URL } from "./print-DYiQC5IW.mjs";
|
|
4
4
|
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
5
5
|
import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-xXfqT4SX.mjs";
|
|
6
6
|
import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
|
|
7
7
|
import { t as PortalClient } from "./client-DbqRBquD.mjs";
|
|
8
|
-
import "./daemon-
|
|
8
|
+
import "./daemon-B5SNuIhD.mjs";
|
|
9
9
|
import "./api-DG5W6iwx.mjs";
|
|
10
10
|
import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
|
|
11
|
-
import { t as PortalDaemonClient } from "./daemon-client-
|
|
11
|
+
import { t as PortalDaemonClient } from "./daemon-client-CneyOup7.mjs";
|
|
12
12
|
import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
|
|
13
13
|
import * as os$1 from "node:os";
|
|
14
14
|
import { homedir, platform, release, tmpdir } from "node:os";
|
|
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
|
|
|
20
20
|
* the commit time of the built tree is one monotonic clock they share.
|
|
21
21
|
*/
|
|
22
22
|
function portalDaemonBuild() {
|
|
23
|
-
return "
|
|
23
|
+
return "1786741092";
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Whether a client carrying `mine` should replace a running daemon carrying
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./client-DbqRBquD.mjs";
|
|
3
|
-
import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-
|
|
3
|
+
import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-B5SNuIhD.mjs";
|
|
4
4
|
import "./api-DG5W6iwx.mjs";
|
|
5
5
|
|
|
6
6
|
export { runPortalDaemon };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-
|
|
2
|
+
import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-B5SNuIhD.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { WebSocket, createWebSocketStream } from "ws";
|
|
5
|
+
|
|
6
|
+
//#region src/chat/portal/tunnel-pool.ts
|
|
7
|
+
const MAX_AGE_MS = 4e4;
|
|
8
|
+
const SWEEP_INTERVAL_MS = 1e4;
|
|
9
|
+
const REFILL_BACKOFF_MS = 15e3;
|
|
10
|
+
/**
|
|
11
|
+
* Pre-warmed tunnel pool for the forward portal's accept path.
|
|
12
|
+
*
|
|
13
|
+
* Dialing a tunnel costs a full edge round-trip (~300ms measured). The pool
|
|
14
|
+
* pays that in the background so accept can adopt a warm tunnel and move the
|
|
15
|
+
* first byte immediately. Target starts at `initialSize` and grows by one on
|
|
16
|
+
* each miss, never above `maxSize` — a miss still cold-dials (today's path);
|
|
17
|
+
* growth only helps the next wave. No decay: a runaway client can hold
|
|
18
|
+
* `maxSize` idle edge sockets until `forward` exits.
|
|
19
|
+
*
|
|
20
|
+
* `dial` must resolve with a stream that is already paused, so any bytes a
|
|
21
|
+
* server-first protocol sends before adoption buffer instead of dropping.
|
|
22
|
+
*/
|
|
23
|
+
function createTunnelPool({ initialSize, maxSize, dial, log }) {
|
|
24
|
+
let entries = [];
|
|
25
|
+
let dialsInFlight = 0;
|
|
26
|
+
let backoffUntil = 0;
|
|
27
|
+
let closed = false;
|
|
28
|
+
let targetSize = initialSize;
|
|
29
|
+
function refill() {
|
|
30
|
+
if (closed) return;
|
|
31
|
+
if (Date.now() < backoffUntil) return;
|
|
32
|
+
while (entries.length + dialsInFlight < targetSize) {
|
|
33
|
+
dialsInFlight += 1;
|
|
34
|
+
dial().then((tunnel) => {
|
|
35
|
+
dialsInFlight -= 1;
|
|
36
|
+
if (closed) {
|
|
37
|
+
tunnel.close();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const entry = {
|
|
41
|
+
tunnel,
|
|
42
|
+
dialedAt: Date.now(),
|
|
43
|
+
dead: false
|
|
44
|
+
};
|
|
45
|
+
tunnel.stream.once("close", () => {
|
|
46
|
+
entry.dead = true;
|
|
47
|
+
});
|
|
48
|
+
tunnel.stream.once("error", () => {
|
|
49
|
+
entry.dead = true;
|
|
50
|
+
});
|
|
51
|
+
entries.push(entry);
|
|
52
|
+
}).catch((err) => {
|
|
53
|
+
dialsInFlight -= 1;
|
|
54
|
+
backoffUntil = Date.now() + REFILL_BACKOFF_MS;
|
|
55
|
+
log(`forward: pool dial failed (backing off): ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sweep() {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const keep = [];
|
|
62
|
+
for (const entry of entries) if (entry.dead || now - entry.dialedAt > MAX_AGE_MS) entry.tunnel.close();
|
|
63
|
+
else keep.push(entry);
|
|
64
|
+
entries = keep;
|
|
65
|
+
refill();
|
|
66
|
+
}
|
|
67
|
+
const sweeper = setInterval(sweep, SWEEP_INTERVAL_MS);
|
|
68
|
+
sweeper.unref();
|
|
69
|
+
refill();
|
|
70
|
+
return {
|
|
71
|
+
take: () => {
|
|
72
|
+
while (entries.length > 0) {
|
|
73
|
+
const entry = entries.pop();
|
|
74
|
+
if (!entry) break;
|
|
75
|
+
if (entry.dead || entry.tunnel.stream.destroyed) continue;
|
|
76
|
+
refill();
|
|
77
|
+
return entry.tunnel;
|
|
78
|
+
}
|
|
79
|
+
if (targetSize < maxSize) targetSize += 1;
|
|
80
|
+
refill();
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
close: () => {
|
|
84
|
+
closed = true;
|
|
85
|
+
clearInterval(sweeper);
|
|
86
|
+
for (const entry of entries) entry.tunnel.close();
|
|
87
|
+
entries = [];
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/chat/portal/forward.ts
|
|
94
|
+
const TARGET_REFRESH_SAFETY_MS = 12e4;
|
|
95
|
+
const INITIAL_POOL_SIZE = 8;
|
|
96
|
+
const MAX_POOL_SIZE = 16;
|
|
97
|
+
/**
|
|
98
|
+
* The reverse portal's client half: listen on the local machine's loopback and
|
|
99
|
+
* pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
|
|
100
|
+
* which pipes to the sandbox's own loopback. The daemon is reached through the
|
|
101
|
+
* agent-webserver edge Worker (sandboxes have public ingress disabled; the
|
|
102
|
+
* Worker owns boot-resolution and injects the sandbox edge-auth token), so a
|
|
103
|
+
* sandbox recycle just costs the next connection a cold-start wait rather
|
|
104
|
+
* than invalidating the forward.
|
|
105
|
+
*/
|
|
106
|
+
async function startForward({ auth, agentId, localPort, targetPort, log }) {
|
|
107
|
+
let target = await fetchForwardTarget(auth, agentId);
|
|
108
|
+
let mintedAt = Date.now();
|
|
109
|
+
async function freshTarget() {
|
|
110
|
+
const ttlMs = target.expiresInSeconds * 1e3;
|
|
111
|
+
if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
|
|
112
|
+
target = await fetchForwardTarget(auth, agentId);
|
|
113
|
+
mintedAt = Date.now();
|
|
114
|
+
}
|
|
115
|
+
return target;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Dial one tunnel through the edge to the daemon's /portal/tcp. Resolves
|
|
119
|
+
* once the upgrade completes, with the duplex PAUSED — the pool holds it
|
|
120
|
+
* warm without dropping any early bytes, and adoption resumes it by piping.
|
|
121
|
+
*/
|
|
122
|
+
async function dialTunnel() {
|
|
123
|
+
const resolved = await freshTarget();
|
|
124
|
+
const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
ws.once("error", reject);
|
|
127
|
+
ws.once("open", () => {
|
|
128
|
+
ws.removeListener("error", reject);
|
|
129
|
+
const stream = createWebSocketStream(ws);
|
|
130
|
+
stream.pause();
|
|
131
|
+
resolve({
|
|
132
|
+
stream,
|
|
133
|
+
close: () => stream.destroy()
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const pool = createTunnelPool({
|
|
139
|
+
initialSize: INITIAL_POOL_SIZE,
|
|
140
|
+
maxSize: MAX_POOL_SIZE,
|
|
141
|
+
dial: dialTunnel,
|
|
142
|
+
log
|
|
143
|
+
});
|
|
144
|
+
const server = net.createServer((sock) => {
|
|
145
|
+
sock.pause();
|
|
146
|
+
const warm = pool.take();
|
|
147
|
+
if (warm) {
|
|
148
|
+
warm.stream.on("error", () => sock.destroy());
|
|
149
|
+
sock.on("error", () => warm.stream.destroy());
|
|
150
|
+
sock.pipe(warm.stream).pipe(sock);
|
|
151
|
+
sock.resume();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
(async () => {
|
|
155
|
+
let tunnel;
|
|
156
|
+
try {
|
|
157
|
+
tunnel = await dialTunnel();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
log(`forward: tunnel connect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
+
sock.destroy();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
tunnel.stream.on("error", () => sock.destroy());
|
|
164
|
+
sock.on("error", () => tunnel.stream.destroy());
|
|
165
|
+
sock.pipe(tunnel.stream).pipe(sock);
|
|
166
|
+
sock.resume();
|
|
167
|
+
})();
|
|
168
|
+
});
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
server.once("error", reject);
|
|
171
|
+
server.listen(localPort, "127.0.0.1", () => {
|
|
172
|
+
server.removeListener("error", reject);
|
|
173
|
+
resolve();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
const addr = server.address();
|
|
177
|
+
return {
|
|
178
|
+
port: addr && typeof addr === "object" ? addr.port : localPort,
|
|
179
|
+
close: () => new Promise((resolve) => {
|
|
180
|
+
pool.close();
|
|
181
|
+
server.close(() => resolve());
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
export { startForward };
|
package/package.json
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
|
|
3
|
-
import net from "node:net";
|
|
4
|
-
import { WebSocket, createWebSocketStream } from "ws";
|
|
5
|
-
|
|
6
|
-
//#region src/chat/portal/forward.ts
|
|
7
|
-
const TARGET_REFRESH_SAFETY_MS = 12e4;
|
|
8
|
-
/**
|
|
9
|
-
* The reverse portal's client half: listen on the local machine's loopback and
|
|
10
|
-
* pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
|
|
11
|
-
* which pipes to the sandbox's own loopback. The daemon is reached through the
|
|
12
|
-
* agent-webserver edge Worker (sandboxes have public ingress disabled; the
|
|
13
|
-
* Worker owns boot-resolution and injects the sandbox edge-auth token), so a
|
|
14
|
-
* sandbox recycle just costs the next connection a cold-start wait rather
|
|
15
|
-
* than invalidating the forward.
|
|
16
|
-
*/
|
|
17
|
-
async function startForward({ auth, agentId, localPort, targetPort, log }) {
|
|
18
|
-
let target = await fetchForwardTarget(auth, agentId);
|
|
19
|
-
let mintedAt = Date.now();
|
|
20
|
-
async function freshTarget() {
|
|
21
|
-
const ttlMs = target.expiresInSeconds * 1e3;
|
|
22
|
-
if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
|
|
23
|
-
target = await fetchForwardTarget(auth, agentId);
|
|
24
|
-
mintedAt = Date.now();
|
|
25
|
-
}
|
|
26
|
-
return target;
|
|
27
|
-
}
|
|
28
|
-
const server = net.createServer((sock) => {
|
|
29
|
-
sock.pause();
|
|
30
|
-
(async () => {
|
|
31
|
-
let resolved;
|
|
32
|
-
try {
|
|
33
|
-
resolved = await freshTarget();
|
|
34
|
-
} catch (err) {
|
|
35
|
-
log(`forward: token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
-
sock.destroy();
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
|
|
40
|
-
ws.on("open", () => {
|
|
41
|
-
const stream = createWebSocketStream(ws);
|
|
42
|
-
stream.on("error", () => sock.destroy());
|
|
43
|
-
sock.on("error", () => stream.destroy());
|
|
44
|
-
sock.pipe(stream).pipe(sock);
|
|
45
|
-
sock.resume();
|
|
46
|
-
});
|
|
47
|
-
ws.on("error", (err) => {
|
|
48
|
-
log(`forward: tunnel connect failed: ${err.message}`);
|
|
49
|
-
sock.destroy();
|
|
50
|
-
});
|
|
51
|
-
})();
|
|
52
|
-
});
|
|
53
|
-
await new Promise((resolve, reject) => {
|
|
54
|
-
server.once("error", reject);
|
|
55
|
-
server.listen(localPort, "127.0.0.1", () => {
|
|
56
|
-
server.removeListener("error", reject);
|
|
57
|
-
resolve();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
const addr = server.address();
|
|
61
|
-
return {
|
|
62
|
-
port: addr && typeof addr === "object" ? addr.port : localPort,
|
|
63
|
-
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
//#endregion
|
|
68
|
-
export { startForward };
|