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/{chunk-5WYSGXFN.js → chunk-QPZMGVY7.js} +14 -6
- package/dist/chunk-QPZMGVY7.js.map +1 -0
- package/dist/cli.js +76 -82
- package/dist/cli.js.map +1 -1
- package/dist/computer/connection-flow.d.ts +13 -3
- package/dist/computer.d.ts +14 -1
- package/dist/{controller-5ZTNIHUV.js → controller-MNMIJHH5.js} +3 -3
- package/dist/{controller-5ZTNIHUV.js.map → controller-MNMIJHH5.js.map} +1 -1
- package/dist/index.cjs +11 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5WYSGXFN.js.map +0 -1
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-
|
|
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." : "
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
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
|
|
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
|
|
484
|
-
mindwire
|
|
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
|
|
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
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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.`
|
|
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
|
-
|
|
683
|
-
|
|
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 (!
|
|
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
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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("
|
|
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(
|
|
766
|
-
|
|
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": {
|