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.
Files changed (2) hide show
  1. package/dist/bin.js +77 -17
  2. 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 rawKey = options.keyBase64Url ? fromBase64Url(options.keyBase64Url) : await generateKey();
266
- const cryptoKey = await importKey(rawKey);
267
- const keyFragment = options.keyBase64Url ?? toBase64Url(rawKey);
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(relayTarget, cryptoKey, keyFragment, pty, options.background, options.sessionId);
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, key, keyFragment, pty, silent = false, sessionId) {
379
+ constructor(relayTarget, readKey, writeKey, readKeyFragment, writeKeyFragment, pty, silent = false, showReadonlyLink = false, sessionId) {
363
380
  this.relayTarget = relayTarget;
364
- this.key = key;
365
- this.keyFragment = keyFragment;
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()}#${this.keyFragment}`;
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()}#${this.keyFragment}`;
427
+ const readonlyUrl = `${readonlyShareUrl.toString()}#r=${encodeURIComponent(this.readKeyFragment)}`;
408
428
  if (!this.silent) {
409
429
  await printShareLinkWithQr("interactive", interactiveUrl);
410
- await printShareLinkWithQr("view-only", readonlyUrl);
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.key, frame.iv, frame.ciphertext, typeAAD(frame.type));
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.key, frame.iv, frame.ciphertext, typeAAD(frame.type));
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 { iv, ciphertext } = await encrypt(this.key, plaintext, typeAAD(type));
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
- keyBase64Url: parsed.keyBase64Url
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
- "--__key",
802
- options.keyBase64Url
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
- keyBase64Url: parsed.hash.slice(1)
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.7",
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"