mindwire 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { defaultStateDirectory, superviseComputer, ensureComputer, computerPairingURI, stopComputer, computerClient, SDK_VERSION, readJSON, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, computerPairingQRData } from './chunk-5WYSGXFN.js';
2
+ import { defaultStateDirectory, superviseComputer, ensureComputer, computerPairingURI, stopComputer, computerClient, SDK_VERSION, readJSON, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, computerPairingQRData } from './chunk-QPZMGVY7.js';
3
3
  import './chunk-T6TD76UA.js';
4
4
  import { promisify, parseArgs } from 'util';
5
5
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -7,10 +7,10 @@ import { realpathSync, mkdtempSync, chmodSync, writeFileSync, rmSync } from 'fs'
7
7
  import * as path from 'path';
8
8
  import { join } from 'path';
9
9
  import { createHash } from 'crypto';
10
- import { hostname, homedir, tmpdir } from 'os';
11
10
  import { createInterface } from 'readline/promises';
12
11
  import { setTimeout as setTimeout$1 } from 'timers/promises';
13
12
  import { execFile, spawn } from 'child_process';
13
+ import { homedir, tmpdir } from 'os';
14
14
  import * as fs from 'fs/promises';
15
15
 
16
16
  var enterScreen = "\x1B[?1049h\x1B[?25l";
@@ -63,14 +63,14 @@ textarea{width:100%;margin-top:12px;padding:8px;color:inherit;background:#252528
63
63
  <div class="code" id="code" role="img" aria-label="Mindwire connection QR code">${svg}</div>
64
64
  <p id="expiry" data-expires="${escapeHTML(invitation.expiresAt)}" aria-live="off"></p>
65
65
  <div class="steps"><p>${reconnect ? "In Mindwire, open your computer in <strong>Workspaces</strong>, then <strong>Reconnect Computer \u2192 Scan Connection Code</strong>." : "In Mindwire, open <strong>Add computer</strong> and scan."}</p>
66
- <p>${reconnect ? "Your saved pairing and workspace stay the same. No new approval is needed." : "Then return to the terminal to approve your phone."}</p></div>
66
+ <p>${reconnect ? "Your saved pairing and workspace stay the same. No new approval is needed." : "Saved keys reconnect automatically. Approve on this computer only if asked."}</p></div>
67
67
  <button type="button" id="copy">Copy ${reconnect ? "reconnect" : "pairing"} link</button>
68
68
  <textarea id="link" readonly hidden aria-label="Pairing link">${escapeHTML(computerPairingURI(invitation))}</textarea>
69
69
  <footer>You can close this page after ${reconnect ? "reconnecting" : "pairing"}.</footer>
70
70
  </main><script>
71
71
  const expiry=document.getElementById('expiry'),code=document.getElementById('code'),copy=document.getElementById('copy'),link=document.getElementById('link');
72
72
  function tick(){const seconds=Math.max(0,Math.ceil((Date.parse(expiry.dataset.expires)-Date.now())/1000));
73
- if(!seconds){code.remove();copy.disabled=true;link.value='';link.hidden=true;expiry.textContent='Code expired. Run mindwire ${reconnect ? "reconnect" : "connect pair"} for a new code.';return;}
73
+ if(!seconds){code.remove();copy.disabled=true;link.value='';link.hidden=true;expiry.textContent='Code expired. Run mindwire connect for a new code.';return;}
74
74
  expiry.textContent='Expires in '+Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0');}
75
75
  tick();setInterval(tick,1000);
76
76
  copy.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(link.value);copy.textContent='Copied';}
@@ -96,9 +96,9 @@ async function openBrowser(url) {
96
96
  async function showPairingInvitation(invitation, options = {}) {
97
97
  const reconnect = "mode" in invitation && invitation.mode === "reconnect";
98
98
  const instruction = scanInstruction(invitation);
99
- const nextStep = reconnect ? "Scan with a saved phone, then press Enter here." : "Return here to approve your phone.";
99
+ const nextStep = reconnect ? "Scan with a saved phone." : "Approve on this computer only if asked.";
100
100
  const waiting = reconnect ? "Your saved pairing stays the same." : "Waiting for your phone\u2026";
101
- const command = reconnect ? "mindwire reconnect" : "mindwire connect pair";
101
+ const command = "mindwire connect";
102
102
  const output = options.output ?? process.stdout;
103
103
  const environment = options.environment ?? process.env;
104
104
  const mode = options.mode ?? "auto";
@@ -418,22 +418,23 @@ function connectionAction(value) {
418
418
  throw new Error("Use mindwire connect, mindwire connect resume, mindwire reconnect, or mindwire connect pair.");
419
419
  }
420
420
  async function chooseConnectionAction(options) {
421
- const paired = options.devices.filter((device) => !device.revoked);
422
- if (options.requested) {
423
- if (options.requested !== "pair" && paired.length === 0) {
424
- throw new Error("No saved phones yet. Run mindwire connect to pair one first.");
425
- }
426
- return options.requested;
427
- }
428
- if (paired.length === 0) return "pair";
429
- if (!options.question) return "resume";
421
+ if (options.requested === "pair" || options.requested === "reconnect") return "pair";
422
+ return options.devices.some((device) => !device.revoked && device.connected) ? "resume" : "pair";
423
+ }
424
+ async function choosePairingDecision(options) {
425
+ const replacements = options.replacements.filter((device) => !device.revoked);
426
+ const prompt = replacements.length ? `
427
+ Previous approvals with this name:
428
+ ${deviceSummary(replacements, true)}
429
+
430
+ Allow this phone? [r = replace these approvals, a = add a different phone, N = deny]: ` : "Allow this phone to access this workspace? [y/N] ";
430
431
  for (; ; ) {
431
- const answer = (await options.question(
432
- "\n 1 Resume saved connection\n 2 Refresh address on a saved phone (QR)\n 3 Pair another phone\n\nChoose [1]: "
433
- )).trim().toLowerCase();
434
- if (answer === "" || answer === "1" || answer === "resume") return "resume";
435
- if (answer === "2" || answer === "reconnect") return "reconnect";
436
- if (answer === "3" || answer === "pair") return "pair";
432
+ const answer = (await options.question(prompt)).trim().toLowerCase();
433
+ if (["", "n", "no"].includes(answer)) return { approve: false };
434
+ if (replacements.length) {
435
+ if (["r", "replace"].includes(answer)) return { approve: true, replaceDeviceIds: replacements.map((device) => device.id) };
436
+ if (["a", "add"].includes(answer)) return { approve: true };
437
+ } else if (["y", "yes"].includes(answer)) return { approve: true };
437
438
  }
438
439
  }
439
440
  async function chooseStartupAction(options) {
@@ -450,20 +451,7 @@ async function chooseStartupAction(options) {
450
451
  if (["n", "no"].includes(answer)) return false;
451
452
  }
452
453
  }
453
- function reconnectCode(info, name, now = /* @__PURE__ */ new Date()) {
454
- if (info.routes.length === 0) throw new Error("No connection address is ready. Run mindwire status and retry once the tunnel is online.");
455
- return {
456
- version: 1,
457
- mode: "reconnect",
458
- computerId: info.computerId,
459
- registryId: info.registryId,
460
- fingerprint: info.fingerprint,
461
- name,
462
- routes: info.routes,
463
- expiresAt: new Date(now.getTime() + 5 * 6e4).toISOString()
464
- };
465
- }
466
- function deviceSummary(devices) {
454
+ function deviceSummary(devices, showKeys = false) {
467
455
  const saved = devices.filter((device) => !device.revoked);
468
456
  const names = /* @__PURE__ */ new Map();
469
457
  const safeName2 = (device) => device.name.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
@@ -471,7 +459,7 @@ function deviceSummary(devices) {
471
459
  return saved.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((device) => {
472
460
  const name = safeName2(device);
473
461
  const status = device.connected === true ? "connected" : "paired";
474
- const identity = (names.get(name) ?? 0) > 1 ? ` \xB7 key ${device.id.slice(0, 8)}` : "";
462
+ const identity = showKeys || (names.get(name) ?? 0) > 1 ? ` \xB7 key ${device.id.slice(0, 8)}` : "";
475
463
  return ` ${name} \xB7 ${status}${identity}`;
476
464
  }).join("\n");
477
465
  }
@@ -480,9 +468,8 @@ function deviceSummary(devices) {
480
468
  var help = `Mindwire \u2014 connect this computer to your phone
481
469
 
482
470
  npm install -g mindwire
483
- mindwire connect Resume a saved connection, or pair your first phone
484
- mindwire connect resume Resume without creating another pairing
485
- mindwire reconnect Refresh a saved phone's address with a QR code
471
+ mindwire connect Connect your phone; saved keys reconnect automatically
472
+ mindwire reconnect Show a connection QR again (also repairs a missing pairing)
486
473
  mindwire connect pair Pair another phone with this computer
487
474
  mindwire connect --relay none --host laptop.tailnet Use your existing VPN
488
475
  mindwire connect --relay none Direct SSH only (reachable network required)
@@ -511,6 +498,7 @@ Options:
511
498
  --no-qr Print only the pairing link
512
499
  --startup Enable automatic startup without a prompt
513
500
  --no-startup Skip automatic startup setup
501
+ --replace-device DEVICE_ID With approve: replace a lost phone key (repeatable)
514
502
 
515
503
  The default works across Wi-Fi and mobile networks. Mindwire privately downloads
516
504
  a verified Cloudflare helper when needed. Relays carry encrypted SSH.
@@ -540,7 +528,8 @@ async function main(args = process.argv.slice(2)) {
540
528
  version: { type: "string" },
541
529
  force: { type: "boolean" },
542
530
  startup: { type: "boolean" },
543
- "no-startup": { type: "boolean" }
531
+ "no-startup": { type: "boolean" },
532
+ "replace-device": { type: "string", multiple: true }
544
533
  } });
545
534
  const command = positionals[0] ?? "help";
546
535
  if (values.help || command === "help") {
@@ -609,7 +598,7 @@ async function main(args = process.argv.slice(2)) {
609
598
  const devices = await client2.computer.devices();
610
599
  const saved = devices.filter((device) => !device.revoked);
611
600
  if (saved.length) emit("saved_devices", { devices: saved }, `
612
- Saved phones
601
+ Saved approvals on this computer
613
602
  ${deviceSummary(saved)}`);
614
603
  const question = !values.json && process.stdin.isTTY ? async (prompt) => {
615
604
  const input = createInterface({ input: process.stdin, output: process.stderr });
@@ -619,7 +608,7 @@ ${deviceSummary(saved)}`);
619
608
  input.close();
620
609
  }
621
610
  } : void 0;
622
- const action = await chooseConnectionAction({ requested: requestedAction, devices, question });
611
+ const action = await chooseConnectionAction({ requested: requestedAction, devices });
623
612
  const startupPreference = await startupStatus(directory);
624
613
  const startupAction = await chooseStartupAction({
625
614
  state: startupPreference,
@@ -638,33 +627,12 @@ ${deviceSummary(saved)}`);
638
627
  emit("startup", startupPreference, startupPreference.enabled ? "Automatic startup is on. Change it with mindwire startup disable." : "Automatic startup is off. Enable it with mindwire startup enable.");
639
628
  }
640
629
  if (action === "resume") {
641
- emit("resumed", { computerId: info.computerId, devices: saved }, "\nReady for your saved phone. Open this computer in Mindwire.");
642
- if (info.routes.some((route) => route.kind === "websocket" && new URL(route.url).hostname.endsWith(".trycloudflare.com"))) {
643
- emit("reconnect_hint", {}, "If its temporary tunnel address changed, run mindwire reconnect to refresh your phone.");
644
- }
645
- return;
646
- }
647
- if (action === "reconnect") {
648
- const code = reconnectCode(await client2.computer.info(), hostname());
649
- emit("reconnect_code", { code, uri: computerPairingURI(code) });
650
- if (values.json) return;
651
- emit("reconnect_instructions", {}, "\nOn your iPhone: Workspaces \u2192 your computer \u2192 Reconnect Computer \u2192 Scan Connection Code.\nThis refreshes the saved connection; it does not pair another phone.");
652
- const display2 = await showPairingInvitation(code, {
653
- mode: values["no-qr"] ? "none" : values.qr ?? "auto"
654
- });
655
- try {
656
- if (question) await question("\nScan with your saved phone, then press Enter to close this code. ");
657
- else if (values.qr === "browser") {
658
- emit("waiting", {}, "This code stays open until it expires. Press Ctrl+C to close it; Mindwire keeps running.");
659
- await setTimeout$1(Math.max(0, Date.parse(code.expiresAt) - Date.now()));
660
- }
661
- } finally {
662
- display2.close();
663
- }
664
- const connected = (await client2.computer.devices()).filter((device) => !device.revoked && device.connected);
665
- emit(connected.length ? "connected" : "waiting_for_phone", { computerId: info.computerId, devices: connected }, connected.length ? `Connected:
630
+ const connected = saved.filter((device) => device.connected);
631
+ emit("connected", { computerId: info.computerId, devices: connected }, `
632
+ Connected:
666
633
  ${deviceSummary(connected)}
667
- Mindwire continues running in the background.` : "Mindwire is running. Open Reconnect Computer on your iPhone and scan the code to refresh its address.");
634
+ Mindwire continues running in the background.`);
635
+ emit("connect_hint", {}, "To scan again or add a phone, run mindwire reconnect.");
668
636
  return;
669
637
  }
670
638
  const invitation = await client2.computer.invite((await client2.computer.info()).routes);
@@ -675,12 +643,31 @@ Mindwire continues running in the background.` : "Mindwire is running. Open Reco
675
643
  });
676
644
  try {
677
645
  let shownRequest;
646
+ let authorized = false;
678
647
  while (Date.now() < Date.parse(invitation.expiresAt)) {
679
648
  const pairing = await client2.computer.pairing(invitation.pairingId);
649
+ if (pairing.status === "waiting" && requestedAction !== "pair" && requestedAction !== "reconnect") {
650
+ const connected = (await client2.computer.devices()).filter((device) => !device.revoked && device.connected);
651
+ if (connected.length && ((info.pairingVersion ?? 0) < 2 || (await client2.computer.closeUnusedPairing(invitation.pairingId)).cancelled)) {
652
+ display?.close();
653
+ emit("connected", { computerId: info.computerId, devices: connected }, `Connected:
654
+ ${deviceSummary(connected)}
655
+ Mindwire continues running in the background.`);
656
+ return;
657
+ }
658
+ }
680
659
  if (pairing.status === "approved") {
681
660
  display?.close();
682
- emit("paired", {}, "Phone connected. You can close this terminal; Mindwire keeps running.");
683
- return;
661
+ if (pairing.completed) {
662
+ emit("paired", { computerId: info.computerId, completed: true }, "Phone connected and saved. You can close this terminal; Mindwire keeps running.");
663
+ return;
664
+ }
665
+ if (!authorized) {
666
+ authorized = true;
667
+ emit("authorized", {}, "Phone authorized. Waiting for it to save the connection\u2026");
668
+ }
669
+ await setTimeout$1(750);
670
+ continue;
684
671
  }
685
672
  if (pairing.status === "rejected") {
686
673
  display?.close();
@@ -692,14 +679,16 @@ Mindwire continues running in the background.` : "Mindwire is running. Open Reco
692
679
  shownRequest = pairing.request.id;
693
680
  const key = Buffer.from(pairing.request.publicKey.split(" ")[1] ?? "", "base64");
694
681
  const fingerprint = "SHA256:" + createHash("sha256").update(key).digest("base64").replace(/=+$/, "");
682
+ const deviceId = createHash("sha256").update(key).digest("base64url");
683
+ const replacements = (info.pairingVersion ?? 0) >= 2 ? (await client2.computer.devices()).filter((device) => !device.revoked && device.id !== deviceId && device.name === pairing.request.name) : [];
695
684
  emit(
696
685
  "approval_required",
697
- { pairingId: invitation.pairingId, request: pairing.request, fingerprint },
686
+ { pairingId: invitation.pairingId, request: pairing.request, fingerprint, replacements },
698
687
  `
699
688
  ${safeName(pairing.request.name)} wants to connect.
700
689
  Device key: ${fingerprint}`
701
690
  );
702
- if (!process.stdin.isTTY) {
691
+ if (!question) {
703
692
  emit(
704
693
  "approval_command",
705
694
  { command: `mindwire approve ${invitation.pairingId} ${pairing.request.id}` },
@@ -707,18 +696,18 @@ Device key: ${fingerprint}`
707
696
  mindwire approve ${invitation.pairingId} ${pairing.request.id}`
708
697
  );
709
698
  } else {
710
- const input = createInterface({ input: process.stdin, output: process.stderr });
711
- try {
712
- const answer = await input.question("Allow this phone to access this workspace? [y/N] ");
713
- await client2.computer.decide(invitation.pairingId, pairing.request.id, /^y(es)?$/i.test(answer.trim()));
714
- } finally {
715
- input.close();
716
- }
699
+ const decision = await choosePairingDecision({ replacements, question });
700
+ await client2.computer.decide(
701
+ invitation.pairingId,
702
+ pairing.request.id,
703
+ decision.approve,
704
+ decision.replaceDeviceIds ? { replaceDeviceIds: decision.replaceDeviceIds } : {}
705
+ );
717
706
  }
718
707
  }
719
708
  await setTimeout$1(750);
720
709
  }
721
- throw new Error("Pairing expired. Run mindwire connect to show a new QR code.");
710
+ throw new Error(authorized ? "The phone was approved but did not finish saving. Run mindwire connect and scan again; its saved key will be reused." : "Connection code expired. Run mindwire connect to show a new QR code.");
722
711
  } finally {
723
712
  display?.close();
724
713
  }
@@ -762,8 +751,13 @@ ${controller.error}` : ""}`
762
751
  case "approve":
763
752
  case "reject": {
764
753
  if (!positionals[1] || !positionals[2]) throw new Error(`Use mindwire ${command} PAIRING_ID REQUEST_ID.`);
765
- await client.computer.decide(positionals[1], positionals[2], command === "approve");
766
- emit(command, {}, command === "approve" ? "Phone approved." : "Phone declined.");
754
+ await client.computer.decide(
755
+ positionals[1],
756
+ positionals[2],
757
+ command === "approve",
758
+ values["replace-device"] ? { replaceDeviceIds: values["replace-device"] } : {}
759
+ );
760
+ emit(command, {}, command === "approve" ? "Phone approved. Waiting for it to save the connection." : "Phone declined.");
767
761
  break;
768
762
  }
769
763
  case "update": {