zcode-acp-server 0.1.0 → 0.3.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/README.md +192 -9
- package/README.zh-CN.md +143 -8
- package/dist/backend/client.d.ts +12 -4
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +72 -7
- package/dist/backend/client.js.map +1 -1
- package/dist/backend/credentials.d.ts.map +1 -1
- package/dist/backend/credentials.js +2 -1
- package/dist/backend/credentials.js.map +1 -1
- package/dist/backend/listener.d.ts +12 -4
- package/dist/backend/listener.d.ts.map +1 -1
- package/dist/backend/listener.js +68 -12
- package/dist/backend/listener.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/backend/types.d.ts.map +1 -1
- package/dist/bin/hub.d.ts +16 -0
- package/dist/bin/hub.d.ts.map +1 -0
- package/dist/bin/hub.js +41 -0
- package/dist/bin/hub.js.map +1 -0
- package/dist/bin/quota.d.ts +54 -0
- package/dist/bin/quota.d.ts.map +1 -0
- package/dist/bin/quota.js +333 -0
- package/dist/bin/quota.js.map +1 -0
- package/dist/config/auto-compact.d.ts +23 -0
- package/dist/config/auto-compact.d.ts.map +1 -0
- package/dist/config/auto-compact.js +67 -0
- package/dist/config/auto-compact.js.map +1 -0
- package/dist/config/mcp-discovery.d.ts +34 -0
- package/dist/config/mcp-discovery.d.ts.map +1 -0
- package/dist/config/mcp-discovery.js +153 -0
- package/dist/config/mcp-discovery.js.map +1 -0
- package/dist/config/model-cache.d.ts +12 -1
- package/dist/config/model-cache.d.ts.map +1 -1
- package/dist/config/model-cache.js +30 -8
- package/dist/config/model-cache.js.map +1 -1
- package/dist/config/options.d.ts +87 -9
- package/dist/config/options.d.ts.map +1 -1
- package/dist/config/options.js +248 -45
- package/dist/config/options.js.map +1 -1
- package/dist/config/plugin-commands.d.ts +24 -0
- package/dist/config/plugin-commands.d.ts.map +1 -0
- package/dist/config/plugin-commands.js +107 -0
- package/dist/config/plugin-commands.js.map +1 -0
- package/dist/config/provider-registry.d.ts +60 -0
- package/dist/config/provider-registry.d.ts.map +1 -0
- package/dist/config/provider-registry.js +128 -0
- package/dist/config/provider-registry.js.map +1 -0
- package/dist/config/runtime-model.d.ts +54 -18
- package/dist/config/runtime-model.d.ts.map +1 -1
- package/dist/config/runtime-model.js +105 -61
- package/dist/config/runtime-model.js.map +1 -1
- package/dist/config/skill-discovery.d.ts +35 -0
- package/dist/config/skill-discovery.d.ts.map +1 -0
- package/dist/config/skill-discovery.js +188 -0
- package/dist/config/skill-discovery.js.map +1 -0
- package/dist/handlers/account.d.ts +43 -0
- package/dist/handlers/account.d.ts.map +1 -0
- package/dist/handlers/account.js +59 -0
- package/dist/handlers/account.js.map +1 -0
- package/dist/handlers/background-tasks.d.ts +72 -0
- package/dist/handlers/background-tasks.d.ts.map +1 -0
- package/dist/handlers/background-tasks.js +330 -0
- package/dist/handlers/background-tasks.js.map +1 -0
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +134 -22
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/extensions.d.ts +15 -0
- package/dist/handlers/extensions.d.ts.map +1 -1
- package/dist/handlers/extensions.js +43 -27
- package/dist/handlers/extensions.js.map +1 -1
- package/dist/handlers/io.d.ts +31 -3
- package/dist/handlers/io.d.ts.map +1 -1
- package/dist/handlers/io.js +108 -9
- package/dist/handlers/io.js.map +1 -1
- package/dist/handlers/replay.d.ts +79 -0
- package/dist/handlers/replay.d.ts.map +1 -0
- package/dist/handlers/replay.js +252 -0
- package/dist/handlers/replay.js.map +1 -0
- package/dist/handlers/server-requests.d.ts +20 -4
- package/dist/handlers/server-requests.d.ts.map +1 -1
- package/dist/handlers/server-requests.js +293 -63
- package/dist/handlers/server-requests.js.map +1 -1
- package/dist/handlers/session.d.ts +113 -10
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +932 -277
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +37 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +143 -8
- package/dist/handlers/slash.js.map +1 -1
- package/dist/index.js +76 -12
- package/dist/index.js.map +1 -1
- package/dist/interaction/adapter.d.ts +19 -33
- package/dist/interaction/adapter.d.ts.map +1 -1
- package/dist/interaction/adapter.js +80 -79
- package/dist/interaction/adapter.js.map +1 -1
- package/dist/lazy-sessions.d.ts +35 -0
- package/dist/lazy-sessions.d.ts.map +1 -0
- package/dist/lazy-sessions.js +98 -0
- package/dist/lazy-sessions.js.map +1 -0
- package/dist/quota/cache.d.ts +18 -0
- package/dist/quota/cache.d.ts.map +1 -0
- package/dist/quota/cache.js +32 -0
- package/dist/quota/cache.js.map +1 -0
- package/dist/quota/client.d.ts +30 -0
- package/dist/quota/client.d.ts.map +1 -0
- package/dist/quota/client.js +57 -0
- package/dist/quota/client.js.map +1 -0
- package/dist/quota/color.d.ts +58 -0
- package/dist/quota/color.d.ts.map +1 -0
- package/dist/quota/color.js +95 -0
- package/dist/quota/color.js.map +1 -0
- package/dist/quota/combined.d.ts +66 -0
- package/dist/quota/combined.d.ts.map +1 -0
- package/dist/quota/combined.js +179 -0
- package/dist/quota/combined.js.map +1 -0
- package/dist/quota/format.d.ts +105 -0
- package/dist/quota/format.d.ts.map +1 -0
- package/dist/quota/format.js +219 -0
- package/dist/quota/format.js.map +1 -0
- package/dist/quota/index.d.ts +23 -0
- package/dist/quota/index.d.ts.map +1 -0
- package/dist/quota/index.js +42 -0
- package/dist/quota/index.js.map +1 -0
- package/dist/quota/opencode-go/cache.d.ts +17 -0
- package/dist/quota/opencode-go/cache.d.ts.map +1 -0
- package/dist/quota/opencode-go/cache.js +31 -0
- package/dist/quota/opencode-go/cache.js.map +1 -0
- package/dist/quota/opencode-go/client.d.ts +28 -0
- package/dist/quota/opencode-go/client.d.ts.map +1 -0
- package/dist/quota/opencode-go/client.js +48 -0
- package/dist/quota/opencode-go/client.js.map +1 -0
- package/dist/quota/opencode-go/config.d.ts +37 -0
- package/dist/quota/opencode-go/config.d.ts.map +1 -0
- package/dist/quota/opencode-go/config.js +58 -0
- package/dist/quota/opencode-go/config.js.map +1 -0
- package/dist/quota/opencode-go/format.d.ts +36 -0
- package/dist/quota/opencode-go/format.d.ts.map +1 -0
- package/dist/quota/opencode-go/format.js +87 -0
- package/dist/quota/opencode-go/format.js.map +1 -0
- package/dist/quota/opencode-go/index.d.ts +30 -0
- package/dist/quota/opencode-go/index.d.ts.map +1 -0
- package/dist/quota/opencode-go/index.js +108 -0
- package/dist/quota/opencode-go/index.js.map +1 -0
- package/dist/quota/opencode-go/parse.d.ts +41 -0
- package/dist/quota/opencode-go/parse.d.ts.map +1 -0
- package/dist/quota/opencode-go/parse.js +75 -0
- package/dist/quota/opencode-go/parse.js.map +1 -0
- package/dist/quota/opencode-go/types.d.ts +48 -0
- package/dist/quota/opencode-go/types.d.ts.map +1 -0
- package/dist/quota/opencode-go/types.js +11 -0
- package/dist/quota/opencode-go/types.js.map +1 -0
- package/dist/quota/parse.d.ts +33 -0
- package/dist/quota/parse.d.ts.map +1 -0
- package/dist/quota/parse.js +200 -0
- package/dist/quota/parse.js.map +1 -0
- package/dist/quota/types.d.ts +72 -0
- package/dist/quota/types.d.ts.map +1 -0
- package/dist/quota/types.js +10 -0
- package/dist/quota/types.js.map +1 -0
- package/dist/remote/broadcast.d.ts +47 -0
- package/dist/remote/broadcast.d.ts.map +1 -0
- package/dist/remote/broadcast.js +121 -0
- package/dist/remote/broadcast.js.map +1 -0
- package/dist/remote/config.d.ts +32 -0
- package/dist/remote/config.d.ts.map +1 -0
- package/dist/remote/config.js +65 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/endpoint.d.ts +30 -0
- package/dist/remote/endpoint.d.ts.map +1 -0
- package/dist/remote/endpoint.js +213 -0
- package/dist/remote/endpoint.js.map +1 -0
- package/dist/remote/hub-server.d.ts +41 -0
- package/dist/remote/hub-server.d.ts.map +1 -0
- package/dist/remote/hub-server.js +346 -0
- package/dist/remote/hub-server.js.map +1 -0
- package/dist/server.d.ts +139 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +179 -4
- package/dist/server.js.map +1 -1
- package/dist/tasks-index.d.ts +14 -4
- package/dist/tasks-index.d.ts.map +1 -1
- package/dist/tasks-index.js +145 -46
- package/dist/tasks-index.js.map +1 -1
- package/dist/translators/event-translator.d.ts +33 -0
- package/dist/translators/event-translator.d.ts.map +1 -1
- package/dist/translators/event-translator.js +101 -0
- package/dist/translators/event-translator.js.map +1 -1
- package/dist/translators/index.d.ts +1 -1
- package/dist/translators/index.d.ts.map +1 -1
- package/dist/translators/index.js +1 -1
- package/dist/translators/index.js.map +1 -1
- package/dist/translators/projection-differ.d.ts +8 -0
- package/dist/translators/projection-differ.d.ts.map +1 -1
- package/dist/translators/projection-differ.js +19 -9
- package/dist/translators/projection-differ.js.map +1 -1
- package/dist/translators/tool-helpers.d.ts +28 -0
- package/dist/translators/tool-helpers.d.ts.map +1 -1
- package/dist/translators/tool-helpers.js +104 -0
- package/dist/translators/tool-helpers.js.map +1 -1
- package/dist/translators/types.d.ts +38 -1
- package/dist/translators/types.d.ts.map +1 -1
- package/dist/translators/types.js.map +1 -1
- package/dist/utils.d.ts +36 -7
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +69 -5
- package/dist/utils.js.map +1 -1
- package/docs/ARCHITECTURE.md +130 -40
- package/docs/BACKLOG.md +59 -0
- package/docs/DEVELOPMENT.md +31 -0
- package/docs/PROTOCOL.md +328 -23
- package/docs/REMOTE-CLIENTS.md +260 -0
- package/docs/REPLAY-GUIDE.md +131 -0
- package/docs/TROUBLESHOOTING.md +131 -14
- package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
- package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
- package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +22 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/proposals/0001-tail-session-replay.md +136 -0
- package/docs/proposals/0002-plan-quota-usage.md +81 -0
- package/package.json +10 -6
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback ACP endpoint + hub registration for remote access.
|
|
3
|
+
*
|
|
4
|
+
* When ZCODE_ACP_REMOTE is enabled, the bridge serves the SAME AgentApp that
|
|
5
|
+
* handles the stdio editor connection on a loopback HTTP/WebSocket endpoint
|
|
6
|
+
* (SDK AcpServer transport). Each remote connection gets its own JSON-RPC id
|
|
7
|
+
* space; fan-out to all clients is handled by the broadcast registry, not
|
|
8
|
+
* here. This endpoint is intentionally NOT exposed to the network — the hub
|
|
9
|
+
* (`zcode-acp-hub`) is the single public entry and proxies into it.
|
|
10
|
+
*
|
|
11
|
+
* The bridge also registers itself with the hub (spawning one if none is
|
|
12
|
+
* listening) and re-registers every 10s as a heartbeat carrying fresh session
|
|
13
|
+
* summaries. Everything here is best-effort: any failure warns and disables
|
|
14
|
+
* the remote side without touching the stdio link.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { createServer } from "node:http";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { AcpServer } from "@agentclientprotocol/sdk/experimental/server";
|
|
20
|
+
import { createNodeHttpHandler, createNodeWebSocketUpgradeHandler, } from "@agentclientprotocol/sdk/experimental/node";
|
|
21
|
+
import { WebSocketServer } from "ws";
|
|
22
|
+
import { AGENT_INFO, log, warn } from "../utils.js";
|
|
23
|
+
/** How often the bridge re-registers with the hub (also the heartbeat). */
|
|
24
|
+
const HEARTBEAT_MS = 10_000;
|
|
25
|
+
/** Minimum spacing between hub spawn attempts (avoids spawn storms). */
|
|
26
|
+
const SPAWN_THROTTLE_MS = 60_000;
|
|
27
|
+
/** Max ports probed above ZCODE_ACP_REMOTE_PORT before giving up. */
|
|
28
|
+
const MAX_PORT_PROBES = 100;
|
|
29
|
+
/** Probe one loopback port; false = taken or otherwise unusable. */
|
|
30
|
+
function tryListen(server, port) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
server.once("error", () => resolve(false));
|
|
33
|
+
server.listen(port, "127.0.0.1", () => resolve(true));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Session summaries for the hub's discovery API. */
|
|
37
|
+
function sessionsPayload(server) {
|
|
38
|
+
return Array.from(server.sessionSummaries.entries(), ([sessionId, s]) => ({
|
|
39
|
+
sessionId,
|
|
40
|
+
...(s.title !== undefined ? { title: s.title } : {}),
|
|
41
|
+
updatedAt: s.updatedAt,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async function postJson(url, body, timeoutMs = 3000) {
|
|
45
|
+
return fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the loopback endpoint and hub registration. Never throws — failures
|
|
54
|
+
* warn and leave the bridge running stdio-only.
|
|
55
|
+
*/
|
|
56
|
+
export async function startRemoteEndpoint(server, app, config) {
|
|
57
|
+
const acpServer = new AcpServer({ agent: app });
|
|
58
|
+
const acpHttpHandler = createNodeHttpHandler(acpServer);
|
|
59
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
60
|
+
const upgradeHandler = createNodeWebSocketUpgradeHandler(acpServer, wss);
|
|
61
|
+
const httpServer = createServer((req, res) => {
|
|
62
|
+
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
63
|
+
if (path === "/acp")
|
|
64
|
+
acpHttpHandler(req, res);
|
|
65
|
+
else {
|
|
66
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
67
|
+
res.end("not found");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
71
|
+
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
72
|
+
if (path === "/acp")
|
|
73
|
+
upgradeHandler(req, socket, head);
|
|
74
|
+
else
|
|
75
|
+
socket.destroy();
|
|
76
|
+
});
|
|
77
|
+
// Port scan: several Zed windows spawn several bridges, each takes the next
|
|
78
|
+
// free port starting at ZCODE_ACP_REMOTE_PORT.
|
|
79
|
+
let port = 0;
|
|
80
|
+
for (let probe = 0; probe < MAX_PORT_PROBES; probe++) {
|
|
81
|
+
if (await tryListen(httpServer, config.bridgePort + probe)) {
|
|
82
|
+
port = config.bridgePort + probe;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!port) {
|
|
87
|
+
warn(`remote: no free loopback port in ${config.bridgePort}..${config.bridgePort + MAX_PORT_PROBES - 1} — remote disabled`);
|
|
88
|
+
wss.close();
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// The listener must not keep the process alive on its own (ADR-0001: the
|
|
92
|
+
// bridge's lifetime follows the stdio editor, not remote clients).
|
|
93
|
+
httpServer.unref();
|
|
94
|
+
httpServer.on("error", (e) => warn(`remote: endpoint error: ${e.message}`));
|
|
95
|
+
log(`remote: ACP endpoint listening on 127.0.0.1:${port}/acp`);
|
|
96
|
+
// ---- hub registration (heartbeat loop) ----
|
|
97
|
+
const instanceId = String(process.pid);
|
|
98
|
+
let stopped = false;
|
|
99
|
+
let authRejected = false;
|
|
100
|
+
let spawnThrottledUntil = 0;
|
|
101
|
+
const payload = () => ({
|
|
102
|
+
token: config.token,
|
|
103
|
+
id: instanceId,
|
|
104
|
+
port,
|
|
105
|
+
pid: process.pid,
|
|
106
|
+
workspace: server.workspaceLabel(),
|
|
107
|
+
sessions: sessionsPayload(server),
|
|
108
|
+
// Lets the hub detect that it is older than this bridge and restart
|
|
109
|
+
// itself (we then re-spawn it from this dist — see registerOnce).
|
|
110
|
+
version: AGENT_INFO.version,
|
|
111
|
+
});
|
|
112
|
+
const spawnHub = () => {
|
|
113
|
+
try {
|
|
114
|
+
// dist/remote/endpoint.js → dist/bin/hub.js (one level up, then bin/).
|
|
115
|
+
const hubJs = fileURLToPath(new URL("../bin/hub.js", import.meta.url));
|
|
116
|
+
const child = spawn(process.execPath, [hubJs], {
|
|
117
|
+
detached: true,
|
|
118
|
+
// Surface the daemon's stderr through the bridge's diagnostics — a
|
|
119
|
+
// detached "ignore" pipe silently eats startup failures.
|
|
120
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
121
|
+
env: {
|
|
122
|
+
...process.env,
|
|
123
|
+
ZCODE_ACP_HUB_PORT: String(config.hubPort),
|
|
124
|
+
ZCODE_ACP_HUB_HOST: config.hubHost,
|
|
125
|
+
ZCODE_ACP_REMOTE_TOKEN: config.token,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
child.stderr?.on("data", (d) => {
|
|
129
|
+
for (const line of d.toString().split("\n")) {
|
|
130
|
+
if (line.trim())
|
|
131
|
+
warn(`remote: hub: ${line}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Spawn failures (ENOENT when run from src without a build) arrive as an
|
|
135
|
+
// async 'error' event — without a listener Node crashes the bridge.
|
|
136
|
+
child.once("error", (e) => {
|
|
137
|
+
warn(`remote: hub spawn failed: ${e.message}`);
|
|
138
|
+
});
|
|
139
|
+
child.unref();
|
|
140
|
+
log(`remote: spawned hub on port ${config.hubPort} (pid ${child.pid})`);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
warn(`remote: hub spawn failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const registerOnce = async () => {
|
|
147
|
+
if (stopped || authRejected)
|
|
148
|
+
return;
|
|
149
|
+
try {
|
|
150
|
+
const res = await postJson(`http://127.0.0.1:${config.hubPort}/api/register`, payload());
|
|
151
|
+
if (res.status === 401) {
|
|
152
|
+
authRejected = true;
|
|
153
|
+
warn("remote: hub rejected the token (401) — registration stopped, check ZCODE_ACP_REMOTE_TOKEN");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Version handshake: the hub saw a newer bridge and is exiting. It is
|
|
157
|
+
// gone by now (it exits ~0.5s after replying) — re-spawn it from THIS
|
|
158
|
+
// dist (the upgraded code) and re-register. Throttled like the spawn
|
|
159
|
+
// below so a hub that keeps answering `restarting` can't loop us.
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
const body = (await res.json().catch(() => null));
|
|
162
|
+
if (body?.restarting) {
|
|
163
|
+
log("remote: hub is older than this bridge — respawning upgraded hub");
|
|
164
|
+
const respawn = setTimeout(() => {
|
|
165
|
+
if (stopped || authRejected)
|
|
166
|
+
return;
|
|
167
|
+
if (Date.now() < spawnThrottledUntil)
|
|
168
|
+
return;
|
|
169
|
+
spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
|
|
170
|
+
spawnHub();
|
|
171
|
+
const retry = setTimeout(() => void registerOnce(), 1500);
|
|
172
|
+
retry.unref();
|
|
173
|
+
}, 2000);
|
|
174
|
+
respawn.unref();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Hub unreachable: (re)spawn it, throttled so a failing spawn can't
|
|
180
|
+
// storm, then retry registration shortly after the daemon warms up
|
|
181
|
+
// instead of waiting a full heartbeat cycle.
|
|
182
|
+
if (Date.now() >= spawnThrottledUntil) {
|
|
183
|
+
spawnThrottledUntil = Date.now() + SPAWN_THROTTLE_MS;
|
|
184
|
+
spawnHub();
|
|
185
|
+
const retry = setTimeout(() => void registerOnce(), 1500);
|
|
186
|
+
retry.unref();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
void registerOnce();
|
|
191
|
+
const heartbeat = setInterval(() => void registerOnce(), HEARTBEAT_MS);
|
|
192
|
+
heartbeat.unref();
|
|
193
|
+
return {
|
|
194
|
+
port,
|
|
195
|
+
async stop() {
|
|
196
|
+
stopped = true;
|
|
197
|
+
clearInterval(heartbeat);
|
|
198
|
+
try {
|
|
199
|
+
await postJson(`http://127.0.0.1:${config.hubPort}/api/unregister`, { token: config.token, id: instanceId }, 1500);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Hub gone or unreachable — its heartbeat TTL will drop us anyway.
|
|
203
|
+
}
|
|
204
|
+
for (const client of wss.clients)
|
|
205
|
+
client.terminate();
|
|
206
|
+
wss.close();
|
|
207
|
+
await acpServer.close().catch(() => undefined);
|
|
208
|
+
httpServer.closeAllConnections?.();
|
|
209
|
+
httpServer.close();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=endpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/remote/endpoint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zcode-acp-hub — machine-level singleton for remote access.
|
|
3
|
+
*
|
|
4
|
+
* The hub is the ONLY public entry point (the port a tunnel maps). It does
|
|
5
|
+
* exactly three things (ADR-0002): token auth, instance discovery, and
|
|
6
|
+
* byte-level WebSocket proxying from a remote client to one bridge's loopback
|
|
7
|
+
* ACP endpoint. It holds no session state and understands no ACP — a proxied
|
|
8
|
+
* connection stays bound to one instance for its whole lifetime.
|
|
9
|
+
*
|
|
10
|
+
* Bridges register via POST /api/register every 10s (the registration doubles
|
|
11
|
+
* as the heartbeat; entries older than the heartbeat TTL are pruned). A client
|
|
12
|
+
* that needs an immediately-honest list (e.g. a phone app's pull-to-refresh)
|
|
13
|
+
* passes ?probe=1 to /api/instances: the hub TCP-probes each registered
|
|
14
|
+
* loopback port and prunes unreachable bridges before answering — no periodic
|
|
15
|
+
* probing, the cost is paid only when someone refreshes. When no instance is
|
|
16
|
+
* registered and no proxy is active for `idleExitMs`, the hub exits — the
|
|
17
|
+
* next bridge re-spawns it on demand.
|
|
18
|
+
*/
|
|
19
|
+
export interface HubOptions {
|
|
20
|
+
port: number;
|
|
21
|
+
host: string;
|
|
22
|
+
token: string;
|
|
23
|
+
/** Registration TTL before an instance is pruned (default 30s). */
|
|
24
|
+
heartbeatTimeoutMs?: number;
|
|
25
|
+
/** Idle time with zero instances and zero proxies before exit (default 10min). */
|
|
26
|
+
idleExitMs?: number;
|
|
27
|
+
/** WebSocket keepalive ping interval (default 30s; tunnels drop idle links). */
|
|
28
|
+
pingIntervalMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface HubHandle {
|
|
31
|
+
port: number;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start the hub. Resolves once listening; rejects on bind failure (including
|
|
36
|
+
* EADDRINUSE when another hub already owns the port).
|
|
37
|
+
*/
|
|
38
|
+
export declare function startHub(options: HubOptions & {
|
|
39
|
+
onIdleExit?: () => void;
|
|
40
|
+
}): Promise<HubHandle>;
|
|
41
|
+
//# sourceMappingURL=hub-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAqGD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAwQ9F"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zcode-acp-hub — machine-level singleton for remote access.
|
|
3
|
+
*
|
|
4
|
+
* The hub is the ONLY public entry point (the port a tunnel maps). It does
|
|
5
|
+
* exactly three things (ADR-0002): token auth, instance discovery, and
|
|
6
|
+
* byte-level WebSocket proxying from a remote client to one bridge's loopback
|
|
7
|
+
* ACP endpoint. It holds no session state and understands no ACP — a proxied
|
|
8
|
+
* connection stays bound to one instance for its whole lifetime.
|
|
9
|
+
*
|
|
10
|
+
* Bridges register via POST /api/register every 10s (the registration doubles
|
|
11
|
+
* as the heartbeat; entries older than the heartbeat TTL are pruned). A client
|
|
12
|
+
* that needs an immediately-honest list (e.g. a phone app's pull-to-refresh)
|
|
13
|
+
* passes ?probe=1 to /api/instances: the hub TCP-probes each registered
|
|
14
|
+
* loopback port and prunes unreachable bridges before answering — no periodic
|
|
15
|
+
* probing, the cost is paid only when someone refreshes. When no instance is
|
|
16
|
+
* registered and no proxy is active for `idleExitMs`, the hub exits — the
|
|
17
|
+
* next bridge re-spawns it on demand.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import net from "node:net";
|
|
22
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
23
|
+
import { AGENT_INFO, compareVersions, log, warn } from "../utils.js";
|
|
24
|
+
const HEARTBEAT_TIMEOUT_MS = 30_000;
|
|
25
|
+
const IDLE_EXIT_MS = 10 * 60_000;
|
|
26
|
+
const PING_INTERVAL_MS = 30_000;
|
|
27
|
+
/** Per-instance TCP probe timeout for /api/instances?probe=1. */
|
|
28
|
+
const PROBE_TIMEOUT_MS = 500;
|
|
29
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
30
|
+
/** Constant-time token compare (hash both to equal length first). */
|
|
31
|
+
function tokenEquals(a, b) {
|
|
32
|
+
const ha = createHash("sha256").update(a).digest();
|
|
33
|
+
const hb = createHash("sha256").update(b).digest();
|
|
34
|
+
return timingSafeEqual(ha, hb);
|
|
35
|
+
}
|
|
36
|
+
function authorized(req, url, token) {
|
|
37
|
+
const header = req.headers.authorization;
|
|
38
|
+
if (header?.startsWith("Bearer "))
|
|
39
|
+
return tokenEquals(header.slice(7), token);
|
|
40
|
+
const query = url.searchParams.get("token");
|
|
41
|
+
return query !== null && tokenEquals(query, token);
|
|
42
|
+
}
|
|
43
|
+
function setCors(res) {
|
|
44
|
+
// The web UI is deployed as a separate origin; the token is the boundary.
|
|
45
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
46
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
47
|
+
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
48
|
+
}
|
|
49
|
+
async function readJson(req) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let size = 0;
|
|
52
|
+
try {
|
|
53
|
+
// The async iterator rejects when the client aborts mid-body — a truncated
|
|
54
|
+
// POST must degrade to "invalid body", not reject into the event loop.
|
|
55
|
+
for await (const chunk of req) {
|
|
56
|
+
size += chunk.length;
|
|
57
|
+
if (size > MAX_BODY_BYTES)
|
|
58
|
+
return null;
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
}
|
|
61
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
62
|
+
return typeof parsed === "object" && parsed !== null
|
|
63
|
+
? parsed
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validSessions(raw) {
|
|
71
|
+
if (!Array.isArray(raw))
|
|
72
|
+
return null;
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const s of raw) {
|
|
75
|
+
const rec = s;
|
|
76
|
+
if (typeof rec?.sessionId !== "string")
|
|
77
|
+
return null;
|
|
78
|
+
out.push({
|
|
79
|
+
sessionId: rec.sessionId,
|
|
80
|
+
...(typeof rec.title === "string" ? { title: rec.title } : {}),
|
|
81
|
+
updatedAt: typeof rec.updatedAt === "number" ? rec.updatedAt : Date.now(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* TCP-probe a bridge's loopback endpoint. Loopback refusals are instant, so
|
|
88
|
+
* the timeout only guards pathological cases; a bare connect+destroy is
|
|
89
|
+
* harmless to the bridge's HTTP server.
|
|
90
|
+
*/
|
|
91
|
+
function portOpen(port, timeoutMs) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const socket = new net.Socket();
|
|
94
|
+
const done = (ok) => {
|
|
95
|
+
socket.destroy();
|
|
96
|
+
resolve(ok);
|
|
97
|
+
};
|
|
98
|
+
socket.setTimeout(timeoutMs);
|
|
99
|
+
socket.once("connect", () => done(true));
|
|
100
|
+
socket.once("timeout", () => done(false));
|
|
101
|
+
socket.once("error", () => done(false));
|
|
102
|
+
socket.connect(port, "127.0.0.1");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Start the hub. Resolves once listening; rejects on bind failure (including
|
|
107
|
+
* EADDRINUSE when another hub already owns the port).
|
|
108
|
+
*/
|
|
109
|
+
export function startHub(options) {
|
|
110
|
+
const { port, host, token, heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, idleExitMs = IDLE_EXIT_MS, pingIntervalMs = PING_INTERVAL_MS, onIdleExit, } = options;
|
|
111
|
+
const instances = new Map();
|
|
112
|
+
const proxyPairs = new Set();
|
|
113
|
+
const timers = [];
|
|
114
|
+
let idleSince = null;
|
|
115
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
116
|
+
const server = createServer((req, res) => {
|
|
117
|
+
// Async handler failures (malformed URL, aborted body) must never escape
|
|
118
|
+
// into the event loop — warn and drop the connection.
|
|
119
|
+
void handleHttp(req, res).catch((e) => {
|
|
120
|
+
warn(`hub: request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
121
|
+
res.destroy();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
server.on("upgrade", (req, socket, head) => {
|
|
125
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
126
|
+
if (url.pathname !== "/acp") {
|
|
127
|
+
socket.destroy();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!authorized(req, url, token)) {
|
|
131
|
+
warn("hub: unauthorized WS upgrade rejected");
|
|
132
|
+
socket.destroy();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const entry = instances.get(url.searchParams.get("instance") ?? "");
|
|
136
|
+
if (!entry) {
|
|
137
|
+
warn("hub: WS upgrade for unknown instance rejected");
|
|
138
|
+
socket.destroy();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Dial the bridge's loopback endpoint before accepting the client side,
|
|
142
|
+
// so a dead bridge fails the upgrade instead of half-opening a pipe.
|
|
143
|
+
const bridge = new WebSocket(`ws://127.0.0.1:${entry.port}/acp`);
|
|
144
|
+
bridge.once("open", () => {
|
|
145
|
+
wss.handleUpgrade(req, socket, head, (client) => startProxy(client, bridge));
|
|
146
|
+
});
|
|
147
|
+
bridge.once("error", (e) => {
|
|
148
|
+
warn(`hub: dial bridge :${entry.port} failed: ${e.message}`);
|
|
149
|
+
socket.destroy();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
async function handleHttp(req, res) {
|
|
153
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
154
|
+
setCors(res);
|
|
155
|
+
if (req.method === "OPTIONS") {
|
|
156
|
+
res.writeHead(204);
|
|
157
|
+
res.end();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (url.pathname === "/api/health") {
|
|
161
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
162
|
+
res.end("ok");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (url.pathname === "/api/instances" && req.method === "GET") {
|
|
166
|
+
if (!authorized(req, url, token)) {
|
|
167
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
168
|
+
res.end("unauthorized");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// On-demand liveness probe (?probe=1): verify every registered bridge's
|
|
172
|
+
// loopback port and prune the unreachable ones before answering, so a
|
|
173
|
+
// client refresh gets an honest list instead of waiting out the
|
|
174
|
+
// heartbeat TTL (hard-killed bridges never unregister).
|
|
175
|
+
if (["1", "true"].includes((url.searchParams.get("probe") ?? "").toLowerCase())) {
|
|
176
|
+
const probes = await Promise.all(Array.from(instances.entries(), async ([id, entry]) => ({
|
|
177
|
+
id,
|
|
178
|
+
ok: await portOpen(entry.port, PROBE_TIMEOUT_MS),
|
|
179
|
+
})));
|
|
180
|
+
for (const { id, ok } of probes) {
|
|
181
|
+
if (!ok) {
|
|
182
|
+
instances.delete(id);
|
|
183
|
+
idleSince = null; // re-arm the idle clock on membership change
|
|
184
|
+
log(`hub: pruned instance ${id} (probe: endpoint unreachable)`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const list = Array.from(instances.values())
|
|
189
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
190
|
+
.map((e) => ({
|
|
191
|
+
id: e.id,
|
|
192
|
+
port: e.port,
|
|
193
|
+
pid: e.pid,
|
|
194
|
+
startedAt: e.startedAt,
|
|
195
|
+
workspace: e.workspace,
|
|
196
|
+
sessions: e.sessions,
|
|
197
|
+
}));
|
|
198
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
199
|
+
res.end(JSON.stringify(list));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if ((url.pathname === "/api/register" || url.pathname === "/api/unregister") &&
|
|
203
|
+
req.method === "POST") {
|
|
204
|
+
const body = await readJson(req);
|
|
205
|
+
if (!body || typeof body.token !== "string" || !tokenEquals(body.token, token)) {
|
|
206
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
207
|
+
res.end("unauthorized");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
211
|
+
if (!id) {
|
|
212
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
213
|
+
res.end("missing id");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (url.pathname === "/api/register") {
|
|
217
|
+
const bridgePort = typeof body.port === "number" ? body.port : 0;
|
|
218
|
+
const sessions = validSessions(body.sessions);
|
|
219
|
+
if (!(bridgePort >= 1 && bridgePort <= 65535) || !sessions) {
|
|
220
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
221
|
+
res.end("invalid register payload");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const prev = instances.get(id);
|
|
225
|
+
instances.set(id, {
|
|
226
|
+
id,
|
|
227
|
+
port: bridgePort,
|
|
228
|
+
pid: typeof body.pid === "number" ? body.pid : 0,
|
|
229
|
+
startedAt: prev?.startedAt ?? Date.now(),
|
|
230
|
+
workspace: typeof body.workspace === "string" ? body.workspace : "",
|
|
231
|
+
sessions,
|
|
232
|
+
lastSeen: Date.now(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
instances.delete(id);
|
|
237
|
+
idleSince = null; // re-arm the idle clock on membership change
|
|
238
|
+
}
|
|
239
|
+
// Version self-upgrade: a bridge NEWER than this hub just registered,
|
|
240
|
+
// so this process is running stale code. Reply first (the bridge
|
|
241
|
+
// re-spawns the hub from its own, newer dist when it sees `restarting`),
|
|
242
|
+
// then exit. Equal/older/absent versions never trigger a restart.
|
|
243
|
+
const stale = url.pathname === "/api/register" &&
|
|
244
|
+
typeof body.version === "string" &&
|
|
245
|
+
compareVersions(body.version, AGENT_INFO.version) > 0;
|
|
246
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
247
|
+
res.end(JSON.stringify(stale ? { ok: true, restarting: true } : { ok: true }));
|
|
248
|
+
if (stale) {
|
|
249
|
+
log(`hub: bridge ${body.version} is newer than hub ${AGENT_INFO.version} — restarting to upgrade`);
|
|
250
|
+
const restart = setTimeout(() => {
|
|
251
|
+
void close().finally(() => onIdleExit?.());
|
|
252
|
+
}, 500);
|
|
253
|
+
restart.unref();
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
258
|
+
res.end("not found");
|
|
259
|
+
}
|
|
260
|
+
function startProxy(client, bridge) {
|
|
261
|
+
const pair = { client, bridge };
|
|
262
|
+
proxyPairs.add(pair);
|
|
263
|
+
const teardown = () => {
|
|
264
|
+
if (!proxyPairs.delete(pair))
|
|
265
|
+
return;
|
|
266
|
+
client.close();
|
|
267
|
+
bridge.close();
|
|
268
|
+
};
|
|
269
|
+
// Forward with the frame's binary flag intact: `ws.send(buffer)` defaults
|
|
270
|
+
// to a BINARY frame, and the SDK's WS server drops non-text frames.
|
|
271
|
+
const forward = (to) => (data, isBinary) => {
|
|
272
|
+
if (to.readyState === WebSocket.OPEN)
|
|
273
|
+
to.send(data, { binary: isBinary });
|
|
274
|
+
};
|
|
275
|
+
client.on("message", forward(bridge));
|
|
276
|
+
bridge.on("message", forward(client));
|
|
277
|
+
client.on("close", teardown);
|
|
278
|
+
client.on("error", teardown);
|
|
279
|
+
bridge.on("close", teardown);
|
|
280
|
+
bridge.on("error", teardown);
|
|
281
|
+
}
|
|
282
|
+
// Prune instances whose bridge stopped heartbeating (crash or Zed exit).
|
|
283
|
+
const pruner = setInterval(() => {
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
for (const [id, entry] of instances) {
|
|
286
|
+
if (now - entry.lastSeen > heartbeatTimeoutMs) {
|
|
287
|
+
instances.delete(id);
|
|
288
|
+
idleSince = null;
|
|
289
|
+
log(`hub: pruned instance ${id} (heartbeat timeout)`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}, Math.min(heartbeatTimeoutMs / 2, 5_000));
|
|
293
|
+
pruner.unref();
|
|
294
|
+
timers.push(pruner);
|
|
295
|
+
// Keepalive pings on both legs — tunnels (notably Cloudflare) drop idle WS.
|
|
296
|
+
const pinger = setInterval(() => {
|
|
297
|
+
for (const { client, bridge } of proxyPairs) {
|
|
298
|
+
if (client.readyState === WebSocket.OPEN)
|
|
299
|
+
client.ping();
|
|
300
|
+
if (bridge.readyState === WebSocket.OPEN)
|
|
301
|
+
bridge.ping();
|
|
302
|
+
}
|
|
303
|
+
}, pingIntervalMs);
|
|
304
|
+
pinger.unref();
|
|
305
|
+
timers.push(pinger);
|
|
306
|
+
// Idle exit: with nothing registered and nobody proxied, the hub exits; the
|
|
307
|
+
// next bridge re-spawns it on demand (see endpoint.ts).
|
|
308
|
+
const idleCheck = setInterval(() => {
|
|
309
|
+
if (instances.size > 0 || proxyPairs.size > 0) {
|
|
310
|
+
idleSince = null;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (idleSince === null)
|
|
314
|
+
idleSince = Date.now();
|
|
315
|
+
if (Date.now() - idleSince >= idleExitMs) {
|
|
316
|
+
log("hub: idle for too long with no instances — exiting");
|
|
317
|
+
clearInterval(idleCheck);
|
|
318
|
+
void close().finally(() => onIdleExit?.());
|
|
319
|
+
}
|
|
320
|
+
}, Math.min(idleExitMs / 4, 10_000));
|
|
321
|
+
idleCheck.unref();
|
|
322
|
+
timers.push(idleCheck);
|
|
323
|
+
async function close() {
|
|
324
|
+
for (const t of timers)
|
|
325
|
+
clearInterval(t);
|
|
326
|
+
for (const { client, bridge } of proxyPairs) {
|
|
327
|
+
client.terminate();
|
|
328
|
+
bridge.terminate();
|
|
329
|
+
}
|
|
330
|
+
proxyPairs.clear();
|
|
331
|
+
wss.close();
|
|
332
|
+
server.closeAllConnections?.();
|
|
333
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
334
|
+
}
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
server.once("error", reject);
|
|
337
|
+
server.listen(port, host, () => {
|
|
338
|
+
// port 0 binds an ephemeral port — report the actual one (used by tests).
|
|
339
|
+
const addr = server.address();
|
|
340
|
+
const bound = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
341
|
+
log(`hub: listening on ${host}:${bound} (instances: 0)`);
|
|
342
|
+
resolve({ port: bound, close });
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=hub-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-server.js","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"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"}
|