fied 0.2.7 → 0.2.9
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/bin.js +77 -17
- package/package.json +2 -4
package/dist/bin.js
CHANGED
|
@@ -262,9 +262,13 @@ async function share(options) {
|
|
|
262
262
|
}
|
|
263
263
|
const cols = options.cols ?? process.stdout.columns ?? 80;
|
|
264
264
|
const rows = options.rows ?? process.stdout.rows ?? 24;
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
265
|
+
const legacyKey = options.keyBase64Url;
|
|
266
|
+
const readKeyRaw = options.readKeyBase64Url ? fromBase64Url(options.readKeyBase64Url) : legacyKey ? fromBase64Url(legacyKey) : await generateKey();
|
|
267
|
+
const writeKeyRaw = options.writeKeyBase64Url ? fromBase64Url(options.writeKeyBase64Url) : legacyKey ? fromBase64Url(legacyKey) : await generateKey();
|
|
268
|
+
const readKey = await importKey(readKeyRaw);
|
|
269
|
+
const writeKey = await importKey(writeKeyRaw);
|
|
270
|
+
const readKeyFragment = options.readKeyBase64Url ?? legacyKey ?? toBase64Url(readKeyRaw);
|
|
271
|
+
const writeKeyFragment = options.writeKeyBase64Url ?? legacyKey ?? toBase64Url(writeKeyRaw);
|
|
268
272
|
const pty = attachSession(targetSession, cols, rows);
|
|
269
273
|
if (!options.background) {
|
|
270
274
|
console.log("");
|
|
@@ -274,7 +278,17 @@ async function share(options) {
|
|
|
274
278
|
console.log(` Size: ${cols}x${rows}`);
|
|
275
279
|
console.log("");
|
|
276
280
|
}
|
|
277
|
-
const bridge = new RelayBridge(
|
|
281
|
+
const bridge = new RelayBridge(
|
|
282
|
+
relayTarget,
|
|
283
|
+
readKey,
|
|
284
|
+
writeKey,
|
|
285
|
+
readKeyFragment,
|
|
286
|
+
writeKeyFragment,
|
|
287
|
+
pty,
|
|
288
|
+
options.background,
|
|
289
|
+
options.showReadonlyLink,
|
|
290
|
+
options.sessionId
|
|
291
|
+
);
|
|
278
292
|
const onUrl = (url) => {
|
|
279
293
|
if (options.background) {
|
|
280
294
|
const sessionId = bridge.getSessionId();
|
|
@@ -352,6 +366,9 @@ async function createSession(relayHttpBase) {
|
|
|
352
366
|
throw new Error(`Failed to create session: ${res.status} ${res.statusText}`);
|
|
353
367
|
}
|
|
354
368
|
const data = await res.json();
|
|
369
|
+
if (typeof data.sessionId !== "string") {
|
|
370
|
+
throw new Error("Invalid session creation response");
|
|
371
|
+
}
|
|
355
372
|
return data.sessionId;
|
|
356
373
|
}
|
|
357
374
|
var WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
@@ -359,12 +376,15 @@ function typeAAD(type) {
|
|
|
359
376
|
return new Uint8Array([type & 255]);
|
|
360
377
|
}
|
|
361
378
|
var RelayBridge = class {
|
|
362
|
-
constructor(relayTarget,
|
|
379
|
+
constructor(relayTarget, readKey, writeKey, readKeyFragment, writeKeyFragment, pty, silent = false, showReadonlyLink = false, sessionId) {
|
|
363
380
|
this.relayTarget = relayTarget;
|
|
364
|
-
this.
|
|
365
|
-
this.
|
|
381
|
+
this.readKey = readKey;
|
|
382
|
+
this.writeKey = writeKey;
|
|
383
|
+
this.readKeyFragment = readKeyFragment;
|
|
384
|
+
this.writeKeyFragment = writeKeyFragment;
|
|
366
385
|
this.pty = pty;
|
|
367
386
|
this.silent = silent;
|
|
387
|
+
this.showReadonlyLink = showReadonlyLink;
|
|
368
388
|
this.sessionId = sessionId ?? null;
|
|
369
389
|
this.pty.onData((data) => {
|
|
370
390
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -402,12 +422,14 @@ var RelayBridge = class {
|
|
|
402
422
|
return;
|
|
403
423
|
}
|
|
404
424
|
const shareUrl = new URL(`s/${this.sessionId}`, this.relayTarget.httpBase);
|
|
405
|
-
const interactiveUrl = `${shareUrl.toString()}
|
|
425
|
+
const interactiveUrl = `${shareUrl.toString()}#r=${encodeURIComponent(this.readKeyFragment)}&w=${encodeURIComponent(this.writeKeyFragment)}`;
|
|
406
426
|
const readonlyShareUrl = new URL(`${shareUrl.pathname.replace(/\/$/, "")}/v`, shareUrl);
|
|
407
|
-
const readonlyUrl = `${readonlyShareUrl.toString()}
|
|
427
|
+
const readonlyUrl = `${readonlyShareUrl.toString()}#r=${encodeURIComponent(this.readKeyFragment)}`;
|
|
408
428
|
if (!this.silent) {
|
|
409
429
|
await printShareLinkWithQr("interactive", interactiveUrl);
|
|
410
|
-
|
|
430
|
+
if (this.showReadonlyLink) {
|
|
431
|
+
await printShareLinkWithQr("view-only", readonlyUrl);
|
|
432
|
+
}
|
|
411
433
|
console.log("");
|
|
412
434
|
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
413
435
|
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
@@ -446,7 +468,7 @@ var RelayBridge = class {
|
|
|
446
468
|
const data = new Uint8Array(raw);
|
|
447
469
|
const frame = parseFrame(data);
|
|
448
470
|
if (frame.type === MSG_TERMINAL_INPUT) {
|
|
449
|
-
const plaintext = await decrypt(this.
|
|
471
|
+
const plaintext = await decrypt(this.writeKey, frame.iv, frame.ciphertext, typeAAD(frame.type));
|
|
450
472
|
const input = parseInputPayload(this.decoder.decode(plaintext));
|
|
451
473
|
if (!input || this.isReplayNonce(input.nonce)) {
|
|
452
474
|
this.invalidInputFrames += 1;
|
|
@@ -459,7 +481,7 @@ var RelayBridge = class {
|
|
|
459
481
|
this.rememberInputNonce(input.nonce);
|
|
460
482
|
this.pty.write(input.data);
|
|
461
483
|
} else if (frame.type === MSG_RESIZE) {
|
|
462
|
-
const plaintext = await decrypt(this.
|
|
484
|
+
const plaintext = await decrypt(this.writeKey, frame.iv, frame.ciphertext, typeAAD(frame.type));
|
|
463
485
|
const resize = parseResizePayload(this.decoder.decode(plaintext));
|
|
464
486
|
if (!resize) {
|
|
465
487
|
this.invalidResizeFrames += 1;
|
|
@@ -518,7 +540,8 @@ var RelayBridge = class {
|
|
|
518
540
|
async sendEncrypted(type, plaintext) {
|
|
519
541
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
520
542
|
try {
|
|
521
|
-
const
|
|
543
|
+
const key = type === MSG_TERMINAL_OUTPUT ? this.readKey : this.writeKey;
|
|
544
|
+
const { iv, ciphertext } = await encrypt(key, plaintext, typeAAD(type));
|
|
522
545
|
const frame = frameMessage(type, iv, ciphertext);
|
|
523
546
|
this.ws.send(frame);
|
|
524
547
|
} catch (err) {
|
|
@@ -661,12 +684,14 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
661
684
|
\x1B[1mOptions:\x1B[0m
|
|
662
685
|
--session, -s <name> tmux session to share (auto-detected if only one)
|
|
663
686
|
--relay <url> relay server URL (default: https://fied.app)
|
|
687
|
+
--view-only print a separate view-only share link
|
|
664
688
|
--allow-insecure-relay allow http://localhost relay (dev only)
|
|
665
689
|
--help, -h show this help
|
|
666
690
|
|
|
667
691
|
\x1B[1mExamples:\x1B[0m
|
|
668
692
|
npx fied share the only tmux session
|
|
669
693
|
npx fied -s mysession share a specific session
|
|
694
|
+
npx fied --view-only include a view-only link
|
|
670
695
|
npx fied --relay http://localhost:8787 use a local relay
|
|
671
696
|
`);
|
|
672
697
|
process.exit(0);
|
|
@@ -680,8 +705,14 @@ if (args.includes("--__daemon")) {
|
|
|
680
705
|
options.relay = args[++i];
|
|
681
706
|
} else if (args[i] === "--allow-insecure-relay") {
|
|
682
707
|
options.allowInsecureRelay = true;
|
|
708
|
+
} else if (args[i] === "--view-only") {
|
|
709
|
+
options.showReadonlyLink = true;
|
|
683
710
|
} else if (args[i] === "--__session-id" && args[i + 1]) {
|
|
684
711
|
options.sessionId = args[++i];
|
|
712
|
+
} else if (args[i] === "--__read-key" && args[i + 1]) {
|
|
713
|
+
options.readKeyBase64Url = args[++i];
|
|
714
|
+
} else if (args[i] === "--__write-key" && args[i + 1]) {
|
|
715
|
+
options.writeKeyBase64Url = args[++i];
|
|
685
716
|
} else if (args[i] === "--__key" && args[i + 1]) {
|
|
686
717
|
options.keyBase64Url = args[++i];
|
|
687
718
|
}
|
|
@@ -697,6 +728,8 @@ async function main() {
|
|
|
697
728
|
let relay;
|
|
698
729
|
let session;
|
|
699
730
|
let allowInsecureRelay = false;
|
|
731
|
+
let showReadonlyLink = false;
|
|
732
|
+
let showReadonlyLinkExplicit = false;
|
|
700
733
|
for (let i = 0; i < args.length; i++) {
|
|
701
734
|
if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
|
|
702
735
|
session = args[++i];
|
|
@@ -704,6 +737,9 @@ async function main() {
|
|
|
704
737
|
relay = args[++i];
|
|
705
738
|
} else if (args[i] === "--allow-insecure-relay") {
|
|
706
739
|
allowInsecureRelay = true;
|
|
740
|
+
} else if (args[i] === "--view-only") {
|
|
741
|
+
showReadonlyLink = true;
|
|
742
|
+
showReadonlyLinkExplicit = true;
|
|
707
743
|
} else if (!args[i].startsWith("-")) {
|
|
708
744
|
continue;
|
|
709
745
|
} else {
|
|
@@ -763,10 +799,14 @@ async function main() {
|
|
|
763
799
|
if (!session) {
|
|
764
800
|
throw new Error("No tmux session selected");
|
|
765
801
|
}
|
|
802
|
+
if (!showReadonlyLinkExplicit && process.stdin.isTTY && process.stderr.isTTY) {
|
|
803
|
+
showReadonlyLink = await confirm("Enable view-only share link?");
|
|
804
|
+
}
|
|
766
805
|
await share({
|
|
767
806
|
session,
|
|
768
807
|
relay,
|
|
769
808
|
allowInsecureRelay,
|
|
809
|
+
showReadonlyLink,
|
|
770
810
|
onShareUrl: async (url) => {
|
|
771
811
|
const background = await confirm("Run in background?");
|
|
772
812
|
if (!background) {
|
|
@@ -778,7 +818,8 @@ async function main() {
|
|
|
778
818
|
relay,
|
|
779
819
|
allowInsecureRelay,
|
|
780
820
|
sessionId: parsed.sessionId,
|
|
781
|
-
|
|
821
|
+
readKeyBase64Url: parsed.readKeyBase64Url,
|
|
822
|
+
writeKeyBase64Url: parsed.writeKeyBase64Url
|
|
782
823
|
});
|
|
783
824
|
console.error("");
|
|
784
825
|
console.error(` \x1B[1m\x1B[32mfied\x1B[0m \u2014 moved to background (PID ${child.pid})`);
|
|
@@ -798,8 +839,10 @@ function spawnBackground(options) {
|
|
|
798
839
|
options.session,
|
|
799
840
|
"--__session-id",
|
|
800
841
|
options.sessionId,
|
|
801
|
-
"--
|
|
802
|
-
options.
|
|
842
|
+
"--__read-key",
|
|
843
|
+
options.readKeyBase64Url,
|
|
844
|
+
"--__write-key",
|
|
845
|
+
options.writeKeyBase64Url
|
|
803
846
|
];
|
|
804
847
|
if (options.relay) childArgs.push("--relay", options.relay);
|
|
805
848
|
if (options.allowInsecureRelay) childArgs.push("--allow-insecure-relay");
|
|
@@ -816,9 +859,26 @@ function parseShareUrl(url) {
|
|
|
816
859
|
if (!match || !parsed.hash) {
|
|
817
860
|
throw new Error("Invalid share URL");
|
|
818
861
|
}
|
|
862
|
+
const hashRaw = parsed.hash.slice(1);
|
|
863
|
+
let readKeyBase64Url = "";
|
|
864
|
+
let writeKeyBase64Url = "";
|
|
865
|
+
if (hashRaw.includes("=") || hashRaw.includes("&")) {
|
|
866
|
+
const params = new URLSearchParams(hashRaw);
|
|
867
|
+
const read = params.get("r");
|
|
868
|
+
const write = params.get("w");
|
|
869
|
+
if (read && write) {
|
|
870
|
+
readKeyBase64Url = read;
|
|
871
|
+
writeKeyBase64Url = write;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (!readKeyBase64Url || !writeKeyBase64Url) {
|
|
875
|
+
readKeyBase64Url = hashRaw;
|
|
876
|
+
writeKeyBase64Url = hashRaw;
|
|
877
|
+
}
|
|
819
878
|
return {
|
|
820
879
|
sessionId: match[1],
|
|
821
|
-
|
|
880
|
+
readKeyBase64Url,
|
|
881
|
+
writeKeyBase64Url
|
|
822
882
|
};
|
|
823
883
|
}
|
|
824
884
|
function timeSince(date) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fied",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Share your tmux session in the browser with end-to-end encryption",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
7
|
-
"fied": "./dist/bin.js"
|
|
8
|
-
},
|
|
6
|
+
"bin": "dist/bin.js",
|
|
9
7
|
"files": [
|
|
10
8
|
"dist/bin.js",
|
|
11
9
|
"README.md"
|