fied 0.2.1 → 0.2.2

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 +105 -24
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -52,6 +52,22 @@ function toBase64Url(bytes) {
52
52
  }
53
53
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
54
54
  }
55
+ function fromBase64Url(str) {
56
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
57
+ while (base64.length % 4 !== 0) {
58
+ base64 += "=";
59
+ }
60
+ if (typeof atob === "function") {
61
+ const binary = atob(base64);
62
+ const bytes = new Uint8Array(binary.length);
63
+ for (let i = 0; i < binary.length; i++) {
64
+ bytes[i] = binary.charCodeAt(i);
65
+ }
66
+ return bytes;
67
+ } else {
68
+ return new Uint8Array(Buffer.from(base64, "base64"));
69
+ }
70
+ }
55
71
 
56
72
  // ../crypto/dist/protocol.js
57
73
  var IV_LENGTH2 = 12;
@@ -166,6 +182,7 @@ var DEFAULT_RELAY = "https://fied.app";
166
182
  var MSG_TERMINAL_OUTPUT = 1;
167
183
  var MSG_TERMINAL_INPUT = 2;
168
184
  var MSG_RESIZE = 3;
185
+ var VIEWER_JOINED = "__fied_viewer_joined__";
169
186
  var RESIZE_MIN_COLS = 20;
170
187
  var RESIZE_MAX_COLS = 1e3;
171
188
  var RESIZE_MIN_ROWS = 5;
@@ -201,9 +218,9 @@ async function share(options) {
201
218
  }
202
219
  const cols = options.cols ?? process.stdout.columns ?? 80;
203
220
  const rows = options.rows ?? process.stdout.rows ?? 24;
204
- const rawKey = await generateKey();
221
+ const rawKey = options.keyBase64Url ? fromBase64Url(options.keyBase64Url) : await generateKey();
205
222
  const cryptoKey = await importKey(rawKey);
206
- const keyFragment = toBase64Url(rawKey);
223
+ const keyFragment = options.keyBase64Url ?? toBase64Url(rawKey);
207
224
  const pty = attachSession(targetSession, cols, rows);
208
225
  if (!options.background) {
209
226
  console.log("");
@@ -213,7 +230,8 @@ async function share(options) {
213
230
  console.log(` Size: ${cols}x${rows}`);
214
231
  console.log("");
215
232
  }
216
- const bridge = new RelayBridge(relayTarget, cryptoKey, keyFragment, pty, options.background);
233
+ const bridge = new RelayBridge(relayTarget, cryptoKey, keyFragment, pty, options.background, options.sessionId);
234
+ let handoffRequested = false;
217
235
  const onUrl = (url) => {
218
236
  if (options.background) {
219
237
  addSession({
@@ -224,8 +242,24 @@ async function share(options) {
224
242
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
225
243
  });
226
244
  }
245
+ if (options.onShareUrl) {
246
+ return Promise.resolve(options.onShareUrl(url)).then((action) => {
247
+ if (action === "handoff") {
248
+ handoffRequested = true;
249
+ }
250
+ });
251
+ }
252
+ return Promise.resolve();
227
253
  };
228
254
  await bridge.connect(onUrl);
255
+ if (handoffRequested) {
256
+ bridge.destroy();
257
+ try {
258
+ pty.kill();
259
+ } catch {
260
+ }
261
+ return;
262
+ }
229
263
  let closed = false;
230
264
  const cleanup = () => {
231
265
  if (closed) return;
@@ -290,12 +324,13 @@ async function createSession(relayHttpBase) {
290
324
  }
291
325
  var WS_CONNECT_TIMEOUT_MS = 1e4;
292
326
  var RelayBridge = class {
293
- constructor(relayTarget, key, keyFragment, pty, silent = false) {
327
+ constructor(relayTarget, key, keyFragment, pty, silent = false, sessionId) {
294
328
  this.relayTarget = relayTarget;
295
329
  this.key = key;
296
330
  this.keyFragment = keyFragment;
297
331
  this.pty = pty;
298
332
  this.silent = silent;
333
+ this.sessionId = sessionId ?? null;
299
334
  this.pty.onData((data) => {
300
335
  if (this.ws?.readyState === WebSocket.OPEN) {
301
336
  this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
@@ -327,7 +362,6 @@ var RelayBridge = class {
327
362
  }
328
363
  const shareUrl = new URL(`s/${this.sessionId}`, this.relayTarget.httpBase);
329
364
  const url = `${shareUrl.toString()}#${this.keyFragment}`;
330
- this.onUrl?.(url);
331
365
  if (!this.silent) {
332
366
  console.log(` \x1B[1mShare this link:\x1B[0m`);
333
367
  console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
@@ -336,6 +370,7 @@ var RelayBridge = class {
336
370
  console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
337
371
  console.log("");
338
372
  }
373
+ await this.onUrl?.(url);
339
374
  }
340
375
  const wsUrl = new URL(`api/sessions/${this.sessionId}/ws`, this.relayTarget.wsBase);
341
376
  wsUrl.searchParams.set("role", "host");
@@ -359,6 +394,8 @@ var RelayBridge = class {
359
394
  const text = this.decoder.decode(raw);
360
395
  if (text === "__fied_ping__") {
361
396
  ws.send("__fied_pong__");
397
+ } else if (text === VIEWER_JOINED) {
398
+ this.pty.write("\f");
362
399
  }
363
400
  return;
364
401
  }
@@ -543,6 +580,10 @@ if (args.includes("--__daemon")) {
543
580
  options.relay = args[++i];
544
581
  } else if (args[i] === "--allow-insecure-relay") {
545
582
  options.allowInsecureRelay = true;
583
+ } else if (args[i] === "--__session-id" && args[i + 1]) {
584
+ options.sessionId = args[++i];
585
+ } else if (args[i] === "--__key" && args[i + 1]) {
586
+ options.keyBase64Url = args[++i];
546
587
  }
547
588
  }
548
589
  share({ ...options, background: true }).catch(() => process.exit(1));
@@ -618,26 +659,66 @@ async function main() {
618
659
  session = session.split(" \u2014 ")[0].trim();
619
660
  }
620
661
  }
621
- const background = await confirm("Run in background?");
622
- if (background) {
623
- const binPath = fileURLToPath(import.meta.url);
624
- const childArgs = ["--__daemon", "--session", session];
625
- if (relay) childArgs.push("--relay", relay);
626
- if (allowInsecureRelay) childArgs.push("--allow-insecure-relay");
627
- const child = spawnChild(process.execPath, [binPath, ...childArgs], {
628
- detached: true,
629
- stdio: "ignore"
630
- });
631
- child.unref();
632
- console.error("");
633
- console.error(` \x1B[1m\x1B[32mfied\x1B[0m \u2014 started in background (PID ${child.pid})`);
634
- console.error(` Session: ${session}`);
635
- console.error(" Run \x1B[1mnpx fied\x1B[0m again to manage.");
636
- console.error("");
637
- setTimeout(() => process.exit(0), 500);
638
- } else {
639
- await share({ session, relay, allowInsecureRelay });
662
+ if (!session) {
663
+ throw new Error("No tmux session selected");
640
664
  }
665
+ await share({
666
+ session,
667
+ relay,
668
+ allowInsecureRelay,
669
+ onShareUrl: async (url) => {
670
+ const background = await confirm("Run in background?");
671
+ if (!background) {
672
+ return "continue";
673
+ }
674
+ const parsed = parseShareUrl(url);
675
+ const child = spawnBackground({
676
+ session,
677
+ relay,
678
+ allowInsecureRelay,
679
+ sessionId: parsed.sessionId,
680
+ keyBase64Url: parsed.keyBase64Url
681
+ });
682
+ console.error("");
683
+ console.error(` \x1B[1m\x1B[32mfied\x1B[0m \u2014 moved to background (PID ${child.pid})`);
684
+ console.error(` Session: ${session}`);
685
+ console.error(" Same share link stays active.");
686
+ console.error(" Run \x1B[1mnpx fied\x1B[0m again to manage.");
687
+ console.error("");
688
+ return "handoff";
689
+ }
690
+ });
691
+ }
692
+ function spawnBackground(options) {
693
+ const binPath = fileURLToPath(import.meta.url);
694
+ const childArgs = [
695
+ "--__daemon",
696
+ "--session",
697
+ options.session,
698
+ "--__session-id",
699
+ options.sessionId,
700
+ "--__key",
701
+ options.keyBase64Url
702
+ ];
703
+ if (options.relay) childArgs.push("--relay", options.relay);
704
+ if (options.allowInsecureRelay) childArgs.push("--allow-insecure-relay");
705
+ const child = spawnChild(process.execPath, [binPath, ...childArgs], {
706
+ detached: true,
707
+ stdio: "ignore"
708
+ });
709
+ child.unref();
710
+ return child;
711
+ }
712
+ function parseShareUrl(url) {
713
+ const parsed = new URL(url);
714
+ const match = parsed.pathname.match(/^\/s\/([A-Za-z0-9_-]{8,64})$/);
715
+ if (!match || !parsed.hash) {
716
+ throw new Error("Invalid share URL");
717
+ }
718
+ return {
719
+ sessionId: match[1],
720
+ keyBase64Url: parsed.hash.slice(1)
721
+ };
641
722
  }
642
723
  function timeSince(date) {
643
724
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fied",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Share your tmux session in the browser with end-to-end encryption",
5
5
  "type": "module",
6
6
  "bin": {