cmux-ssh-here 0.6.0 → 0.7.0
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/README.md +4 -1
- package/bin.js +38 -7
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ That's it. No `sshd` to configure, no `~/.ssh/authorized_keys` to edit, no firew
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
The terminal turns into a live dashboard — the link, a countdown bar for its lifetime, and who's connected:
|
|
19
|
+
The terminal turns into a live dashboard — the link, a scannable QR code, a countdown bar for its lifetime, and who's connected:
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
@@ -27,8 +27,10 @@ Open the link and cmux connects on its own — one click, straight into the shel
|
|
|
27
27
|
## Why you'll like it
|
|
28
28
|
|
|
29
29
|
- ⚡ **Zero setup** — one `npx` command, no SSH server administration.
|
|
30
|
+
- 📱 **Scan to connect** — a QR code in the terminal opens the link on a phone or tablet.
|
|
30
31
|
- 🔑 **No credentials to share** — auth is a one-time token baked into the link.
|
|
31
32
|
- ⏳ **Self-expiring** — the link rotates every 3 minutes; leaked links go stale on their own. Live sessions stay connected.
|
|
33
|
+
- 🎯 **One-time mode** — `--once` locks the link to the first device that connects and rejects everyone else.
|
|
32
34
|
- 👀 **Live dashboard** — see the current link, a countdown bar, and every connected client at a glance.
|
|
33
35
|
- 🧩 **Real SSH** — full PTY shell, `scp`/`sftp`, and the exec channel cmux needs to bootstrap remote workspaces.
|
|
34
36
|
- 🪟 **Persistent sessions** — when `tmux` is present, sessions survive disconnects and are shared across connections.
|
|
@@ -66,6 +68,7 @@ The device you open the link from just needs [cmux](https://cmux.com) installed.
|
|
|
66
68
|
|
|
67
69
|
## Options
|
|
68
70
|
|
|
71
|
+
- `npx cmux-ssh-here --once` — single-use link: locks to the first device that connects, rejects any other.
|
|
69
72
|
- `PORT=2222 npx cmux-ssh-here` — fixed port (random free port by default).
|
|
70
73
|
- `CMUX_SSH_TTL=600 npx cmux-ssh-here` — link/token lifetime in seconds before regeneration (default 180).
|
|
71
74
|
- `CMUX_SSH_DEBUG=1 npx cmux-ssh-here` — log incoming auth/env/exec/shell requests to stderr (disables the live dashboard).
|
package/bin.js
CHANGED
|
@@ -18,6 +18,7 @@ import { spawn } from "node:child_process";
|
|
|
18
18
|
import { chmodSync } from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
21
22
|
|
|
22
23
|
// ponytail: the host side needs a POSIX shell + scp/exec semantics, and cmux's
|
|
23
24
|
// own remote daemon only ships linux/darwin/freebsd builds — so a Windows host
|
|
@@ -48,6 +49,13 @@ const regenerateToken = () => {
|
|
|
48
49
|
};
|
|
49
50
|
regenerateToken();
|
|
50
51
|
|
|
52
|
+
// --once: single-use link. After the first client connects, lock to its IP and
|
|
53
|
+
// stop rotating — new machines are rejected, but that client's several
|
|
54
|
+
// connections (cmux opens ControlMaster + daemon + probes) keep working.
|
|
55
|
+
const ONCE = process.argv.includes("--once");
|
|
56
|
+
let consumed = false;
|
|
57
|
+
let lockedIp = null;
|
|
58
|
+
|
|
51
59
|
// Active authenticated connections, shown live in the server terminal.
|
|
52
60
|
const sessions = new Map();
|
|
53
61
|
let nextSid = 0;
|
|
@@ -258,13 +266,19 @@ let render = () => {}; // assigned once the server is listening (knows ip/port)
|
|
|
258
266
|
const server = new Server(serverCfg, (client, info) => {
|
|
259
267
|
client.on("authentication", (ctx) => {
|
|
260
268
|
debug("auth", ctx.method, ctx.username);
|
|
261
|
-
|
|
269
|
+
// Correct token, plus (in --once after consumption) only the locked-in IP.
|
|
270
|
+
const ok = ctx.username === token && (!consumed || info?.ip === lockedIp);
|
|
271
|
+
return ok ? ctx.accept() : ctx.reject();
|
|
262
272
|
});
|
|
263
273
|
client.on("ready", () => {
|
|
264
274
|
// ponytail: just track the connection; the 5s timer redraws — no per-event
|
|
265
275
|
// repaint (that was the screen-churn that broke copying).
|
|
266
276
|
const id = nextSid++;
|
|
267
277
|
sessions.set(id, { ip: info?.ip || "?", since: Date.now() });
|
|
278
|
+
if (ONCE && !consumed) {
|
|
279
|
+
consumed = true;
|
|
280
|
+
lockedIp = info?.ip || "?";
|
|
281
|
+
}
|
|
268
282
|
client.on("close", () => sessions.delete(id));
|
|
269
283
|
});
|
|
270
284
|
client.on("session", (accept) => {
|
|
@@ -343,6 +357,16 @@ server.listen(PORT, "0.0.0.0", function () {
|
|
|
343
357
|
return `[${"█".repeat(filled)}${"░".repeat(W - filled)}]`;
|
|
344
358
|
};
|
|
345
359
|
|
|
360
|
+
// ASCII QR of the current link — scan with a phone/tablet to open in cmux.
|
|
361
|
+
const qrFor = (text) => {
|
|
362
|
+
let out = "";
|
|
363
|
+
qrcodeTerminal.generate(text, { small: true }, (q) => (out = q));
|
|
364
|
+
return out
|
|
365
|
+
.split("\n")
|
|
366
|
+
.map((l) => " " + l)
|
|
367
|
+
.join("\n");
|
|
368
|
+
};
|
|
369
|
+
|
|
346
370
|
// Collapse cmux's several SSH connections from one machine into one row per IP.
|
|
347
371
|
const sessionRows = () => {
|
|
348
372
|
const byIp = new Map();
|
|
@@ -356,19 +380,25 @@ server.listen(PORT, "0.0.0.0", function () {
|
|
|
356
380
|
);
|
|
357
381
|
};
|
|
358
382
|
|
|
383
|
+
const mode = ONCE ? " (one-time link)" : "";
|
|
384
|
+
|
|
359
385
|
render = () => {
|
|
360
386
|
if (!liveUI) return;
|
|
361
387
|
const rem = remainingSec();
|
|
388
|
+
const link = buildLink();
|
|
362
389
|
const lines = [
|
|
363
390
|
"",
|
|
364
|
-
` cmux-ssh-here — shell as ${user} over the LAN`,
|
|
391
|
+
` cmux-ssh-here — shell as ${user} over the LAN${mode}`,
|
|
365
392
|
"",
|
|
366
|
-
" Open in cmux:",
|
|
367
|
-
` ${
|
|
393
|
+
" Open in cmux (or scan to open on a phone/tablet):",
|
|
394
|
+
` ${link}`,
|
|
368
395
|
"",
|
|
369
|
-
|
|
396
|
+
qrFor(link),
|
|
370
397
|
"",
|
|
371
398
|
];
|
|
399
|
+
if (consumed) lines.push(` 🔒 One-time link used — locked to ${lockedIp}.`);
|
|
400
|
+
else lines.push(` Link valid ${bar(rem)} ${rem}s`);
|
|
401
|
+
lines.push("");
|
|
372
402
|
const rows = sessionRows();
|
|
373
403
|
if (rows.length) lines.push(` Connected (${rows.length}):`, ...rows);
|
|
374
404
|
else lines.push(" No active sessions yet.");
|
|
@@ -380,9 +410,10 @@ server.listen(PORT, "0.0.0.0", function () {
|
|
|
380
410
|
if (liveUI) render();
|
|
381
411
|
else console.log(`\n Open in cmux (regenerates in ${remainingSec()}s):\n ${buildLink()}\n`);
|
|
382
412
|
|
|
383
|
-
// Refresh every 5s: regenerate the link when it expires
|
|
413
|
+
// Refresh every 5s: regenerate the link when it expires (unless a one-time
|
|
414
|
+
// link has already been consumed — then it's frozen), then redraw.
|
|
384
415
|
setInterval(() => {
|
|
385
|
-
if (remainingSec() <= 0) {
|
|
416
|
+
if (!consumed && remainingSec() <= 0) {
|
|
386
417
|
regenerateToken();
|
|
387
418
|
if (!liveUI) console.log(`\n [link regenerated]\n ${buildLink()}\n`);
|
|
388
419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cmux-ssh-here",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Spin up a token-auth SSH server and print a cmux deep link to connect over LAN",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"node-pty": "^1.0.0",
|
|
37
|
+
"qrcode-terminal": "^0.12.0",
|
|
37
38
|
"ssh2": "^1.16.0"
|
|
38
39
|
}
|
|
39
40
|
}
|