mindwire 0.1.34 → 0.1.36

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.
Files changed (39) hide show
  1. package/dist/binary-cache-CG52TXCZ.js +3 -0
  2. package/dist/{binary-cache-VM7VHCEU.js.map → binary-cache-CG52TXCZ.js.map} +1 -1
  3. package/dist/chunk-67BNDGRK.js +20 -0
  4. package/dist/chunk-67BNDGRK.js.map +1 -0
  5. package/dist/{chunk-3FR7BFAW.js → chunk-PFYKJ6LX.js} +56 -4
  6. package/dist/chunk-PFYKJ6LX.js.map +1 -0
  7. package/dist/{chunk-3QFDSYKC.js → chunk-RTGZDLSG.js} +523 -172
  8. package/dist/chunk-RTGZDLSG.js.map +1 -0
  9. package/dist/cli.js +763 -32
  10. package/dist/cli.js.map +1 -1
  11. package/dist/computer/connection-change.d.ts +13 -0
  12. package/dist/computer/connection-flow.d.ts +3 -3
  13. package/dist/computer/directory-http.d.ts +8 -0
  14. package/dist/computer/discovery.d.ts +13 -0
  15. package/dist/computer/lifecycle.d.ts +2 -1
  16. package/dist/computer/managed-relay.d.ts +27 -0
  17. package/dist/computer/oblien-auth.d.ts +25 -0
  18. package/dist/computer/provider-errors.d.ts +9 -0
  19. package/dist/computer/provider-info.d.ts +7 -0
  20. package/dist/computer/provider-setup.d.ts +20 -0
  21. package/dist/computer/relay-health.d.ts +2 -2
  22. package/dist/computer/relay.d.ts +11 -3
  23. package/dist/computer/setup-prompt.d.ts +9 -0
  24. package/dist/computer.d.ts +27 -1
  25. package/dist/connection-change-IBNL42BL.js +41 -0
  26. package/dist/connection-change-IBNL42BL.js.map +1 -0
  27. package/dist/{controller-AGT6MCJG.js → controller-2SJLZMJZ.js} +199 -279
  28. package/dist/controller-2SJLZMJZ.js.map +1 -0
  29. package/dist/{controller-handoff-3LAYBJUL.js → controller-handoff-RUUHODNC.js} +4 -4
  30. package/dist/{controller-handoff-3LAYBJUL.js.map → controller-handoff-RUUHODNC.js.map} +1 -1
  31. package/dist/index.cjs +12 -3
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +12 -3
  34. package/dist/index.js.map +1 -1
  35. package/package.json +5 -5
  36. package/dist/binary-cache-VM7VHCEU.js +0 -4
  37. package/dist/chunk-3FR7BFAW.js.map +0 -1
  38. package/dist/chunk-3QFDSYKC.js.map +0 -1
  39. package/dist/controller-AGT6MCJG.js.map +0 -1
@@ -1,247 +1,17 @@
1
- import { binaryCacheDirectory, cachedExecutable, verifyReleaseBytes, cacheExecutable, executableChecksum } from './chunk-3FR7BFAW.js';
2
- import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, writeJSON, directRoutes, CONTROLLER_PROTOCOL, ownChild } from './chunk-3QFDSYKC.js';
3
- import './chunk-T6TD76UA.js';
4
- import { execFile, execFileSync, spawn } from 'child_process';
5
- import { randomBytes, randomUUID } from 'crypto';
1
+ import { connectionInfo } from './chunk-67BNDGRK.js';
2
+ import { readJSON, processStateAlive, currentProcessIdentity, ensureDaemonBinary, SDK_VERSION, computerClient, processIdentity, adoptProcess, adoptRelay, RelayOutput, writeJSON, startRelay, providerNeedsAction, directRoutes, ProviderConnectionError, CONTROLLER_PROTOCOL, ownChild, hasCertificateError } from './chunk-RTGZDLSG.js';
3
+ import './chunk-PFYKJ6LX.js';
4
+ import { execFileSync, spawn } from 'child_process';
5
+ import { randomBytes } from 'crypto';
6
6
  import * as fs from 'fs/promises';
7
- import { open, rm, readFile, mkdtemp, writeFile } from 'fs/promises';
8
- import * as path3 from 'path';
7
+ import * as path from 'path';
9
8
  import { setTimeout as setTimeout$1 } from 'timers/promises';
10
- import { tmpdir } from 'os';
11
- import { promisify } from 'util';
12
9
  import { request } from 'https';
13
10
  import { lookup } from 'dns';
14
11
  import { Resolver } from 'dns/promises';
15
12
  import { isIP } from 'net';
16
13
  import WebSocket from 'ws';
17
14
 
18
- var CLOUDFLARED_VERSION = "2026.9.3";
19
- var execute = promisify(execFile);
20
- async function ensureCloudflareBinary(options = {}) {
21
- const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
22
- const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
23
- if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
24
- throw new Error("Install cloudflared for this computer, or select another connection provider.");
25
- }
26
- const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
27
- const bin = path3.join(
28
- options.cacheDir ?? await binaryCacheDirectory(),
29
- "cloudflared",
30
- CLOUDFLARED_VERSION,
31
- `${platform}-${arch}`,
32
- platform === "win32" ? "cloudflared.exe" : "cloudflared"
33
- );
34
- const cached = await cachedExecutable(bin);
35
- if (cached) return cached;
36
- await options.onProgress?.("Downloading the secure connection helper\u2026");
37
- const request2 = options.fetch ?? fetch;
38
- const release = await request2(
39
- `https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
40
- { headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
41
- );
42
- if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
43
- const metadata = await release.json();
44
- const item = metadata.assets?.find((value) => value.name === asset);
45
- const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
46
- const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
47
- if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
48
- const downloaded = await request2(expectedURL, { signal: AbortSignal.timeout(12e4) });
49
- if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
50
- const archive = new Uint8Array(await downloaded.arrayBuffer());
51
- await verifyReleaseBytes(archive, checksum, asset);
52
- let executable = archive;
53
- if (platform === "darwin") {
54
- const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
55
- try {
56
- const archivePath = path3.join(temporary, asset);
57
- await writeFile(archivePath, archive, { mode: 384 });
58
- const result = await execute(
59
- "/usr/bin/tar",
60
- ["-xzOf", archivePath, "cloudflared"],
61
- { encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
62
- );
63
- executable = new Uint8Array(result.stdout);
64
- } finally {
65
- await rm(temporary, { recursive: true, force: true });
66
- }
67
- }
68
- return cacheExecutable(bin, executable, await executableChecksum(executable));
69
- }
70
- async function cloudflareCommand(options) {
71
- try {
72
- await execute("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
73
- return "cloudflared";
74
- } catch (error) {
75
- if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
76
- return ensureCloudflareBinary(options);
77
- }
78
- }
79
- var RelayOutput = class _RelayOutput {
80
- constructor(file, handle) {
81
- this.file = file;
82
- this.handle = handle;
83
- }
84
- file;
85
- handle;
86
- position = 0;
87
- timer;
88
- pending = Promise.resolve();
89
- closing;
90
- static async create(directory = tmpdir()) {
91
- const file = path3.join(directory, `.relay-output-${randomUUID()}.log`);
92
- return new _RelayOutput(file, await open(file, "ax+", 384));
93
- }
94
- static async adopt(directory, file) {
95
- if (path3.dirname(file) !== path3.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path3.basename(file))) return;
96
- try {
97
- const output = new _RelayOutput(file, await open(file, "r+"));
98
- output.discard();
99
- return output;
100
- } catch (error) {
101
- if (error.code !== "ENOENT") throw error;
102
- }
103
- }
104
- get fd() {
105
- return this.handle.fd;
106
- }
107
- async read() {
108
- const { size } = await this.handle.stat();
109
- if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
110
- const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
111
- const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
112
- this.position += bytesRead;
113
- return data.subarray(0, bytesRead);
114
- }
115
- discard() {
116
- if (this.timer || this.closing) return;
117
- const drain = () => {
118
- this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
119
- });
120
- };
121
- drain();
122
- this.timer = setInterval(drain, 2e3);
123
- this.timer.unref();
124
- }
125
- close() {
126
- return this.closing ??= (async () => {
127
- clearInterval(this.timer);
128
- await this.pending;
129
- await this.handle.close();
130
- await rm(this.file, { force: true });
131
- })();
132
- }
133
- };
134
-
135
- // src/computer/relay.ts
136
- async function adoptRelay(owner, route, directory, outputFile) {
137
- const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
138
- return { route, owner, outputFile: output?.file, async close() {
139
- await owner.close();
140
- await output?.close();
141
- } };
142
- }
143
- function websocketURL(value) {
144
- const url = new URL(value);
145
- if (url.protocol === "https:") url.protocol = "wss:";
146
- if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
147
- throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
148
- }
149
- if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
150
- return url.toString();
151
- }
152
- async function startRelay(options, port, runtime = {}) {
153
- runtime.signal?.throwIfAborted();
154
- if (options.kind === "none") return { close() {
155
- } };
156
- if (options.kind === "custom") {
157
- if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
158
- return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
159
- } };
160
- }
161
- const origin = `http://127.0.0.1:${port}`;
162
- const env = { ...process.env };
163
- let command, args;
164
- if (options.kind === "cloudflare") {
165
- command = await cloudflareCommand(runtime);
166
- await runtime.onProgress?.("Connecting through Cloudflare\u2026");
167
- if (options.cloudflareTokenFile) {
168
- if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
169
- env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
170
- args = ["tunnel", "--no-autoupdate", "run"];
171
- } else {
172
- if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
173
- args = ["tunnel", "--no-autoupdate", "--url", origin];
174
- }
175
- } else {
176
- command = "ngrok";
177
- args = ["http", origin, "--log", "stdout", "--log-format", "json"];
178
- if (options.url) {
179
- const url = new URL(websocketURL(options.url));
180
- args.push("--url", `https://${url.host}`);
181
- }
182
- }
183
- runtime.signal?.throwIfAborted();
184
- const output = await RelayOutput.create(runtime.outputDirectory);
185
- const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
186
- let failure;
187
- child.on("error", (error) => {
188
- failure = error;
189
- });
190
- let owner;
191
- try {
192
- await new Promise((resolve3, reject) => {
193
- child.once("spawn", resolve3);
194
- child.once("error", reject);
195
- });
196
- owner = await ownChild(child);
197
- await runtime.onSpawn?.(owner, output.file);
198
- const deadline = Date.now() + 45e3;
199
- let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
200
- while (Date.now() < deadline) {
201
- runtime.signal?.throwIfAborted();
202
- if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
203
- if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
204
- const received = buffer + (await output.read()).toString();
205
- buffer = received.slice(-16384);
206
- let found;
207
- if (options.kind === "cloudflare") {
208
- cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
209
- cloudflareConnected ||= /Registered tunnel connection/.test(received);
210
- if (cloudflareConnected) found = cloudflareAddress;
211
- } else {
212
- for (const line of received.split(/\r?\n/)) {
213
- try {
214
- const event = JSON.parse(line);
215
- if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
216
- } catch {
217
- }
218
- }
219
- }
220
- if (found) {
221
- const route = { kind: "websocket", url: websocketURL(found) };
222
- output.discard();
223
- const provider = owner;
224
- return {
225
- route,
226
- process: child,
227
- owner,
228
- outputFile: output.file,
229
- async close() {
230
- await provider.close();
231
- await output.close();
232
- }
233
- };
234
- }
235
- await setTimeout$1(50, void 0, { signal: runtime.signal });
236
- }
237
- throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
238
- } catch (error) {
239
- if (owner) await owner.close();
240
- else if (child.pid) await (await ownChild(child)).close();
241
- await output.close();
242
- throw error;
243
- }
244
- }
245
15
  var RelayCheckError = class extends Error {
246
16
  constructor(code, message, cause, addressMissing = false) {
247
17
  super(message, cause ? { cause } : void 0);
@@ -253,7 +23,7 @@ var RelayCheckError = class extends Error {
253
23
  addressMissing;
254
24
  };
255
25
  function relayNeedsRestart(error, failures, failedForMs) {
256
- return failures >= 2 && failedForMs >= 9e4 && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
26
+ return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "provider_certificate") && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
257
27
  }
258
28
  var quickTunnelHost = /^[a-z0-9-]+\.trycloudflare\.com$/i;
259
29
  async function resolveCloudflareAddress(hostname, family = 0, options = {}) {
@@ -336,7 +106,7 @@ var relayLookup = (hostname, options, callback) => {
336
106
  async function checkRelay(route, options = {}) {
337
107
  options.signal?.throwIfAborted();
338
108
  if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
339
- await new Promise((resolve3, reject) => {
109
+ await new Promise((resolve2, reject) => {
340
110
  const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
341
111
  let finished = false, banner = "";
342
112
  const finish = (error) => {
@@ -346,12 +116,14 @@ async function checkRelay(route, options = {}) {
346
116
  options.signal?.removeEventListener("abort", aborted);
347
117
  socket.terminate();
348
118
  if (error) reject(error);
349
- else resolve3();
119
+ else resolve2();
350
120
  };
351
121
  const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
352
122
  const timer = setTimeout(() => finish(new RelayCheckError("timeout", "The internet tunnel isn't responding. Retrying\u2026")), options.timeoutMs ?? 8e3);
353
123
  socket.on("error", (error) => {
354
- if (["ENOTFOUND", "EAI_AGAIN"].includes(error.code ?? "")) {
124
+ if (hasCertificateError(error)) {
125
+ finish(new RelayCheckError("provider_certificate", "The tunnel's TLS certificate could not be verified. Check the provider's certificate and this computer's date and time.", error));
126
+ } else if (["ENOTFOUND", "EAI_AGAIN"].includes(error.code ?? "")) {
355
127
  const provider = new URL(route.url).hostname.endsWith(".trycloudflare.com") ? "Cloudflare's" : "the tunnel's";
356
128
  finish(new RelayCheckError(
357
129
  "dns",
@@ -385,17 +157,17 @@ async function checkRelay(route, options = {}) {
385
157
  });
386
158
  }
387
159
  async function relayProviderReachable(options, signal) {
388
- if (options.kind !== "cloudflare" && options.kind !== "ngrok") return false;
160
+ if (!["cloudflare", "ngrok", "oblien"].includes(options.kind)) return false;
389
161
  signal?.throwIfAborted();
390
- return new Promise((resolve3) => {
391
- const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : "https://api.ngrok.com/";
162
+ return new Promise((resolve2) => {
163
+ const url = options.kind === "cloudflare" ? "https://api.trycloudflare.com/" : options.kind === "oblien" ? "https://api.oblien.com/" : "https://api.ngrok.com/";
392
164
  let finished = false;
393
165
  const done = (reachable) => {
394
166
  if (finished) return;
395
167
  finished = true;
396
168
  clearTimeout(timer);
397
169
  probe.destroy();
398
- resolve3(reachable);
170
+ resolve2(reachable);
399
171
  };
400
172
  const probe = request(url, { method: "HEAD", signal }, (response) => {
401
173
  response.destroy();
@@ -408,15 +180,26 @@ async function relayProviderReachable(options, signal) {
408
180
  }
409
181
 
410
182
  // src/computer/controller.ts
183
+ async function saveRelay(file, handle, port, options) {
184
+ if (handle?.owner?.identity) await writeJSON(file, {
185
+ owner: handle.owner.identity,
186
+ port,
187
+ options: JSON.stringify(options),
188
+ route: handle.route,
189
+ outputFile: handle.outputFile,
190
+ configFile: handle.configFile
191
+ });
192
+ else await fs.rm(file, { force: true });
193
+ }
411
194
  function recoveryDelay(failures) {
412
195
  return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
413
196
  }
414
197
  async function superviseComputer(directory) {
415
- if ((await readJSON(path3.join(directory, "computer-stopped.json")))?.stopped) return;
416
- let config = await readJSON(path3.join(directory, "computer-config.json"));
198
+ if ((await readJSON(path.join(directory, "computer-stopped.json")))?.stopped) return;
199
+ let config = await readJSON(path.join(directory, "computer-config.json"));
417
200
  if (!config) throw new Error("Computer configuration is missing.");
418
- const controllerPath = path3.join(directory, "computer-controller.json");
419
- const relayPath = path3.join(directory, "computer-relay.json");
201
+ const controllerPath = path.join(directory, "computer-controller.json");
202
+ const relayPath = path.join(directory, "computer-relay.json");
420
203
  const previous = await readJSON(controllerPath);
421
204
  if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
422
205
  const owner = await currentProcessIdentity();
@@ -424,6 +207,7 @@ async function superviseComputer(directory) {
424
207
  await phase("Preparing Mindwire\u2026");
425
208
  let child, relay, stopping = false;
426
209
  let relayTask, updateTask;
210
+ let connectionTask;
427
211
  let relayResult;
428
212
  let checkTask;
429
213
  let checkResult;
@@ -436,30 +220,30 @@ async function superviseComputer(directory) {
436
220
  process.on("SIGINT", stop);
437
221
  let token;
438
222
  try {
439
- token = (await fs.readFile(path3.join(directory, "daemon.token"), "utf8")).trim();
223
+ token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
440
224
  } catch (error) {
441
225
  if (error.code !== "ENOENT") throw error;
442
226
  token = randomBytes(32).toString("hex");
443
- await fs.writeFile(path3.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
227
+ await fs.writeFile(path.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
444
228
  }
445
229
  const daemonEnv = { ...process.env };
446
230
  if (process.platform === "win32") {
447
231
  try {
448
232
  const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
449
233
  if (git) {
450
- const shellDirectory = path3.resolve(path3.dirname(git), "..", "bin");
451
- await fs.access(path3.join(shellDirectory, "bash.exe"));
234
+ const shellDirectory = path.resolve(path.dirname(git), "..", "bin");
235
+ await fs.access(path.join(shellDirectory, "bash.exe"));
452
236
  const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
453
- daemonEnv[pathKey] = `${shellDirectory}${path3.delimiter}${daemonEnv[pathKey] ?? ""}`;
237
+ daemonEnv[pathKey] = `${shellDirectory}${path.delimiter}${daemonEnv[pathKey] ?? ""}`;
454
238
  }
455
239
  } catch {
456
240
  }
457
241
  }
458
- const oldRuntime = await readJSON(path3.join(directory, "computer-runtime.json"));
242
+ const oldRuntime = await readJSON(path.join(directory, "computer-runtime.json"));
459
243
  let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
460
244
  let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
461
245
  const launch = async (binary) => {
462
- await fs.rm(path3.join(directory, "computer-runtime.json"), { force: true });
246
+ await fs.rm(path.join(directory, "computer-runtime.json"), { force: true });
463
247
  const process2 = spawn(binary, ["--computer"], {
464
248
  cwd: config.directory,
465
249
  stdio: ["ignore", "inherit", "inherit"],
@@ -468,8 +252,8 @@ async function superviseComputer(directory) {
468
252
  ...daemonEnv,
469
253
  DAEMON_TOKEN: token,
470
254
  ADDR: "127.0.0.1:0",
471
- STATE_PATH: path3.join(directory, "agent-state.json"),
472
- WORKSPACE_DB_PATH: path3.join(directory, "workspace.db"),
255
+ STATE_PATH: path.join(directory, "agent-state.json"),
256
+ WORKSPACE_DB_PATH: path.join(directory, "workspace.db"),
473
257
  AGENT_CWD: config.directory,
474
258
  COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
475
259
  COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
@@ -480,8 +264,8 @@ async function superviseComputer(directory) {
480
264
  process2.on("error", (error) => {
481
265
  failure = error;
482
266
  });
483
- await new Promise((resolve3, reject) => {
484
- process2.once("spawn", resolve3);
267
+ await new Promise((resolve2, reject) => {
268
+ process2.once("spawn", resolve2);
485
269
  process2.once("error", reject);
486
270
  });
487
271
  child = await ownChild(process2);
@@ -534,11 +318,18 @@ async function superviseComputer(directory) {
534
318
  client = await launch(binary);
535
319
  }
536
320
  const saved = await readJSON(relayPath);
321
+ const candidatePath = path.join(directory, "computer-relay-candidate.json");
322
+ const abandonedCandidate = await readJSON(candidatePath);
323
+ if (abandonedCandidate && (abandonedCandidate.owner.pid !== saved?.owner.pid || abandonedCandidate.owner.start !== saved?.owner.start)) {
324
+ const abandonedOwner = await adoptProcess(abandonedCandidate.owner);
325
+ if (abandonedOwner) await (await adoptRelay(abandonedOwner, abandonedCandidate.route, directory, abandonedCandidate.outputFile, abandonedCandidate.configFile)).close();
326
+ }
327
+ await fs.rm(candidatePath, { force: true });
537
328
  if (saved) {
538
329
  const owner2 = await adoptProcess(saved.owner);
539
330
  if (owner2) {
540
331
  if (saved.outputFile && saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
541
- relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
332
+ relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
542
333
  relayWasReachable = !!previous?.routes?.some((route) => JSON.stringify(route) === JSON.stringify(saved.route));
543
334
  if (previous?.relayFailureSince && relayWasReachable) {
544
335
  relayFailedAt = Math.min(Date.now(), previous.relayFailureSince);
@@ -546,7 +337,7 @@ async function superviseComputer(directory) {
546
337
  }
547
338
  } else {
548
339
  if (!saved.outputFile) await phase("Refreshing the internet tunnel for this update\u2026");
549
- const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
340
+ const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
550
341
  await abandoned.close();
551
342
  await fs.rm(relayPath, { force: true });
552
343
  }
@@ -556,6 +347,128 @@ async function superviseComputer(directory) {
556
347
  }
557
348
  let lastController = "";
558
349
  while (!stopping) {
350
+ const changePath = path.join(directory, "computer-connection-change.json");
351
+ const change = !connectionTask && !relayTask && !updateTask && !changingDaemon && client && child?.alive() ? await readJSON(changePath) : void 0;
352
+ if (change && ["queued", "connecting"].includes(change.status)) {
353
+ connectionTask = (async () => {
354
+ const previousConfig = config;
355
+ const next = { ...previousConfig, ...change.patch };
356
+ let previousRelay = relay;
357
+ let releasedPrevious = false;
358
+ let candidate;
359
+ let committed = false;
360
+ let lastVerificationError;
361
+ const changing = new AbortController();
362
+ const cancel = () => changing.abort();
363
+ abort.signal.addEventListener("abort", cancel, { once: true });
364
+ const timeout = setTimeout(cancel, 115e3);
365
+ const report = (message) => writeJSON(changePath, { ...change, status: "connecting", message });
366
+ try {
367
+ await report("Preparing the new connection; your current service keeps running\u2026");
368
+ if (JSON.stringify(next.relay) === JSON.stringify(previousConfig.relay) && previousRelay && (!previousRelay.owner || previousRelay.owner.alive())) candidate = previousRelay;
369
+ else {
370
+ if (previousRelay?.owner && ["ngrok", "oblien"].includes(next.relay.kind) && next.relay.kind === previousConfig.relay.kind && next.relay.url === previousConfig.relay.url) {
371
+ await report("Refreshing the saved provider connection; your terminals keep running\u2026");
372
+ releasedPrevious = true;
373
+ relayReachable = false;
374
+ await previousRelay.close();
375
+ }
376
+ candidate = await startRelay(next.relay, websocketPort, {
377
+ cacheDir: path.join(directory, "tools"),
378
+ outputDirectory: directory,
379
+ cliPath: process.argv[1],
380
+ signal: changing.signal,
381
+ onProgress: report,
382
+ onSpawn: async (owner2, outputFile, configFile) => {
383
+ if (owner2.identity) await writeJSON(candidatePath, {
384
+ owner: owner2.identity,
385
+ port: websocketPort,
386
+ options: JSON.stringify(next.relay),
387
+ outputFile,
388
+ configFile
389
+ });
390
+ }
391
+ });
392
+ }
393
+ if (next.relay.kind !== "none") {
394
+ if (!candidate?.route) throw new Error("The provider didn't return an address.");
395
+ await report("Verifying the new secure connection\u2026");
396
+ for (; ; ) {
397
+ changing.signal.throwIfAborted();
398
+ try {
399
+ await checkRelay(candidate.route, { signal: changing.signal });
400
+ break;
401
+ } catch (error) {
402
+ if (error instanceof Error) lastVerificationError = error;
403
+ if (error instanceof RelayCheckError && providerNeedsAction(error.code)) throw error;
404
+ await setTimeout$1(2e3, void 0, { signal: changing.signal });
405
+ }
406
+ }
407
+ }
408
+ changing.signal.throwIfAborted();
409
+ const nextRoutes = directRoutes(next, sshPort);
410
+ if (candidate?.route) nextRoutes.push(candidate.route);
411
+ if (!nextRoutes.length) throw new Error("No reachable address was configured.");
412
+ await client.computer.setRoutes(nextRoutes, connectionInfo(next.relay, next.host));
413
+ await writeJSON(path.join(directory, "computer-config.json"), next);
414
+ await saveRelay(relayPath, candidate, websocketPort, next.relay);
415
+ config = next;
416
+ relay = candidate;
417
+ routes = nextRoutes;
418
+ published = "";
419
+ relayReachable = true;
420
+ relayWasReachable = true;
421
+ relayHealthFailures = 0;
422
+ relayFailedAt = 0;
423
+ relayError = void 0;
424
+ relayErrorCode = void 0;
425
+ checkedAt = Date.now();
426
+ nextCheckAt = checkedAt + 3e4;
427
+ committed = true;
428
+ if (previousRelay && previousRelay !== candidate) await previousRelay.close();
429
+ await writeJSON(changePath, { ...change, status: "complete", message: "Persistent connection is ready." });
430
+ } catch (error) {
431
+ if (!committed) {
432
+ if (candidate && candidate !== previousRelay) await candidate.close();
433
+ await writeJSON(path.join(directory, "computer-config.json"), previousConfig);
434
+ if (releasedPrevious) {
435
+ previousRelay = void 0;
436
+ if (!stopping) {
437
+ await report("Restoring the previous provider connection\u2026");
438
+ try {
439
+ previousRelay = await startRelay(previousConfig.relay, websocketPort, {
440
+ cacheDir: path.join(directory, "tools"),
441
+ outputDirectory: directory,
442
+ cliPath: process.argv[1],
443
+ signal: abort.signal
444
+ });
445
+ } catch {
446
+ }
447
+ }
448
+ relayReachable = false;
449
+ nextCheckAt = 0;
450
+ nextRelayAt = 0;
451
+ }
452
+ relay = previousRelay;
453
+ await saveRelay(relayPath, previousRelay, websocketPort, previousConfig.relay);
454
+ published = "";
455
+ }
456
+ await writeJSON(changePath, {
457
+ ...change,
458
+ status: committed ? "complete" : "failed",
459
+ message: committed ? "Connection is ready." : `Connection setup didn't finish. ${error instanceof Error && error.name !== "AbortError" ? error.message : lastVerificationError?.message ?? "Check the provider configuration and internet connection."} The previous service keeps running.`
460
+ });
461
+ } finally {
462
+ clearTimeout(timeout);
463
+ abort.signal.removeEventListener("abort", cancel);
464
+ await fs.rm(candidatePath, { force: true });
465
+ }
466
+ })().catch(() => {
467
+ relayError = "The connection change could not be saved. Check free disk space, then run mindwire connection again.";
468
+ }).finally(() => {
469
+ connectionTask = void 0;
470
+ });
471
+ }
559
472
  if (!changingDaemon && child && !child.alive()) {
560
473
  child = void 0;
561
474
  client = void 0;
@@ -580,7 +493,8 @@ async function superviseComputer(directory) {
580
493
  nextDaemonAt = Date.now() + recoveryDelay(++daemonFailures);
581
494
  }
582
495
  }
583
- if (relay?.owner && !relay.owner.alive()) {
496
+ const requestedCheck = await readJSON(path.join(directory, "computer-check.json"));
497
+ if (!connectionTask && relay?.owner && !relay.owner.alive()) {
584
498
  await relay.close();
585
499
  relay = void 0;
586
500
  relayReachable = false;
@@ -608,30 +522,35 @@ async function superviseComputer(directory) {
608
522
  port: websocketPort,
609
523
  options: JSON.stringify(config.relay),
610
524
  route: relay.route,
611
- outputFile: relay.outputFile
525
+ outputFile: relay.outputFile,
526
+ configFile: relay.configFile
612
527
  });
613
528
  } else {
529
+ checkedAt = Date.now();
614
530
  relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
615
- nextRelayAt = Date.now() + recoveryDelay(++relayFailures);
531
+ relayErrorCode = result.error instanceof ProviderConnectionError ? result.error.code : void 0;
532
+ nextRelayAt = Date.now() + (providerNeedsAction(relayErrorCode) ? 6e4 : recoveryDelay(++relayFailures));
616
533
  }
617
534
  }
618
- if (!relay && !relayTask && Date.now() >= nextRelayAt) {
535
+ if (!relay && !relayTask && !connectionTask && (Date.now() >= nextRelayAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
619
536
  relayError = void 0;
620
537
  relayErrorCode = void 0;
621
538
  relayMessage = "Reconnecting your internet tunnel\u2026";
622
539
  relayTask = startRelay(config.relay, websocketPort, {
623
- cacheDir: path3.join(directory, "tools"),
540
+ cacheDir: path.join(directory, "tools"),
624
541
  outputDirectory: directory,
542
+ cliPath: process.argv[1],
625
543
  signal: abort.signal,
626
544
  onProgress: (message) => {
627
545
  relayMessage = message;
628
546
  },
629
- onSpawn: async (owner2, outputFile) => {
547
+ onSpawn: async (owner2, outputFile, configFile) => {
630
548
  if (owner2.identity) await writeJSON(relayPath, {
631
549
  owner: owner2.identity,
632
550
  port: websocketPort,
633
551
  options: JSON.stringify(config.relay),
634
- outputFile
552
+ outputFile,
553
+ configFile
635
554
  });
636
555
  }
637
556
  }).then((handle) => {
@@ -660,7 +579,7 @@ async function superviseComputer(directory) {
660
579
  relayFailedAt ||= checkedAt;
661
580
  relayHealthFailures++;
662
581
  nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
663
- if (result.canRestart) {
582
+ if (result.canRestart && !connectionTask) {
664
583
  await phase(result.error instanceof RelayCheckError && result.error.addressMissing ? "Cloudflare's address expired. Creating a new secure tunnel\u2026" : "The internet tunnel stopped responding. Replacing it\u2026");
665
584
  await relay.close();
666
585
  relay = void 0;
@@ -671,7 +590,6 @@ async function superviseComputer(directory) {
671
590
  }
672
591
  }
673
592
  }
674
- const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
675
593
  if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
676
594
  if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
677
595
  const checking = relay;
@@ -687,10 +605,11 @@ async function superviseComputer(directory) {
687
605
  }
688
606
  routes = directRoutes(config, sshPort);
689
607
  if (relay?.route && relayWasReachable) routes.push(relay.route);
690
- const routeJSON = JSON.stringify(routes);
608
+ const metadata = connectionInfo(config.relay, config.host);
609
+ const routeJSON = JSON.stringify({ routes, connection: metadata });
691
610
  if (!changingDaemon && client && child?.alive() && routeJSON !== published) {
692
611
  try {
693
- await client.computer.setRoutes(routes);
612
+ await client.computer.setRoutes(routes, metadata);
694
613
  published = routeJSON;
695
614
  } catch {
696
615
  }
@@ -715,8 +634,8 @@ async function superviseComputer(directory) {
715
634
  await writeJSON(controllerPath, state);
716
635
  lastController = serialized;
717
636
  }
718
- const updatePath = path3.join(directory, "computer-update.json");
719
- const update = !updateTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
637
+ const updatePath = path.join(directory, "computer-update.json");
638
+ const update = !updateTask && !connectionTask && client && child?.alive() ? await readJSON(updatePath) : void 0;
720
639
  if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
721
640
  updateTask = (async () => {
722
641
  const status = async (state2, error) => writeJSON(
@@ -755,9 +674,9 @@ async function superviseComputer(directory) {
755
674
  }
756
675
  binary = replacement;
757
676
  config = { ...config, daemonBin: void 0, version: update.version };
758
- await writeJSON(path3.join(directory, "computer-config.json"), config);
759
- await client.computer.setRoutes(routes);
760
- published = JSON.stringify(routes);
677
+ await writeJSON(path.join(directory, "computer-config.json"), config);
678
+ await client.computer.setRoutes(routes, connectionInfo(config.relay, config.host));
679
+ published = "";
761
680
  await status("complete");
762
681
  } catch (error) {
763
682
  await status("failed", error instanceof Error ? error.message : "Service update failed.");
@@ -779,6 +698,7 @@ async function superviseComputer(directory) {
779
698
  await relayTask;
780
699
  await checkTask;
781
700
  await updateTask;
701
+ await connectionTask;
782
702
  if (relayResult?.handle && relayResult.handle !== relay) await relayResult.handle.close();
783
703
  await relay?.close();
784
704
  if (child) await child.close();
@@ -789,5 +709,5 @@ async function superviseComputer(directory) {
789
709
  }
790
710
 
791
711
  export { recoveryDelay, superviseComputer };
792
- //# sourceMappingURL=controller-AGT6MCJG.js.map
793
- //# sourceMappingURL=controller-AGT6MCJG.js.map
712
+ //# sourceMappingURL=controller-2SJLZMJZ.js.map
713
+ //# sourceMappingURL=controller-2SJLZMJZ.js.map