mindwire 0.1.28 → 0.1.30
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-RN7ERVJ2.js} +39 -12
- package/dist/chunk-RN7ERVJ2.js.map +1 -0
- package/dist/cli.js +87 -89
- package/dist/cli.js.map +1 -1
- package/dist/computer/connection-flow.d.ts +16 -3
- package/dist/computer/controller-handoff.d.ts +6 -0
- package/dist/computer/lifecycle.d.ts +4 -0
- package/dist/computer/relay-health.d.ts +11 -0
- package/dist/computer.d.ts +17 -1
- package/dist/{controller-5ZTNIHUV.js → controller-DIJKMWWU.js} +156 -11
- package/dist/controller-DIJKMWWU.js.map +1 -0
- package/dist/controller-handoff-4GXF4ATR.js +71 -0
- package/dist/controller-handoff-4GXF4ATR.js.map +1 -0
- package/dist/index.cjs +20 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -12
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/package.json +7 -3
- package/dist/chunk-5WYSGXFN.js.map +0 -1
- package/dist/controller-5ZTNIHUV.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-RN7ERVJ2.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";
|
|
@@ -417,23 +417,30 @@ function connectionAction(value) {
|
|
|
417
417
|
if (value === "resume" || value === "reconnect" || value === "pair") return value;
|
|
418
418
|
throw new Error("Use mindwire connect, mindwire connect resume, mindwire reconnect, or mindwire connect pair.");
|
|
419
419
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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;
|
|
420
|
+
function approvalCommand(pairingId, requestId) {
|
|
421
|
+
if (![pairingId, requestId].every((id) => /^[A-Za-z0-9_-]{8,128}$/.test(id))) {
|
|
422
|
+
throw new Error("Invalid phone pairing request. Run mindwire connect to create a new code.");
|
|
427
423
|
}
|
|
428
|
-
|
|
429
|
-
|
|
424
|
+
return `mindwire approve -- ${pairingId} ${requestId}`;
|
|
425
|
+
}
|
|
426
|
+
async function chooseConnectionAction(options) {
|
|
427
|
+
if (options.requested === "pair" || options.requested === "reconnect") return "pair";
|
|
428
|
+
return options.devices.some((device) => !device.revoked && device.connected) ? "resume" : "pair";
|
|
429
|
+
}
|
|
430
|
+
async function choosePairingDecision(options) {
|
|
431
|
+
const replacements = options.replacements.filter((device) => !device.revoked);
|
|
432
|
+
const prompt = replacements.length ? `
|
|
433
|
+
Previous approvals with this name:
|
|
434
|
+
${deviceSummary(replacements, true)}
|
|
435
|
+
|
|
436
|
+
Allow this phone? [r = replace these approvals, a = add a different phone, N = deny]: ` : "Allow this phone to access this computer? [y/N] ";
|
|
430
437
|
for (; ; ) {
|
|
431
|
-
const answer = (await options.question(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
438
|
+
const answer = (await options.question(prompt)).trim().toLowerCase();
|
|
439
|
+
if (["", "n", "no"].includes(answer)) return { approve: false };
|
|
440
|
+
if (replacements.length) {
|
|
441
|
+
if (["r", "replace"].includes(answer)) return { approve: true, replaceDeviceIds: replacements.map((device) => device.id) };
|
|
442
|
+
if (["a", "add"].includes(answer)) return { approve: true };
|
|
443
|
+
} else if (["y", "yes"].includes(answer)) return { approve: true };
|
|
437
444
|
}
|
|
438
445
|
}
|
|
439
446
|
async function chooseStartupAction(options) {
|
|
@@ -450,20 +457,7 @@ async function chooseStartupAction(options) {
|
|
|
450
457
|
if (["n", "no"].includes(answer)) return false;
|
|
451
458
|
}
|
|
452
459
|
}
|
|
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) {
|
|
460
|
+
function deviceSummary(devices, showKeys = false) {
|
|
467
461
|
const saved = devices.filter((device) => !device.revoked);
|
|
468
462
|
const names = /* @__PURE__ */ new Map();
|
|
469
463
|
const safeName2 = (device) => device.name.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
@@ -471,7 +465,7 @@ function deviceSummary(devices) {
|
|
|
471
465
|
return saved.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((device) => {
|
|
472
466
|
const name = safeName2(device);
|
|
473
467
|
const status = device.connected === true ? "connected" : "paired";
|
|
474
|
-
const identity = (names.get(name) ?? 0) > 1 ? ` \xB7 key ${device.id.slice(0, 8)}` : "";
|
|
468
|
+
const identity = showKeys || (names.get(name) ?? 0) > 1 ? ` \xB7 key ${device.id.slice(0, 8)}` : "";
|
|
475
469
|
return ` ${name} \xB7 ${status}${identity}`;
|
|
476
470
|
}).join("\n");
|
|
477
471
|
}
|
|
@@ -480,9 +474,8 @@ function deviceSummary(devices) {
|
|
|
480
474
|
var help = `Mindwire \u2014 connect this computer to your phone
|
|
481
475
|
|
|
482
476
|
npm install -g mindwire
|
|
483
|
-
mindwire connect
|
|
484
|
-
mindwire
|
|
485
|
-
mindwire reconnect Refresh a saved phone's address with a QR code
|
|
477
|
+
mindwire connect Connect your phone; saved keys reconnect automatically
|
|
478
|
+
mindwire reconnect Show a connection QR again (also repairs a missing pairing)
|
|
486
479
|
mindwire connect pair Pair another phone with this computer
|
|
487
480
|
mindwire connect --relay none --host laptop.tailnet Use your existing VPN
|
|
488
481
|
mindwire connect --relay none Direct SSH only (reachable network required)
|
|
@@ -511,6 +504,7 @@ Options:
|
|
|
511
504
|
--no-qr Print only the pairing link
|
|
512
505
|
--startup Enable automatic startup without a prompt
|
|
513
506
|
--no-startup Skip automatic startup setup
|
|
507
|
+
--replace-device DEVICE_ID With approve: replace a lost phone key (repeatable)
|
|
514
508
|
|
|
515
509
|
The default works across Wi-Fi and mobile networks. Mindwire privately downloads
|
|
516
510
|
a verified Cloudflare helper when needed. Relays carry encrypted SSH.
|
|
@@ -540,7 +534,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
540
534
|
version: { type: "string" },
|
|
541
535
|
force: { type: "boolean" },
|
|
542
536
|
startup: { type: "boolean" },
|
|
543
|
-
"no-startup": { type: "boolean" }
|
|
537
|
+
"no-startup": { type: "boolean" },
|
|
538
|
+
"replace-device": { type: "string", multiple: true }
|
|
544
539
|
} });
|
|
545
540
|
const command = positionals[0] ?? "help";
|
|
546
541
|
if (values.help || command === "help") {
|
|
@@ -609,7 +604,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
609
604
|
const devices = await client2.computer.devices();
|
|
610
605
|
const saved = devices.filter((device) => !device.revoked);
|
|
611
606
|
if (saved.length) emit("saved_devices", { devices: saved }, `
|
|
612
|
-
Saved
|
|
607
|
+
Saved approvals on this computer
|
|
613
608
|
${deviceSummary(saved)}`);
|
|
614
609
|
const question = !values.json && process.stdin.isTTY ? async (prompt) => {
|
|
615
610
|
const input = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -619,7 +614,7 @@ ${deviceSummary(saved)}`);
|
|
|
619
614
|
input.close();
|
|
620
615
|
}
|
|
621
616
|
} : void 0;
|
|
622
|
-
const action = await chooseConnectionAction({ requested: requestedAction, devices
|
|
617
|
+
const action = await chooseConnectionAction({ requested: requestedAction, devices });
|
|
623
618
|
const startupPreference = await startupStatus(directory);
|
|
624
619
|
const startupAction = await chooseStartupAction({
|
|
625
620
|
state: startupPreference,
|
|
@@ -638,33 +633,12 @@ ${deviceSummary(saved)}`);
|
|
|
638
633
|
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
634
|
}
|
|
640
635
|
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:
|
|
636
|
+
const connected = saved.filter((device) => device.connected);
|
|
637
|
+
emit("connected", { computerId: info.computerId, devices: connected }, `
|
|
638
|
+
Connected:
|
|
666
639
|
${deviceSummary(connected)}
|
|
667
|
-
Mindwire continues running in the background.`
|
|
640
|
+
Mindwire continues running in the background.`);
|
|
641
|
+
emit("connect_hint", {}, "To scan again or add a phone, run mindwire reconnect.");
|
|
668
642
|
return;
|
|
669
643
|
}
|
|
670
644
|
const invitation = await client2.computer.invite((await client2.computer.info()).routes);
|
|
@@ -675,12 +649,31 @@ Mindwire continues running in the background.` : "Mindwire is running. Open Reco
|
|
|
675
649
|
});
|
|
676
650
|
try {
|
|
677
651
|
let shownRequest;
|
|
652
|
+
let authorized = false;
|
|
678
653
|
while (Date.now() < Date.parse(invitation.expiresAt)) {
|
|
679
654
|
const pairing = await client2.computer.pairing(invitation.pairingId);
|
|
655
|
+
if (pairing.status === "waiting" && requestedAction !== "pair" && requestedAction !== "reconnect") {
|
|
656
|
+
const connected = (await client2.computer.devices()).filter((device) => !device.revoked && device.connected);
|
|
657
|
+
if (connected.length && ((info.pairingVersion ?? 0) < 2 || (await client2.computer.closeUnusedPairing(invitation.pairingId)).cancelled)) {
|
|
658
|
+
display?.close();
|
|
659
|
+
emit("connected", { computerId: info.computerId, devices: connected }, `Connected:
|
|
660
|
+
${deviceSummary(connected)}
|
|
661
|
+
Mindwire continues running in the background.`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
680
665
|
if (pairing.status === "approved") {
|
|
681
666
|
display?.close();
|
|
682
|
-
|
|
683
|
-
|
|
667
|
+
if (pairing.completed) {
|
|
668
|
+
emit("paired", { computerId: info.computerId, completed: true }, "Phone connected and saved. You can close this terminal; Mindwire keeps running.");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (!authorized) {
|
|
672
|
+
authorized = true;
|
|
673
|
+
emit("authorized", {}, "Phone authorized. Waiting for it to save the connection\u2026");
|
|
674
|
+
}
|
|
675
|
+
await setTimeout$1(750);
|
|
676
|
+
continue;
|
|
684
677
|
}
|
|
685
678
|
if (pairing.status === "rejected") {
|
|
686
679
|
display?.close();
|
|
@@ -692,33 +685,33 @@ Mindwire continues running in the background.` : "Mindwire is running. Open Reco
|
|
|
692
685
|
shownRequest = pairing.request.id;
|
|
693
686
|
const key = Buffer.from(pairing.request.publicKey.split(" ")[1] ?? "", "base64");
|
|
694
687
|
const fingerprint = "SHA256:" + createHash("sha256").update(key).digest("base64").replace(/=+$/, "");
|
|
688
|
+
const deviceId = createHash("sha256").update(key).digest("base64url");
|
|
689
|
+
const replacements = (info.pairingVersion ?? 0) >= 2 ? (await client2.computer.devices()).filter((device) => !device.revoked && device.id !== deviceId && device.name === pairing.request.name) : [];
|
|
695
690
|
emit(
|
|
696
691
|
"approval_required",
|
|
697
|
-
{ pairingId: invitation.pairingId, request: pairing.request, fingerprint },
|
|
692
|
+
{ pairingId: invitation.pairingId, request: pairing.request, fingerprint, replacements },
|
|
698
693
|
`
|
|
699
694
|
${safeName(pairing.request.name)} wants to connect.
|
|
700
|
-
Device key: ${fingerprint}
|
|
695
|
+
Device key: ${fingerprint}
|
|
696
|
+
Access: files and commands as your computer account.`
|
|
701
697
|
);
|
|
702
|
-
if (!
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
`Review this device, then run:
|
|
707
|
-
mindwire approve ${invitation.pairingId} ${pairing.request.id}`
|
|
708
|
-
);
|
|
698
|
+
if (!question) {
|
|
699
|
+
const command2 = approvalCommand(invitation.pairingId, pairing.request.id);
|
|
700
|
+
emit("approval_command", { command: command2 }, `Review this device, then run:
|
|
701
|
+
${command2}`);
|
|
709
702
|
} else {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
703
|
+
const decision = await choosePairingDecision({ replacements, question });
|
|
704
|
+
await client2.computer.decide(
|
|
705
|
+
invitation.pairingId,
|
|
706
|
+
pairing.request.id,
|
|
707
|
+
decision.approve,
|
|
708
|
+
decision.replaceDeviceIds ? { replaceDeviceIds: decision.replaceDeviceIds } : {}
|
|
709
|
+
);
|
|
717
710
|
}
|
|
718
711
|
}
|
|
719
712
|
await setTimeout$1(750);
|
|
720
713
|
}
|
|
721
|
-
throw new Error("
|
|
714
|
+
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
715
|
} finally {
|
|
723
716
|
display?.close();
|
|
724
717
|
}
|
|
@@ -762,8 +755,13 @@ ${controller.error}` : ""}`
|
|
|
762
755
|
case "approve":
|
|
763
756
|
case "reject": {
|
|
764
757
|
if (!positionals[1] || !positionals[2]) throw new Error(`Use mindwire ${command} PAIRING_ID REQUEST_ID.`);
|
|
765
|
-
await client.computer.decide(
|
|
766
|
-
|
|
758
|
+
await client.computer.decide(
|
|
759
|
+
positionals[1],
|
|
760
|
+
positionals[2],
|
|
761
|
+
command === "approve",
|
|
762
|
+
values["replace-device"] ? { replaceDeviceIds: values["replace-device"] } : {}
|
|
763
|
+
);
|
|
764
|
+
emit(command, {}, command === "approve" ? "Phone approved. Waiting for it to save the connection." : "Phone declined.");
|
|
767
765
|
break;
|
|
768
766
|
}
|
|
769
767
|
case "update": {
|