mindwire 0.1.33 → 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-H2I3VRBS.js → chunk-RTGZDLSG.js} +529 -177
  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 +5 -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 +22 -6
  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-QBC6L7AT.js → controller-2SJLZMJZ.js} +283 -290
  28. package/dist/controller-2SJLZMJZ.js.map +1 -0
  29. package/dist/{controller-handoff-NFDOLPJL.js → controller-handoff-RUUHODNC.js} +4 -4
  30. package/dist/{controller-handoff-NFDOLPJL.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-H2I3VRBS.js.map +0 -1
  39. package/dist/controller-QBC6L7AT.js.map +0 -1
@@ -1,272 +1,102 @@
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-H2I3VRBS.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';
12
+ import { isIP } from 'net';
15
13
  import WebSocket from 'ws';
16
14
 
17
- var CLOUDFLARED_VERSION = "2026.9.3";
18
- var execute = promisify(execFile);
19
- async function ensureCloudflareBinary(options = {}) {
20
- const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
21
- const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
22
- if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
23
- throw new Error("Install cloudflared for this computer, or select another connection provider.");
24
- }
25
- const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
26
- const bin = path3.join(
27
- options.cacheDir ?? await binaryCacheDirectory(),
28
- "cloudflared",
29
- CLOUDFLARED_VERSION,
30
- `${platform}-${arch}`,
31
- platform === "win32" ? "cloudflared.exe" : "cloudflared"
32
- );
33
- const cached = await cachedExecutable(bin);
34
- if (cached) return cached;
35
- await options.onProgress?.("Downloading the secure connection helper\u2026");
36
- const request2 = options.fetch ?? fetch;
37
- const release = await request2(
38
- `https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
39
- { headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
40
- );
41
- if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
42
- const metadata = await release.json();
43
- const item = metadata.assets?.find((value) => value.name === asset);
44
- const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
45
- const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
46
- if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
47
- const downloaded = await request2(expectedURL, { signal: AbortSignal.timeout(12e4) });
48
- if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
49
- const archive = new Uint8Array(await downloaded.arrayBuffer());
50
- await verifyReleaseBytes(archive, checksum, asset);
51
- let executable = archive;
52
- if (platform === "darwin") {
53
- const temporary = await mkdtemp(path3.join(tmpdir(), "mindwire-cloudflared-"));
54
- try {
55
- const archivePath = path3.join(temporary, asset);
56
- await writeFile(archivePath, archive, { mode: 384 });
57
- const result = await execute(
58
- "/usr/bin/tar",
59
- ["-xzOf", archivePath, "cloudflared"],
60
- { encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
61
- );
62
- executable = new Uint8Array(result.stdout);
63
- } finally {
64
- await rm(temporary, { recursive: true, force: true });
65
- }
66
- }
67
- return cacheExecutable(bin, executable, await executableChecksum(executable));
68
- }
69
- async function cloudflareCommand(options) {
70
- try {
71
- await execute("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
72
- return "cloudflared";
73
- } catch (error) {
74
- if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
75
- return ensureCloudflareBinary(options);
76
- }
77
- }
78
- var RelayOutput = class _RelayOutput {
79
- constructor(file, handle) {
80
- this.file = file;
81
- this.handle = handle;
82
- }
83
- file;
84
- handle;
85
- position = 0;
86
- timer;
87
- pending = Promise.resolve();
88
- closing;
89
- static async create(directory = tmpdir()) {
90
- const file = path3.join(directory, `.relay-output-${randomUUID()}.log`);
91
- return new _RelayOutput(file, await open(file, "ax+", 384));
92
- }
93
- static async adopt(directory, file) {
94
- if (path3.dirname(file) !== path3.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path3.basename(file))) return;
95
- try {
96
- const output = new _RelayOutput(file, await open(file, "r+"));
97
- output.discard();
98
- return output;
99
- } catch (error) {
100
- if (error.code !== "ENOENT") throw error;
101
- }
102
- }
103
- get fd() {
104
- return this.handle.fd;
105
- }
106
- async read() {
107
- const { size } = await this.handle.stat();
108
- if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
109
- const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
110
- const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
111
- this.position += bytesRead;
112
- return data.subarray(0, bytesRead);
113
- }
114
- discard() {
115
- if (this.timer || this.closing) return;
116
- const drain = () => {
117
- this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
118
- });
119
- };
120
- drain();
121
- this.timer = setInterval(drain, 2e3);
122
- this.timer.unref();
123
- }
124
- close() {
125
- return this.closing ??= (async () => {
126
- clearInterval(this.timer);
127
- await this.pending;
128
- await this.handle.close();
129
- await rm(this.file, { force: true });
130
- })();
131
- }
132
- };
133
-
134
- // src/computer/relay.ts
135
- async function adoptRelay(owner, route, directory, outputFile) {
136
- const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
137
- return { route, owner, outputFile: output?.file, async close() {
138
- await owner.close();
139
- await output?.close();
140
- } };
141
- }
142
- function websocketURL(value) {
143
- const url = new URL(value);
144
- if (url.protocol === "https:") url.protocol = "wss:";
145
- if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
146
- throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
147
- }
148
- if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
149
- return url.toString();
150
- }
151
- async function startRelay(options, port, runtime = {}) {
152
- runtime.signal?.throwIfAborted();
153
- if (options.kind === "none") return { close() {
154
- } };
155
- if (options.kind === "custom") {
156
- if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
157
- return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
158
- } };
159
- }
160
- const origin = `http://127.0.0.1:${port}`;
161
- const env = { ...process.env };
162
- let command, args;
163
- if (options.kind === "cloudflare") {
164
- command = await cloudflareCommand(runtime);
165
- await runtime.onProgress?.("Connecting through Cloudflare\u2026");
166
- if (options.cloudflareTokenFile) {
167
- if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
168
- env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
169
- args = ["tunnel", "--no-autoupdate", "run"];
170
- } else {
171
- if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
172
- args = ["tunnel", "--no-autoupdate", "--url", origin];
173
- }
174
- } else {
175
- command = "ngrok";
176
- args = ["http", origin, "--log", "stdout", "--log-format", "json"];
177
- if (options.url) {
178
- const url = new URL(websocketURL(options.url));
179
- args.push("--url", `https://${url.host}`);
180
- }
181
- }
182
- runtime.signal?.throwIfAborted();
183
- const output = await RelayOutput.create(runtime.outputDirectory);
184
- const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
185
- let failure;
186
- child.on("error", (error) => {
187
- failure = error;
188
- });
189
- let owner;
190
- try {
191
- await new Promise((resolve3, reject) => {
192
- child.once("spawn", resolve3);
193
- child.once("error", reject);
194
- });
195
- owner = await ownChild(child);
196
- await runtime.onSpawn?.(owner, output.file);
197
- const deadline = Date.now() + 45e3;
198
- let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
199
- while (Date.now() < deadline) {
200
- runtime.signal?.throwIfAborted();
201
- if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
202
- if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
203
- const received = buffer + (await output.read()).toString();
204
- buffer = received.slice(-16384);
205
- let found;
206
- if (options.kind === "cloudflare") {
207
- cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
208
- cloudflareConnected ||= /Registered tunnel connection/.test(received);
209
- if (cloudflareConnected) found = cloudflareAddress;
210
- } else {
211
- for (const line of received.split(/\r?\n/)) {
212
- try {
213
- const event = JSON.parse(line);
214
- if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
215
- } catch {
216
- }
217
- }
218
- }
219
- if (found) {
220
- const route = { kind: "websocket", url: websocketURL(found) };
221
- output.discard();
222
- const provider = owner;
223
- return {
224
- route,
225
- process: child,
226
- owner,
227
- outputFile: output.file,
228
- async close() {
229
- await provider.close();
230
- await output.close();
231
- }
232
- };
233
- }
234
- await setTimeout$1(50, void 0, { signal: runtime.signal });
235
- }
236
- throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
237
- } catch (error) {
238
- if (owner) await owner.close();
239
- else if (child.pid) await (await ownChild(child)).close();
240
- await output.close();
241
- throw error;
242
- }
243
- }
244
15
  var RelayCheckError = class extends Error {
245
- constructor(code, message, cause) {
16
+ constructor(code, message, cause, addressMissing = false) {
246
17
  super(message, cause ? { cause } : void 0);
247
18
  this.code = code;
19
+ this.addressMissing = addressMissing;
248
20
  this.name = "RelayCheckError";
249
21
  }
250
22
  code;
23
+ addressMissing;
251
24
  };
252
25
  function relayNeedsRestart(error, failures, failedForMs) {
253
- return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "dns");
26
+ return failures >= 2 && failedForMs >= 9e4 && !(error instanceof RelayCheckError && error.code === "provider_certificate") && (!(error instanceof RelayCheckError && error.code === "dns") || error.addressMissing);
27
+ }
28
+ var quickTunnelHost = /^[a-z0-9-]+\.trycloudflare\.com$/i;
29
+ async function resolveCloudflareAddress(hostname, family = 0, options = {}) {
30
+ if (!quickTunnelHost.test(hostname)) return { addresses: [], missing: false };
31
+ const abort = new AbortController();
32
+ const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 3e3);
33
+ try {
34
+ const types = family === 4 ? [1] : family === 6 ? [28] : [1, 28];
35
+ const results = await Promise.all(types.map(async (type) => {
36
+ try {
37
+ const response = await (options.fetch ?? fetch)(
38
+ `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=${type}`,
39
+ { headers: { accept: "application/dns-json" }, redirect: "error", signal: abort.signal }
40
+ );
41
+ if (response.status !== 200 || !response.body) {
42
+ await response.body?.cancel();
43
+ return void 0;
44
+ }
45
+ const reader = response.body.getReader();
46
+ let text = "", size = 0;
47
+ const decoder = new TextDecoder();
48
+ try {
49
+ for (; ; ) {
50
+ const chunk = await reader.read();
51
+ if (chunk.done) break;
52
+ size += chunk.value.length;
53
+ if (size > 16384) return void 0;
54
+ text += decoder.decode(chunk.value, { stream: true });
55
+ }
56
+ } finally {
57
+ await reader.cancel();
58
+ }
59
+ const result = JSON.parse(text + decoder.decode());
60
+ if (!Array.isArray(result.Question) || !result.Question.some((question) => question.name?.replace(/\.$/, "").toLowerCase() === hostname.toLowerCase() && question.type === type)) return void 0;
61
+ if (result.Status === 3) return { addresses: [], missing: true };
62
+ if (result.Status !== 0) return void 0;
63
+ const addresses = (Array.isArray(result.Answer) ? result.Answer : []).flatMap((answer) => {
64
+ const family2 = answer.type === 1 ? 4 : answer.type === 28 ? 6 : 0;
65
+ return family2 && answer.type === type && typeof answer.data === "string" && isIP(answer.data) === family2 ? [{ address: answer.data, family: family2 }] : [];
66
+ });
67
+ return { addresses, missing: false };
68
+ } catch {
69
+ return void 0;
70
+ }
71
+ }));
72
+ return {
73
+ addresses: results.flatMap((result) => result?.addresses ?? []),
74
+ missing: results.every((result) => result?.missing === true)
75
+ };
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
254
79
  }
255
80
  var relayLookup = (hostname, options, callback) => {
256
81
  lookup(hostname, options, (error, address, family) => {
257
- if (error?.code !== "ENOTFOUND" || !/^[a-z0-9-]+\.trycloudflare\.com$/i.test(hostname)) {
82
+ if (!["ENOTFOUND", "EAI_AGAIN"].includes(error?.code ?? "") || !quickTunnelHost.test(hostname)) {
258
83
  callback(error, address, family);
259
84
  return;
260
85
  }
261
86
  const resolver = new Resolver({ timeout: 3e3, tries: 1 });
87
+ const requestedFamily = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : options.family ?? 0;
262
88
  void Promise.all([
263
- options.family === 6 ? [] : resolver.resolve4(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 4 })), () => []),
264
- options.family === 4 ? [] : resolver.resolve6(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 6 })), () => [])
265
- ]).then((results) => {
266
- const addresses = results.flat();
89
+ requestedFamily === 6 ? [] : resolver.resolve4(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 4 })), () => []),
90
+ requestedFamily === 4 ? [] : resolver.resolve6(hostname).then((addresses) => addresses.map((address2) => ({ address: address2, family: 6 })), () => [])
91
+ ]).then(async (results) => {
92
+ let addresses = results.flat();
267
93
  if (!addresses.length) {
268
- callback(error, address, family);
269
- return;
94
+ const provider = await resolveCloudflareAddress(hostname, requestedFamily);
95
+ addresses = provider.addresses;
96
+ if (!addresses.length) {
97
+ callback(Object.assign(error, { relayAddressMissing: provider.missing }), address, family);
98
+ return;
99
+ }
270
100
  }
271
101
  if (options.all) callback(null, addresses);
272
102
  else callback(null, addresses[0].address, addresses[0].family);
@@ -276,7 +106,7 @@ var relayLookup = (hostname, options, callback) => {
276
106
  async function checkRelay(route, options = {}) {
277
107
  options.signal?.throwIfAborted();
278
108
  if (route.kind !== "websocket") throw new Error("Expected an internet tunnel address.");
279
- await new Promise((resolve3, reject) => {
109
+ await new Promise((resolve2, reject) => {
280
110
  const socket = new WebSocket(route.url, { maxPayload: 4096, perMessageDeflate: false, followRedirects: false, lookup: options.lookup ?? relayLookup });
281
111
  let finished = false, banner = "";
282
112
  const finish = (error) => {
@@ -286,14 +116,21 @@ async function checkRelay(route, options = {}) {
286
116
  options.signal?.removeEventListener("abort", aborted);
287
117
  socket.terminate();
288
118
  if (error) reject(error);
289
- else resolve3();
119
+ else resolve2();
290
120
  };
291
121
  const aborted = () => finish(options.signal?.reason ?? new Error("Connection check cancelled."));
292
122
  const timer = setTimeout(() => finish(new RelayCheckError("timeout", "The internet tunnel isn't responding. Retrying\u2026")), options.timeoutMs ?? 8e3);
293
123
  socket.on("error", (error) => {
294
- 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 ?? "")) {
295
127
  const provider = new URL(route.url).hostname.endsWith(".trycloudflare.com") ? "Cloudflare's" : "the tunnel's";
296
- finish(new RelayCheckError("dns", `Waiting for ${provider} internet address (DNS)\u2026`, error));
128
+ finish(new RelayCheckError(
129
+ "dns",
130
+ `Waiting for ${provider} internet address (DNS)\u2026`,
131
+ error,
132
+ error.relayAddressMissing === true
133
+ ));
297
134
  } else if (/(?:[Uu]nexpected server response|[Ee]xpected 101)/.test(error.message)) {
298
135
  finish(new RelayCheckError("proxy", "The internet tunnel isn't reaching Mindwire. Retrying\u2026", error));
299
136
  } else {
@@ -320,17 +157,17 @@ async function checkRelay(route, options = {}) {
320
157
  });
321
158
  }
322
159
  async function relayProviderReachable(options, signal) {
323
- if (options.kind !== "cloudflare" && options.kind !== "ngrok") return false;
160
+ if (!["cloudflare", "ngrok", "oblien"].includes(options.kind)) return false;
324
161
  signal?.throwIfAborted();
325
- return new Promise((resolve3) => {
326
- 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/";
327
164
  let finished = false;
328
165
  const done = (reachable) => {
329
166
  if (finished) return;
330
167
  finished = true;
331
168
  clearTimeout(timer);
332
169
  probe.destroy();
333
- resolve3(reachable);
170
+ resolve2(reachable);
334
171
  };
335
172
  const probe = request(url, { method: "HEAD", signal }, (response) => {
336
173
  response.destroy();
@@ -343,15 +180,26 @@ async function relayProviderReachable(options, signal) {
343
180
  }
344
181
 
345
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
+ }
346
194
  function recoveryDelay(failures) {
347
195
  return Math.min(6e4, 1e3 * 2 ** Math.min(6, Math.max(0, failures - 1)));
348
196
  }
349
197
  async function superviseComputer(directory) {
350
- if ((await readJSON(path3.join(directory, "computer-stopped.json")))?.stopped) return;
351
- 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"));
352
200
  if (!config) throw new Error("Computer configuration is missing.");
353
- const controllerPath = path3.join(directory, "computer-controller.json");
354
- 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");
355
203
  const previous = await readJSON(controllerPath);
356
204
  if (await processStateAlive(previous)) throw new Error("The computer already has a service controller.");
357
205
  const owner = await currentProcessIdentity();
@@ -359,6 +207,7 @@ async function superviseComputer(directory) {
359
207
  await phase("Preparing Mindwire\u2026");
360
208
  let child, relay, stopping = false;
361
209
  let relayTask, updateTask;
210
+ let connectionTask;
362
211
  let relayResult;
363
212
  let checkTask;
364
213
  let checkResult;
@@ -371,30 +220,30 @@ async function superviseComputer(directory) {
371
220
  process.on("SIGINT", stop);
372
221
  let token;
373
222
  try {
374
- token = (await fs.readFile(path3.join(directory, "daemon.token"), "utf8")).trim();
223
+ token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
375
224
  } catch (error) {
376
225
  if (error.code !== "ENOENT") throw error;
377
226
  token = randomBytes(32).toString("hex");
378
- 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" });
379
228
  }
380
229
  const daemonEnv = { ...process.env };
381
230
  if (process.platform === "win32") {
382
231
  try {
383
232
  const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
384
233
  if (git) {
385
- const shellDirectory = path3.resolve(path3.dirname(git), "..", "bin");
386
- 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"));
387
236
  const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
388
- daemonEnv[pathKey] = `${shellDirectory}${path3.delimiter}${daemonEnv[pathKey] ?? ""}`;
237
+ daemonEnv[pathKey] = `${shellDirectory}${path.delimiter}${daemonEnv[pathKey] ?? ""}`;
389
238
  }
390
239
  } catch {
391
240
  }
392
241
  }
393
- const oldRuntime = await readJSON(path3.join(directory, "computer-runtime.json"));
242
+ const oldRuntime = await readJSON(path.join(directory, "computer-runtime.json"));
394
243
  let sshPort = config.sshPort || oldRuntime?.sshPort || 0;
395
244
  let websocketPort = config.websocketPort || oldRuntime?.websocketPort || 0;
396
245
  const launch = async (binary) => {
397
- await fs.rm(path3.join(directory, "computer-runtime.json"), { force: true });
246
+ await fs.rm(path.join(directory, "computer-runtime.json"), { force: true });
398
247
  const process2 = spawn(binary, ["--computer"], {
399
248
  cwd: config.directory,
400
249
  stdio: ["ignore", "inherit", "inherit"],
@@ -403,8 +252,8 @@ async function superviseComputer(directory) {
403
252
  ...daemonEnv,
404
253
  DAEMON_TOKEN: token,
405
254
  ADDR: "127.0.0.1:0",
406
- STATE_PATH: path3.join(directory, "agent-state.json"),
407
- 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"),
408
257
  AGENT_CWD: config.directory,
409
258
  COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
410
259
  COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
@@ -415,8 +264,8 @@ async function superviseComputer(directory) {
415
264
  process2.on("error", (error) => {
416
265
  failure = error;
417
266
  });
418
- await new Promise((resolve3, reject) => {
419
- process2.once("spawn", resolve3);
267
+ await new Promise((resolve2, reject) => {
268
+ process2.once("spawn", resolve2);
420
269
  process2.once("error", reject);
421
270
  });
422
271
  child = await ownChild(process2);
@@ -469,14 +318,26 @@ async function superviseComputer(directory) {
469
318
  client = await launch(binary);
470
319
  }
471
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 });
472
328
  if (saved) {
473
329
  const owner2 = await adoptProcess(saved.owner);
474
330
  if (owner2) {
475
331
  if (saved.outputFile && saved.route && saved.port === websocketPort && saved.options === JSON.stringify(config.relay)) {
476
- relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
332
+ relay = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
333
+ relayWasReachable = !!previous?.routes?.some((route) => JSON.stringify(route) === JSON.stringify(saved.route));
334
+ if (previous?.relayFailureSince && relayWasReachable) {
335
+ relayFailedAt = Math.min(Date.now(), previous.relayFailureSince);
336
+ relayHealthFailures = previous.relayFailures ?? 0;
337
+ }
477
338
  } else {
478
339
  if (!saved.outputFile) await phase("Refreshing the internet tunnel for this update\u2026");
479
- const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile);
340
+ const abandoned = await adoptRelay(owner2, saved.route, directory, saved.outputFile, saved.configFile);
480
341
  await abandoned.close();
481
342
  await fs.rm(relayPath, { force: true });
482
343
  }
@@ -486,6 +347,128 @@ async function superviseComputer(directory) {
486
347
  }
487
348
  let lastController = "";
488
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
+ }
489
472
  if (!changingDaemon && child && !child.alive()) {
490
473
  child = void 0;
491
474
  client = void 0;
@@ -510,7 +493,8 @@ async function superviseComputer(directory) {
510
493
  nextDaemonAt = Date.now() + recoveryDelay(++daemonFailures);
511
494
  }
512
495
  }
513
- 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()) {
514
498
  await relay.close();
515
499
  relay = void 0;
516
500
  relayReachable = false;
@@ -538,30 +522,35 @@ async function superviseComputer(directory) {
538
522
  port: websocketPort,
539
523
  options: JSON.stringify(config.relay),
540
524
  route: relay.route,
541
- outputFile: relay.outputFile
525
+ outputFile: relay.outputFile,
526
+ configFile: relay.configFile
542
527
  });
543
528
  } else {
529
+ checkedAt = Date.now();
544
530
  relayError = result.error instanceof Error ? result.error.message : "The internet tunnel couldn't connect.";
545
- 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));
546
533
  }
547
534
  }
548
- if (!relay && !relayTask && Date.now() >= nextRelayAt) {
535
+ if (!relay && !relayTask && !connectionTask && (Date.now() >= nextRelayAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
549
536
  relayError = void 0;
550
537
  relayErrorCode = void 0;
551
538
  relayMessage = "Reconnecting your internet tunnel\u2026";
552
539
  relayTask = startRelay(config.relay, websocketPort, {
553
- cacheDir: path3.join(directory, "tools"),
540
+ cacheDir: path.join(directory, "tools"),
554
541
  outputDirectory: directory,
542
+ cliPath: process.argv[1],
555
543
  signal: abort.signal,
556
544
  onProgress: (message) => {
557
545
  relayMessage = message;
558
546
  },
559
- onSpawn: async (owner2, outputFile) => {
547
+ onSpawn: async (owner2, outputFile, configFile) => {
560
548
  if (owner2.identity) await writeJSON(relayPath, {
561
549
  owner: owner2.identity,
562
550
  port: websocketPort,
563
551
  options: JSON.stringify(config.relay),
564
- outputFile
552
+ outputFile,
553
+ configFile
565
554
  });
566
555
  }
567
556
  }).then((handle) => {
@@ -590,7 +579,8 @@ async function superviseComputer(directory) {
590
579
  relayFailedAt ||= checkedAt;
591
580
  relayHealthFailures++;
592
581
  nextCheckAt = checkedAt + Math.min(15e3, recoveryDelay(relayHealthFailures));
593
- if (result.canRestart) {
582
+ if (result.canRestart && !connectionTask) {
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");
594
584
  await relay.close();
595
585
  relay = void 0;
596
586
  relayWasReachable = false;
@@ -600,7 +590,6 @@ async function superviseComputer(directory) {
600
590
  }
601
591
  }
602
592
  }
603
- const requestedCheck = await readJSON(path3.join(directory, "computer-check.json"));
604
593
  if (config.relay.kind === "none") checkedAt = Math.max(checkedAt, requestedCheck?.requestedAt ?? 0);
605
594
  if (relay?.route && !checkTask && (Date.now() >= nextCheckAt || (requestedCheck?.requestedAt ?? 0) > checkedAt)) {
606
595
  const checking = relay;
@@ -609,17 +598,18 @@ async function superviseComputer(directory) {
609
598
  await checkRelay(checking.route, { signal: abort.signal });
610
599
  checkResult = { relay: checking };
611
600
  } catch (error) {
612
- const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && await relayProviderReachable(config.relay, abort.signal).catch(() => false);
601
+ const canRestart = !!checking.owner && relayFailedAt > 0 && relayNeedsRestart(error, relayHealthFailures, Date.now() - relayFailedAt) && (error instanceof RelayCheckError && error.addressMissing || await relayProviderReachable(config.relay, abort.signal).catch(() => false));
613
602
  checkResult = { relay: checking, error, canRestart };
614
603
  }
615
604
  })();
616
605
  }
617
606
  routes = directRoutes(config, sshPort);
618
607
  if (relay?.route && relayWasReachable) routes.push(relay.route);
619
- const routeJSON = JSON.stringify(routes);
608
+ const metadata = connectionInfo(config.relay, config.host);
609
+ const routeJSON = JSON.stringify({ routes, connection: metadata });
620
610
  if (!changingDaemon && client && child?.alive() && routeJSON !== published) {
621
611
  try {
622
- await client.computer.setRoutes(routes);
612
+ await client.computer.setRoutes(routes, metadata);
623
613
  published = routeJSON;
624
614
  } catch {
625
615
  }
@@ -635,15 +625,17 @@ async function superviseComputer(directory) {
635
625
  phase: ready ? void 0 : daemonError ?? relayError ?? relayMessage,
636
626
  error: daemonError ?? relayError,
637
627
  errorCode: daemonError ? void 0 : relayErrorCode,
638
- recovering: !ready
628
+ recovering: !ready,
629
+ relayFailureSince: relayFailedAt || void 0,
630
+ relayFailures: relayHealthFailures || void 0
639
631
  };
640
632
  const serialized = JSON.stringify(state);
641
633
  if (serialized !== lastController) {
642
634
  await writeJSON(controllerPath, state);
643
635
  lastController = serialized;
644
636
  }
645
- const updatePath = path3.join(directory, "computer-update.json");
646
- 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;
647
639
  if (update && ["queued", "downloading", "waiting", "restarting"].includes(update.status)) {
648
640
  updateTask = (async () => {
649
641
  const status = async (state2, error) => writeJSON(
@@ -682,9 +674,9 @@ async function superviseComputer(directory) {
682
674
  }
683
675
  binary = replacement;
684
676
  config = { ...config, daemonBin: void 0, version: update.version };
685
- await writeJSON(path3.join(directory, "computer-config.json"), config);
686
- await client.computer.setRoutes(routes);
687
- 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 = "";
688
680
  await status("complete");
689
681
  } catch (error) {
690
682
  await status("failed", error instanceof Error ? error.message : "Service update failed.");
@@ -706,6 +698,7 @@ async function superviseComputer(directory) {
706
698
  await relayTask;
707
699
  await checkTask;
708
700
  await updateTask;
701
+ await connectionTask;
709
702
  if (relayResult?.handle && relayResult.handle !== relay) await relayResult.handle.close();
710
703
  await relay?.close();
711
704
  if (child) await child.close();
@@ -716,5 +709,5 @@ async function superviseComputer(directory) {
716
709
  }
717
710
 
718
711
  export { recoveryDelay, superviseComputer };
719
- //# sourceMappingURL=controller-QBC6L7AT.js.map
720
- //# sourceMappingURL=controller-QBC6L7AT.js.map
712
+ //# sourceMappingURL=controller-2SJLZMJZ.js.map
713
+ //# sourceMappingURL=controller-2SJLZMJZ.js.map